Skip to content

Commit 2ca1119

Browse files
authored
Continue tests after a crash when using isolated = false (#1)
* Add ability to continue test execution after failure with isolated=true * Fix bug where isolated=false would execute all the tests. * Write tests to a file instead of passing them as arguments avoiding the argument limitation.
1 parent 0fc3552 commit 2ca1119

File tree

4 files changed

+162
-21
lines changed

4 files changed

+162
-21
lines changed

build_and_install.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
3+
# Script to build AndroidX Test m2repository and install orchestrator APKs
4+
# Based on CONTRIBUTING.md instructions
5+
6+
set -e # Exit on any error
7+
8+
echo "🔨 Building AndroidX Test m2repository..."
9+
bazelisk build :axt_m2repository
10+
11+
echo "📦 Unpacking m2repository to ~/.m2/"
12+
unzip -o bazel-bin/axt_m2repository.zip -d ~/.m2/
13+
14+
echo "📱 Installing orchestrator and services APKs..."
15+
16+
# Find and install test services APK
17+
SERVICES_APK=~/.m2/repository/androidx/test/services/test-services/1.7.0-alpha01/test-services-1.7.0-alpha01.apk
18+
if [ -n "$SERVICES_APK" ]; then
19+
echo "Installing test services APK: $SERVICES_APK"
20+
adb install --force-queryable -r "$SERVICES_APK"
21+
else
22+
echo "❌ Test services APK not found in ~/.m2/repository"
23+
exit 1
24+
fi
25+
26+
# Find and install orchestrator APK
27+
ORCHESTRATOR_APK=~/.m2/repository/androidx/test/orchestrator/1.7.0-alpha01/orchestrator-1.7.0-alpha01.apk
28+
if [ -n "$ORCHESTRATOR_APK" ]; then
29+
echo "Installing orchestrator APK: $ORCHESTRATOR_APK"
30+
adb install --force-queryable -r "$ORCHESTRATOR_APK"
31+
else
32+
echo "❌ Orchestrator APK not found in ~/.m2/repository"
33+
exit 1
34+
fi
35+
36+
echo "✅ Build and installation complete!"

runner/android_test_orchestrator/java/androidx/test/orchestrator/AndroidTestOrchestrator.java

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,26 +40,37 @@
4040
import android.os.Build;
4141
import android.os.Bundle;
4242
import android.os.Debug;
43+
import android.os.Environment;
4344
import android.os.IBinder;
4445
import android.os.RemoteException;
4546
import android.text.TextUtils;
4647
import android.util.Log;
48+
4749
import androidx.annotation.VisibleForTesting;
4850
import androidx.core.content.ContextCompat;
4951
import androidx.test.orchestrator.TestRunnable.RunFinishedListener;
5052
import androidx.test.orchestrator.junit.ParcelableDescription;
53+
import androidx.test.orchestrator.junit.ParcelableFailure;
5154
import androidx.test.orchestrator.listeners.OrchestrationListenerManager;
5255
import androidx.test.orchestrator.listeners.OrchestrationResult;
5356
import androidx.test.orchestrator.listeners.OrchestrationResultPrinter;
57+
import androidx.test.orchestrator.listeners.OrchestrationRunListener;
5458
import androidx.test.services.shellexecutor.ClientNotConnected;
5559
import androidx.test.services.shellexecutor.ShellExecSharedConstants;
5660
import androidx.test.services.shellexecutor.ShellExecutor;
5761
import androidx.test.services.shellexecutor.ShellExecutorFactory;
62+
63+
import java.io.BufferedWriter;
5864
import java.io.ByteArrayOutputStream;
65+
import java.io.File;
5966
import java.io.FileNotFoundException;
67+
import java.io.FileOutputStream;
6068
import java.io.IOException;
6169
import java.io.OutputStream;
70+
import java.io.OutputStreamWriter;
6271
import java.io.PrintStream;
72+
import java.nio.charset.StandardCharsets;
73+
import java.util.ArrayList;
6374
import java.util.Arrays;
6475
import java.util.HashMap;
6576
import java.util.Iterator;
@@ -127,6 +138,27 @@
127138
public final class AndroidTestOrchestrator extends android.app.Instrumentation
128139
implements RunFinishedListener {
129140

141+
private class OrchestratorListener extends OrchestrationRunListener {
142+
@Override
143+
public void testStarted(ParcelableDescription description) {
144+
if (!runsInIsolatedMode(arguments)) {
145+
test = description.getClassName() + "#" + description.getMethodName();
146+
testsToExecuteNoIsolation.remove(test);
147+
}
148+
}
149+
150+
@Override
151+
public void testFailure(ParcelableFailure failure) {
152+
isRecoveringFromCrash = true;
153+
}
154+
155+
@Override
156+
public void testProcessFinished(String message) {
157+
super.testProcessFinished(message);
158+
Log.i(TAG, "Test process finished: " + message);
159+
}
160+
}
161+
130162
private static final String TAG = "AndroidTestOrchestrator";
131163
// As defined in the AndroidManifest of the Orchestrator app.
132164
private static final String ORCHESTRATOR_SERVICE_LOCATION = "OrchestratorService";
@@ -145,6 +177,7 @@ public final class AndroidTestOrchestrator extends android.app.Instrumentation
145177
private final OrchestrationResultPrinter resultPrinter = new OrchestrationResultPrinter();
146178
private final OrchestrationListenerManager listenerManager =
147179
new OrchestrationListenerManager(this);
180+
private final OrchestratorListener orchestratorListener = new OrchestratorListener();
148181

149182
private final ExecutorService executorService;
150183

@@ -158,6 +191,9 @@ public final class AndroidTestOrchestrator extends android.app.Instrumentation
158191
private String test;
159192
private Iterator<String> testIterator;
160193

194+
private List<String> testsToExecuteNoIsolation;
195+
private boolean isRecoveringFromCrash = false;
196+
161197
public AndroidTestOrchestrator() {
162198
super();
163199
// We never want to execute multiple tests in parallel.
@@ -315,6 +351,7 @@ public void runFinished() {
315351
if (null == test) {
316352
List<String> allTests = callbackLogic.provideCollectedTests();
317353
testIterator = allTests.iterator();
354+
testsToExecuteNoIsolation = new ArrayList<>(allTests);
318355
addListeners(allTests.size());
319356

320357
if (allTests.isEmpty()) {
@@ -333,17 +370,76 @@ public void runFinished() {
333370
}
334371

335372
private void executeEntireTestSuite() {
373+
Log.i(TAG, "Executing entire test suite...");
374+
// If we're recovering from a crash, continue with remaining tests
375+
if (isRecoveringFromCrash && testsToExecuteNoIsolation != null && !testsToExecuteNoIsolation.isEmpty()) {
376+
isRecoveringFromCrash = false;
377+
executeRemainingTests();
378+
return;
379+
}
380+
336381
if (null != test) {
337382
finish(Activity.RESULT_OK, createResultBundle());
338383
return;
339384
}
340385

341-
// We don't actually need test to have any particular value,
342-
// just to indicate we've started execution.
386+
executeRemainingTests();
387+
}
388+
389+
private void executeRemainingTests() {
390+
Log.i(TAG, "Executing remaining tests...");
391+
// Create a list of remaining tests from the current iterator position
392+
List<String> remainingTests = new ArrayList<>(testsToExecuteNoIsolation);
393+
394+
if (remainingTests.isEmpty()) {
395+
Log.i(TAG, "No remaining tests to execute.");
396+
finish(Activity.RESULT_OK, createResultBundle());
397+
return;
398+
}
399+
400+
// Set test to indicate execution has started
343401
test = "";
402+
// Execute remaining tests using a subset TestRunnable, we need to prevent the argument list from being too long
403+
// Run 500 tests at a time
404+
Log.i(TAG, "Executing subset of remaining tests: " + remainingTests.size() + " tests.");
405+
406+
// Write tests to file to avoid argument length limits
407+
String testFilePath = writeTestsToFile(remainingTests);
408+
Log.i(TAG, "Test file path: " + testFilePath);
409+
344410
executorService.execute(
345-
TestRunnable.legacyTestRunnable(
346-
getContext(), getSecret(arguments), arguments, getOutputStream(), this));
411+
TestRunnable.testSubsetRunnable(
412+
getContext(), getSecret(arguments), arguments, getOutputStream(), this, testFilePath));
413+
}
414+
415+
private String writeTestsToFile(List<String> tests) {
416+
String fileName = "test_subset.txt";
417+
String filePath = "/data/local/tmp/" + fileName;
418+
Context context = getContext();
419+
String secret = getSecret(arguments);
420+
421+
try {
422+
// Create the file using shell command to ensure proper permissions
423+
execShellCommandSync(context, secret, "touch", Arrays.asList(filePath));
424+
425+
// Make the file world readable/writable so both processes can access it
426+
execShellCommandSync(context, secret, "chmod", Arrays.asList("666", filePath));
427+
428+
// Write the test content to the file using shell commands
429+
StringBuilder content = new StringBuilder();
430+
for (String test : tests) {
431+
content.append(test).append("\n");
432+
}
433+
434+
// Use echo to write content (escape any special characters)
435+
String escapedContent = content.toString().replace("\"", "\\\"").replace("$", "\\$");
436+
execShellCommandSync(context, secret, "sh", Arrays.asList("-c", "echo \"" + escapedContent + "\" > " + filePath));
437+
438+
return filePath;
439+
} catch (Exception e) {
440+
Log.e(TAG, "Failed to write tests to file using shell commands", e);
441+
throw new RuntimeException("Could not write tests to file", e);
442+
}
347443
}
348444

349445
private void executeNextTest() {
@@ -434,6 +530,7 @@ private String getOutputFile(String testName) {
434530
private void addListeners(int testSize) {
435531
listenerManager.addListener(resultBuilder);
436532
listenerManager.addListener(resultPrinter);
533+
listenerManager.addListener(orchestratorListener);
437534
listenerManager.orchestrationRunStarted(testSize);
438535
}
439536

runner/android_test_orchestrator/java/androidx/test/orchestrator/TestRunnable.java

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,65 +45,68 @@ public class TestRunnable implements Runnable {
4545
private final RunFinishedListener listener;
4646
private final OutputStream outputStream;
4747
private final String test;
48+
private final String testFilePath;
4849
private final boolean collectTests;
4950
private final Context context;
5051
private final String secret;
5152

5253
/**
53-
* Constructs a TestRunnable executes all tests in arguments.
54+
* Constructs a TestRunnable which will run a single test.
5455
*
5556
* @param context A context
5657
* @param secret A string representing the speakeasy binder key
5758
* @param arguments contains arguments to be passed to the target instrumentation
5859
* @param outputStream the stream to write the results of the test process
5960
* @param listener a callback listener to know when the run has completed
61+
* @param test contains a specific test#method to run. Will override whatever is specified in the
62+
* bundle.
6063
*/
61-
public static TestRunnable legacyTestRunnable(
64+
public static TestRunnable singleTestRunnable(
6265
Context context,
6366
String secret,
6467
Bundle arguments,
6568
OutputStream outputStream,
66-
RunFinishedListener listener) {
67-
return new TestRunnable(context, secret, arguments, outputStream, listener, null, false);
69+
RunFinishedListener listener,
70+
String test) {
71+
return new TestRunnable(context, secret, arguments, outputStream, listener, test, null, false);
6872
}
6973

7074
/**
71-
* Constructs a TestRunnable which will run a single test.
75+
* Constructs a TestRunnable which will ask the instrumentation to list out its tests.
7276
*
7377
* @param context A context
7478
* @param secret A string representing the speakeasy binder key
7579
* @param arguments contains arguments to be passed to the target instrumentation
7680
* @param outputStream the stream to write the results of the test process
7781
* @param listener a callback listener to know when the run has completed
78-
* @param test contains a specific test#method to run. Will override whatever is specified in the
79-
* bundle.
8082
*/
81-
public static TestRunnable singleTestRunnable(
83+
public static TestRunnable testCollectionRunnable(
8284
Context context,
8385
String secret,
8486
Bundle arguments,
8587
OutputStream outputStream,
86-
RunFinishedListener listener,
87-
String test) {
88-
return new TestRunnable(context, secret, arguments, outputStream, listener, test, false);
88+
RunFinishedListener listener) {
89+
return new TestRunnable(context, secret, arguments, outputStream, listener, null, null, true);
8990
}
9091

9192
/**
92-
* Constructs a TestRunnable which will ask the instrumentation to list out its tests.
93+
* Constructs a TestRunnable which will run a specific subset of tests from a file.
9394
*
9495
* @param context A context
9596
* @param secret A string representing the speakeasy binder key
9697
* @param arguments contains arguments to be passed to the target instrumentation
9798
* @param outputStream the stream to write the results of the test process
9899
* @param listener a callback listener to know when the run has completed
100+
* @param testFilePath the path to a file containing the tests to run
99101
*/
100-
public static TestRunnable testCollectionRunnable(
102+
public static TestRunnable testSubsetRunnable(
101103
Context context,
102104
String secret,
103105
Bundle arguments,
104106
OutputStream outputStream,
105-
RunFinishedListener listener) {
106-
return new TestRunnable(context, secret, arguments, outputStream, listener, null, true);
107+
RunFinishedListener listener,
108+
String testFilePath) {
109+
return new TestRunnable(context, secret, arguments, outputStream, listener, null, testFilePath, false);
107110
}
108111

109112
@VisibleForTesting
@@ -114,13 +117,15 @@ public static TestRunnable testCollectionRunnable(
114117
OutputStream outputStream,
115118
RunFinishedListener listener,
116119
String test,
120+
String testFilePath,
117121
boolean collectTests) {
118122
this.context = context;
119123
this.secret = secret;
120124
this.arguments = new Bundle(arguments);
121125
this.outputStream = outputStream;
122126
this.listener = listener;
123127
this.test = test;
128+
this.testFilePath = testFilePath;
124129
this.collectTests = collectTests;
125130
}
126131

@@ -178,9 +183,12 @@ private Bundle getTargetInstrumentationArguments() {
178183
targetArgs.remove("testFile");
179184
}
180185

181-
// Override the class parameter with the current test target.
186+
// Override the class parameter with the current test target or use testFile for test subset.
182187
if (test != null) {
183188
targetArgs.putString(AJUR_CLASS_ARGUMENT, test);
189+
} else if (testFilePath != null && !testFilePath.isEmpty()) {
190+
// For test subset, use testFile parameter to read tests from file
191+
targetArgs.putString("testFile", testFilePath);
184192
}
185193

186194
return targetArgs;

runner/android_test_orchestrator/javatests/androidx/test/orchestrator/TestRunnableTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ private static class FakeTestRunnable extends TestRunnable {
5959
RunFinishedListener listener,
6060
String test,
6161
boolean collectTests) {
62-
super(context, secret, arguments, outputStream, listener, test, collectTests);
62+
super(context, secret, arguments, outputStream, listener, test, null, collectTests);
6363
}
6464

6565
@Override

0 commit comments

Comments
 (0)