4040import android .os .Build ;
4141import android .os .Bundle ;
4242import android .os .Debug ;
43+ import android .os .Environment ;
4344import android .os .IBinder ;
4445import android .os .RemoteException ;
4546import android .text .TextUtils ;
4647import android .util .Log ;
48+
4749import androidx .annotation .VisibleForTesting ;
4850import androidx .core .content .ContextCompat ;
4951import androidx .test .orchestrator .TestRunnable .RunFinishedListener ;
5052import androidx .test .orchestrator .junit .ParcelableDescription ;
53+ import androidx .test .orchestrator .junit .ParcelableFailure ;
5154import androidx .test .orchestrator .listeners .OrchestrationListenerManager ;
5255import androidx .test .orchestrator .listeners .OrchestrationResult ;
5356import androidx .test .orchestrator .listeners .OrchestrationResultPrinter ;
57+ import androidx .test .orchestrator .listeners .OrchestrationRunListener ;
5458import androidx .test .services .shellexecutor .ClientNotConnected ;
5559import androidx .test .services .shellexecutor .ShellExecSharedConstants ;
5660import androidx .test .services .shellexecutor .ShellExecutor ;
5761import androidx .test .services .shellexecutor .ShellExecutorFactory ;
62+
63+ import java .io .BufferedWriter ;
5864import java .io .ByteArrayOutputStream ;
65+ import java .io .File ;
5966import java .io .FileNotFoundException ;
67+ import java .io .FileOutputStream ;
6068import java .io .IOException ;
6169import java .io .OutputStream ;
70+ import java .io .OutputStreamWriter ;
6271import java .io .PrintStream ;
72+ import java .nio .charset .StandardCharsets ;
73+ import java .util .ArrayList ;
6374import java .util .Arrays ;
6475import java .util .HashMap ;
6576import java .util .Iterator ;
127138public 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
0 commit comments