1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.testtype;
17 
18 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
19 import com.android.tradefed.build.IBuildInfo;
20 import com.android.tradefed.config.IConfiguration;
21 import com.android.tradefed.config.IConfigurationReceiver;
22 import com.android.tradefed.config.Option;
23 import com.android.tradefed.config.Option.Importance;
24 import com.android.tradefed.config.OptionClass;
25 import com.android.tradefed.device.DeviceNotAvailableException;
26 import com.android.tradefed.error.HarnessRuntimeException;
27 import com.android.tradefed.invoker.TestInformation;
28 import com.android.tradefed.invoker.logger.CurrentInvocation;
29 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
30 import com.android.tradefed.isolation.FilterSpec;
31 import com.android.tradefed.isolation.JUnitEvent;
32 import com.android.tradefed.isolation.RunnerMessage;
33 import com.android.tradefed.isolation.RunnerOp;
34 import com.android.tradefed.isolation.RunnerReply;
35 import com.android.tradefed.isolation.TestParameters;
36 import com.android.tradefed.log.LogUtil.CLog;
37 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
38 import com.android.tradefed.result.FailureDescription;
39 import com.android.tradefed.result.FileInputStreamSource;
40 import com.android.tradefed.result.ITestInvocationListener;
41 import com.android.tradefed.result.InputStreamSource;
42 import com.android.tradefed.result.LogDataType;
43 import com.android.tradefed.result.TestDescription;
44 import com.android.tradefed.result.error.InfraErrorIdentifier;
45 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
46 import com.android.tradefed.util.FileUtil;
47 import com.android.tradefed.util.ResourceUtil;
48 import com.android.tradefed.util.RunUtil;
49 import com.android.tradefed.util.StreamUtil;
50 import com.android.tradefed.util.SystemUtil;
51 
52 import com.google.common.annotations.VisibleForTesting;
53 
54 import java.io.File;
55 import java.io.FileNotFoundException;
56 import java.io.IOException;
57 import java.lang.ProcessBuilder.Redirect;
58 import java.net.ServerSocket;
59 import java.net.Socket;
60 import java.net.SocketTimeoutException;
61 import java.time.Duration;
62 import java.time.Instant;
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.Comparator;
66 import java.util.HashMap;
67 import java.util.HashSet;
68 import java.util.LinkedHashSet;
69 import java.util.List;
70 import java.util.Set;
71 import java.util.concurrent.TimeUnit;
72 import java.util.stream.Collectors;
73 
74 /**
75  * Implements a TradeFed runner that uses a subprocess to execute the tests in a low-dependency
76  * environment instead of executing them on the main process.
77  *
78  * <p>This runner assumes that all of the jars configured are in the same test directory and
79  * launches the subprocess in that directory. Since it must choose a working directory for the
80  * subprocess, and many tests benefit from that directory being the test directory, this was the
81  * best compromise available.
82  */
83 @OptionClass(alias = "isolated-host-test")
84 public class IsolatedHostTest
85         implements IRemoteTest,
86                 IBuildReceiver,
87                 ITestAnnotationFilterReceiver,
88                 ITestFilterReceiver,
89                 IConfigurationReceiver,
90                 ITestCollector {
91     @Option(
92             name = "class",
93             description =
94                     "The JUnit test classes to run, in the format <package>.<class>. eg."
95                             + " \"com.android.foo.Bar\". This field can be repeated.",
96             importance = Importance.IF_UNSET)
97     private Set<String> mClasses = new LinkedHashSet<>();
98 
99     @Option(
100             name = "jar",
101             description = "The jars containing the JUnit test class to run.",
102             importance = Importance.IF_UNSET)
103     private Set<String> mJars = new HashSet<String>();
104 
105     @Option(
106             name = "socket-timeout",
107             description =
108                     "The longest allowable time between messages from the subprocess before "
109                             + "assuming that it has malfunctioned or died.",
110             importance = Importance.IF_UNSET)
111     private int mSocketTimeout = 1 * 60 * 1000;
112 
113     @Option(
114             name = "include-annotation",
115             description = "The set of annotations a test must have to be run.")
116     private Set<String> mIncludeAnnotations = new HashSet<>();
117 
118     @Option(
119             name = "exclude-annotation",
120             description =
121                     "The set of annotations to exclude tests from running. A test must have "
122                             + "none of the annotations in this list to run.")
123     private Set<String> mExcludeAnnotations = new HashSet<>();
124 
125     @Option(
126             name = "java-flags",
127             description =
128                     "The set of flags to pass to the Java subprocess for complicated test "
129                             + "needs.")
130     private List<String> mJavaFlags = new ArrayList<>();
131 
132     @Option(
133             name = "use-robolectric-resources",
134             description =
135                     "Option to put the Robolectric specific resources directory option on "
136                             + "the Java command line.")
137     private boolean mRobolectricResources = false;
138 
139     @Option(
140             name = "exclude-paths",
141             description = "The (prefix) paths to exclude from searching in the jars.")
142     private Set<String> mExcludePaths =
143             new HashSet<>(Arrays.asList("org/junit", "com/google/common/collect/testing/google"));
144 
145     @Option(
146             name = "exclude-robolectric-packages",
147             description =
148                     "Indicates whether to exclude 'org/robolectric' when robolectric resources."
149                             + " Defaults to be true.")
150     private boolean mExcludeRobolectricPackages = true;
151 
152     @Option(
153             name = "java-folder",
154             description = "The JDK to be used. If unset, the JDK on $PATH will be used.")
155     private File mJdkFolder = null;
156 
157     @Option(
158             name = "classpath-override",
159             description =
160                     "[Local Debug Only] Force a classpath (isolation runner dependencies are still"
161                             + " added to this classpath)")
162     private String mClasspathOverride = null;
163 
164     @Option(
165             name = "robolectric-android-all-name",
166             description =
167                     "The android-all resource jar to be used, e.g."
168                             + " 'android-all-R-robolectric-r0.jar'")
169     private String mAndroidAllName = "android-all-current-robolectric-r0.jar";
170 
171     @Option(
172             name = TestTimeoutEnforcer.TEST_CASE_TIMEOUT_OPTION,
173             description = TestTimeoutEnforcer.TEST_CASE_TIMEOUT_DESCRIPTION)
174     private Duration mTestCaseTimeout = Duration.ofSeconds(0L);
175 
176     @Option(
177             name = "use-ravenwood-resources",
178             description =
179                     "Option to put the Ravenwood specific resources directory option on "
180                             + "the Java command line.")
181     private boolean mRavenwoodResources = false;
182 
183     private static final String QUALIFIED_PATH = "/com/android/tradefed/isolation";
184     private IBuildInfo mBuildInfo;
185     private Set<String> mIncludeFilters = new HashSet<>();
186     private Set<String> mExcludeFilters = new HashSet<>();
187     private boolean mCollectTestsOnly = false;
188     private File mSubprocessLog;
189     private File mWorkDir;
190     private boolean mReportedFailure = false;
191 
192     private static final String ROOT_DIR = "ROOT_DIR";
193     private ServerSocket mServer = null;
194 
195     private File mIsolationJar;
196 
197     private boolean debug = false;
198 
199     private IConfiguration mConfig = null;
200 
201     private File mCoverageExecFile;
202 
setDebug(boolean debug)203     public void setDebug(boolean debug) {
204         this.debug = debug;
205     }
206 
207     /** {@inheritDoc} */
208     @Override
run(TestInformation testInfo, ITestInvocationListener listener)209     public void run(TestInformation testInfo, ITestInvocationListener listener)
210             throws DeviceNotAvailableException {
211         mReportedFailure = false;
212         Process isolationRunner = null;
213         File artifactsDir = null;
214 
215         try {
216             mServer = new ServerSocket(0);
217             if (!this.debug) {
218                 mServer.setSoTimeout(mSocketTimeout);
219             }
220             artifactsDir = FileUtil.createTempDir("robolectric-screenshot-artifacts");
221             String classpath = this.compileClassPath();
222             List<String> cmdArgs = this.compileCommandArgs(classpath, artifactsDir);
223             CLog.v(String.join(" ", cmdArgs));
224             RunUtil runner = new RunUtil();
225 
226             String ldLibraryPath = this.compileLdLibraryPath();
227             if (ldLibraryPath != null) {
228                 runner.setEnvVariable("LD_LIBRARY_PATH", ldLibraryPath);
229             }
230 
231             // Note the below chooses a working directory based on the jar that happens to
232             // be first in the list of configured jars.  The baked-in assumption is that
233             // all configured jars are in the same parent directory, otherwise the behavior
234             // here is non-deterministic.
235             mWorkDir = findJarDirectory();
236             runner.setWorkingDir(mWorkDir);
237             CLog.v("Using PWD: %s", mWorkDir.getAbsolutePath());
238 
239             mSubprocessLog = FileUtil.createTempFile("subprocess-logs", "");
240             runner.setRedirectStderrToStdout(true);
241 
242             isolationRunner = runner.runCmdInBackground(Redirect.to(mSubprocessLog), cmdArgs);
243             CLog.v("Started subprocess.");
244 
245             if (this.debug) {
246                 CLog.v(
247                         "JVM subprocess is waiting for a debugger to connect, will now wait"
248                                 + " indefinitely for connection.");
249             }
250 
251             Socket socket = mServer.accept();
252             if (!this.debug) {
253                 socket.setSoTimeout(mSocketTimeout);
254             }
255             CLog.v("Connected to subprocess.");
256 
257             List<String> testJarAbsPaths = getJarPaths(mJars);
258 
259             TestParameters.Builder paramsBuilder =
260                     TestParameters.newBuilder()
261                             .addAllTestClasses(mClasses)
262                             .addAllTestJarAbsPaths(testJarAbsPaths)
263                             .addAllExcludePaths(mExcludePaths)
264                             .setDryRun(mCollectTestsOnly);
265 
266             if (!mIncludeFilters.isEmpty()
267                     || !mExcludeFilters.isEmpty()
268                     || !mIncludeAnnotations.isEmpty()
269                     || !mExcludeAnnotations.isEmpty()) {
270                 paramsBuilder.setFilter(
271                         FilterSpec.newBuilder()
272                                 .addAllIncludeFilters(mIncludeFilters)
273                                 .addAllExcludeFilters(mExcludeFilters)
274                                 .addAllIncludeAnnotations(mIncludeAnnotations)
275                                 .addAllExcludeAnnotations(mExcludeAnnotations));
276             }
277             executeTests(socket, listener, paramsBuilder.build());
278 
279             RunnerMessage.newBuilder()
280                     .setCommand(RunnerOp.RUNNER_OP_STOP)
281                     .build()
282                     .writeDelimitedTo(socket.getOutputStream());
283         } catch (IOException e) {
284             if (!mReportedFailure) {
285                 // Avoid overriding the failure
286                 FailureDescription failure =
287                         FailureDescription.create(
288                                 StreamUtil.getStackTrace(e), FailureStatus.INFRA_FAILURE);
289                 listener.testRunFailed(failure);
290                 listener.testRunEnded(0L, new HashMap<String, Metric>());
291             }
292         } finally {
293             try {
294                 // Ensure the subprocess finishes
295                 if (isolationRunner != null) {
296                     if (isolationRunner.isAlive()) {
297                         CLog.v(
298                                 "Subprocess is still alive after test phase - waiting for it to"
299                                         + " terminate.");
300                         isolationRunner.waitFor(10, TimeUnit.SECONDS);
301                         if (isolationRunner.isAlive()) {
302                             CLog.v(
303                                     "Subprocess is still alive after test phase - requesting"
304                                             + " termination.");
305                             // Isolation runner still alive for some reason, try to kill it
306                             isolationRunner.destroy();
307                             isolationRunner.waitFor(10, TimeUnit.SECONDS);
308 
309                             // If the process is still alive after trying to kill it nicely
310                             // then end it forcibly.
311                             if (isolationRunner.isAlive()) {
312                                 CLog.v(
313                                         "Subprocess is still alive after test phase - forcibly"
314                                                 + " terminating it.");
315                                 isolationRunner.destroyForcibly();
316                             }
317                         }
318                     }
319                 }
320             } catch (InterruptedException e) {
321                 throw new HarnessRuntimeException(
322                         "Interrupted while stopping subprocess",
323                         e,
324                         InfraErrorIdentifier.INTERRUPTED_DURING_SUBPROCESS_SHUTDOWN);
325             }
326 
327             if (isCoverageEnabled()) {
328                 logCoverageExecFile(listener);
329             }
330             FileUtil.deleteFile(mIsolationJar);
331             uploadTestArtifacts(artifactsDir, listener);
332         }
333     }
334 
335     /** Assembles the command arguments to execute the subprocess runner. */
compileCommandArgs(String classpath, File artifactsDir)336     public List<String> compileCommandArgs(String classpath, File artifactsDir) {
337         List<String> cmdArgs = new ArrayList<>();
338 
339         if (mJdkFolder == null) {
340             cmdArgs.add(SystemUtil.getRunningJavaBinaryPath().getAbsolutePath());
341             CLog.v("Using host java version.");
342         } else {
343             File javaExec = FileUtil.findFile(mJdkFolder, "java");
344             if (javaExec == null) {
345                 throw new IllegalArgumentException(
346                         String.format(
347                                 "Couldn't find java executable in given JDK folder: %s",
348                                 mJdkFolder.getAbsolutePath()));
349             }
350             String javaPath = javaExec.getAbsolutePath();
351             cmdArgs.add(javaPath);
352             CLog.v("Using java executable at %s", javaPath);
353         }
354         if (isCoverageEnabled()) {
355             if (mConfig.getCoverageOptions().getJaCoCoAgentPath() != null) {
356                 try {
357                     mCoverageExecFile = FileUtil.createTempFile("coverage", ".exec");
358                     String javaAgent =
359                             String.format(
360                                     "-javaagent:%s=destfile=%s,"
361                                             + "inclnolocationclasses=true,"
362                                             + "exclclassloader="
363                                             + "jdk.internal.reflect.DelegatingClassLoader",
364                                     mConfig.getCoverageOptions().getJaCoCoAgentPath(),
365                                     mCoverageExecFile.getAbsolutePath());
366                     cmdArgs.add(javaAgent);
367                 } catch (IOException e) {
368                     CLog.e(e);
369                 }
370             } else {
371                 CLog.e("jacocoagent path is not set.");
372             }
373         }
374 
375         cmdArgs.add("-cp");
376         cmdArgs.add(classpath);
377 
378         cmdArgs.addAll(mJavaFlags);
379 
380         if (mRobolectricResources) {
381             cmdArgs.addAll(compileRobolectricOptions(artifactsDir));
382             // Prevent tradefed from eagerly loading classes, which may not load without shadows
383             // applied.
384             if (mExcludeRobolectricPackages) {
385                 mExcludePaths.add("org/robolectric");
386             }
387         }
388         if (mRavenwoodResources) {
389             // For the moment, swap in the default JUnit upstream runner
390             cmdArgs.add("-Dandroid.junit.runner=org.junit.runners.JUnit4");
391         }
392 
393         if (this.debug) {
394             cmdArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8656");
395         }
396 
397         cmdArgs.addAll(
398                 List.of(
399                         "com.android.tradefed.isolation.IsolationRunner",
400                         "-",
401                         "--port",
402                         Integer.toString(mServer.getLocalPort()),
403                         "--address",
404                         mServer.getInetAddress().getHostAddress(),
405                         "--timeout",
406                         Integer.toString(mSocketTimeout)));
407         return cmdArgs;
408     }
409 
410     /**
411      * Finds the directory where the first configured jar is located.
412      *
413      * <p>This is used to determine the correct folder to use for a working directory for the
414      * subprocess runner.
415      */
findJarDirectory()416     private File findJarDirectory() {
417         File testDir = findTestDirectory();
418         for (String jar : mJars) {
419             File f = FileUtil.findFile(testDir, jar);
420             if (f != null && f.exists()) {
421                 return f.getParentFile();
422             }
423         }
424         return null;
425     }
426 
427     /**
428      * Retrieves the file registered in the build info as the test directory
429      *
430      * @return a {@link File} object representing the test directory
431      */
findTestDirectory()432     private File findTestDirectory() {
433         File testsDir = mBuildInfo.getFile(BuildInfoFileKey.HOST_LINKED_DIR);
434         if (testsDir != null && testsDir.exists()) {
435             return testsDir;
436         }
437         testsDir = mBuildInfo.getFile(BuildInfoFileKey.TESTDIR_IMAGE);
438         if (testsDir != null && testsDir.exists()) {
439             return testsDir;
440         }
441         throw new IllegalArgumentException("Test directory not found, cannot proceed");
442     }
443 
uploadTestArtifacts(File logDir, ITestInvocationListener listener)444     public void uploadTestArtifacts(File logDir, ITestInvocationListener listener) {
445         try {
446             for (File subFile : logDir.listFiles()) {
447                 if (subFile.isDirectory()) {
448                     uploadTestArtifacts(subFile, listener);
449                 } else {
450                     if (!subFile.exists()) {
451                         continue;
452                     }
453                     try (InputStreamSource dataStream = new FileInputStreamSource(subFile, true)) {
454                         String cleanName = subFile.getName().replace(",", "_");
455                         LogDataType type = LogDataType.TEXT;
456                         if (cleanName.endsWith(".png")) {
457                             type = LogDataType.PNG;
458                         } else if (cleanName.endsWith(".jpg") || cleanName.endsWith(".jpeg")) {
459                             type = LogDataType.JPEG;
460                         } else if (cleanName.endsWith(".pb")) {
461                             type = LogDataType.PB;
462                         }
463                         listener.testLog(cleanName, type, dataStream);
464                     }
465                 }
466             }
467         } finally {
468             FileUtil.recursiveDelete(logDir);
469         }
470     }
471 
getRavenwoodRuntimeDir(File testDir)472     private File getRavenwoodRuntimeDir(File testDir) {
473         File ravenwoodRuntime = FileUtil.findFile(testDir, "ravenwood-runtime");
474         if (ravenwoodRuntime == null || !ravenwoodRuntime.isDirectory()) {
475             throw new HarnessRuntimeException(
476                     "Could not find Ravenwood runtime needed for execution. " + testDir,
477                     InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
478         }
479         return ravenwoodRuntime;
480     }
481 
482     /**
483      * Creates a classpath for the subprocess that includes the needed jars to run the tests
484      *
485      * @return a string specifying the colon separated classpath.
486      */
compileClassPath()487     public String compileClassPath() {
488         // Use LinkedHashSet because we don't want duplicates, but we still
489         // want to preserve the insertion order. e.g. mIsolationJar should always be the
490         // first one.
491         Set<String> paths = new LinkedHashSet<>();
492         File testDir = findTestDirectory();
493 
494         try {
495             mIsolationJar = getIsolationJar(CurrentInvocation.getWorkFolder());
496             paths.add(mIsolationJar.getAbsolutePath());
497         } catch (IOException e) {
498             throw new RuntimeException(e);
499         }
500 
501         if (mClasspathOverride != null) {
502             paths.add(mClasspathOverride);
503         } else {
504             if (mRobolectricResources) {
505                 // This is contingent on the current android-all version.
506                 File androidAllJar = FileUtil.findFile(testDir, mAndroidAllName);
507                 if (androidAllJar == null) {
508                     throw new HarnessRuntimeException(
509                             "Could not find android-all jar needed for test execution.",
510                             InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
511                 }
512                 paths.add(androidAllJar.getAbsolutePath());
513             } else if (mRavenwoodResources) {
514                 addAllFilesUnder(paths, getRavenwoodRuntimeDir(testDir));
515             }
516 
517             for (String jar : mJars) {
518                 File f = FileUtil.findFile(testDir, jar);
519                 if (f != null && f.exists()) {
520                     paths.add(f.getAbsolutePath());
521                     addAllFilesUnder(paths, f.getParentFile());
522                 }
523             }
524         }
525 
526         String jarClasspath = String.join(java.io.File.pathSeparator, paths);
527 
528         return jarClasspath;
529     }
530 
531     /** Add all files under {@code File} sorted by filename to {@code paths}. */
addAllFilesUnder(Set<String> paths, File parentDirectory)532     private static void addAllFilesUnder(Set<String> paths, File parentDirectory) {
533         var files = parentDirectory.listFiles((f) -> f.isFile());
534         Arrays.sort(files, Comparator.comparing(File::getName));
535 
536         for (File file : files) {
537             paths.add(file.getAbsolutePath());
538         }
539     }
540 
541     @VisibleForTesting
getEnvironment(String key)542     String getEnvironment(String key) {
543         return System.getenv(key);
544     }
545 
546     /**
547      * Return LD_LIBRARY_PATH for tests that require native library.
548      *
549      * @return a string specifying the colon separated library path.
550      */
compileLdLibraryPath()551     private String compileLdLibraryPath() {
552         return compileLdLibraryPathInner(getEnvironment("ANDROID_HOST_OUT"));
553     }
554 
555     /**
556      * We call this version from the unit test, and directly pass ANDROID_HOST_OUT. We need it
557      * because Java has no API to set environmental variables.
558      */
559     @VisibleForTesting
compileLdLibraryPathInner(String androidHostOut)560     protected String compileLdLibraryPathInner(String androidHostOut) {
561         if (mClasspathOverride != null) {
562             return null;
563         }
564         // TODO(b/324134773) Unify with TestRunnerUtil.getLdLibraryPath().
565 
566         File testDir = findTestDirectory();
567         // Collect all the directories that may contain `lib` or `lib64` for the test.
568         Set<String> dirs = new LinkedHashSet<>();
569 
570         // Search the directories containing the test jars.
571         for (String jar : mJars) {
572             File f = FileUtil.findFile(testDir, jar);
573             if (f == null || !f.exists()) {
574                 continue;
575             }
576             // Include the directory containing the test jar.
577             File parent = f.getParentFile();
578             if (parent != null) {
579                 dirs.add(parent.getAbsolutePath());
580 
581                 // Also include the parent directory -- which is typically (?) "testcases" --
582                 // for running tests based on test zip.
583                 File grandParent = parent.getParentFile();
584                 if (grandParent != null) {
585                     dirs.add(grandParent.getAbsolutePath());
586                 }
587             }
588         }
589         // Optionally search the ravenwood runtime dir.
590         if (mRavenwoodResources) {
591             dirs.add(getRavenwoodRuntimeDir(testDir).getAbsolutePath());
592         }
593         // Search ANDROID_HOST_OUT.
594         if (androidHostOut != null) {
595             dirs.add(androidHostOut);
596         }
597 
598         // Look into all the above directories, and if there are any 'lib' or 'lib64', then
599         // add it to LD_LIBRARY_PATH.
600         String libs[] = {"lib", "lib64"};
601 
602         Set<String> result = new LinkedHashSet<>();
603 
604         for (String dir : dirs) {
605             File path = new File(dir);
606             if (!path.isDirectory()) {
607                 continue;
608             }
609 
610             for (String lib : libs) {
611                 File libFile = new File(path, lib);
612 
613                 if (libFile.isDirectory()) {
614                     result.add(libFile.getAbsolutePath());
615                 }
616             }
617         }
618         if (result.isEmpty()) {
619             return null;
620         }
621         return String.join(java.io.File.pathSeparator, result);
622     }
623 
compileRobolectricOptions(File artifactsDir)624     private List<String> compileRobolectricOptions(File artifactsDir) {
625         List<String> options = new ArrayList<>();
626         File testDir = findTestDirectory();
627         File androidAllDir = FileUtil.findFile(testDir, "android-all");
628         if (androidAllDir == null) {
629             throw new IllegalArgumentException("android-all directory not found, cannot proceed");
630         }
631         String dependencyDir =
632                 "-Drobolectric.dependency.dir=" + androidAllDir.getAbsolutePath() + "/";
633         options.add(dependencyDir);
634         if (artifactsDir != null) {
635             String artifactsDirFull =
636                     "-Drobolectric.artifacts.dir=" + artifactsDir.getAbsolutePath() + "/";
637             options.add(artifactsDirFull);
638         }
639         options.add("-Drobolectric.offline=true");
640         options.add("-Drobolectric.logging=stdout");
641         options.add("-Drobolectric.resourcesMode=binary");
642         options.add("-Drobolectric.usePreinstrumentedJars=false");
643         // TODO(rexhoffman) figure out how to get the local conscrypt working - shared objects and
644         // such.
645         options.add("-Drobolectric.conscryptMode=OFF");
646 
647         if (this.debug) {
648             options.add("-Drobolectric.logging.enabled=true");
649         }
650         return options;
651     }
652 
653     /**
654      * Runs the tests by talking to the subprocess assuming the setup is done.
655      *
656      * @param socket A socket connected to the subprocess control socket
657      * @param listener The TradeFed invocation listener from run()
658      * @param params The tests to run and their options
659      * @throws IOException
660      */
executeTests( Socket socket, ITestInvocationListener listener, TestParameters params)661     private void executeTests(
662             Socket socket, ITestInvocationListener listener, TestParameters params)
663             throws IOException {
664         // If needed apply the wrapping listeners like timeout enforcer.
665         listener = wrapListener(listener);
666         RunnerMessage.newBuilder()
667                 .setCommand(RunnerOp.RUNNER_OP_RUN_TEST)
668                 .setParams(params)
669                 .build()
670                 .writeDelimitedTo(socket.getOutputStream());
671 
672         TestDescription currentTest = null;
673         Instant start = Instant.now();
674         CloseableTraceScope methodScope = null;
675         CloseableTraceScope runScope = null;
676         boolean runStarted = false;
677         try {
678             mainLoop:
679             while (true) {
680                 try {
681                     RunnerReply reply = RunnerReply.parseDelimitedFrom(socket.getInputStream());
682                     if (reply == null) {
683                         if (currentTest != null) {
684                             // Subprocess has hard crashed
685                             listener.testFailed(currentTest, "Subprocess died unexpectedly.");
686                             listener.testEnded(
687                                     currentTest,
688                                     System.currentTimeMillis(),
689                                     new HashMap<String, Metric>());
690                         }
691                         // Try collecting the hs_err logs that the JVM dumps when it segfaults.
692                         List<File> logFiles =
693                                 Arrays.stream(mWorkDir.listFiles())
694                                         .filter(
695                                                 f ->
696                                                         f.getName().startsWith("hs_err")
697                                                                 && f.getName().endsWith(".log"))
698                                         .collect(Collectors.toList());
699 
700                         if (!runStarted) {
701                             listener.testRunStarted(this.getClass().getCanonicalName(), 0);
702                         }
703                         for (File f : logFiles) {
704                             try (FileInputStreamSource source =
705                                     new FileInputStreamSource(f, true)) {
706                                 listener.testLog("hs_err_log-VM-crash", LogDataType.TEXT, source);
707                             }
708                         }
709                         mReportedFailure = true;
710                         FailureDescription failure =
711                                 FailureDescription.create(
712                                                 "The subprocess died unexpectedly.",
713                                                 FailureStatus.TEST_FAILURE)
714                                         .setFullRerun(false);
715                         listener.testRunFailed(failure);
716                         listener.testRunEnded(0L, new HashMap<String, Metric>());
717                         break mainLoop;
718                     }
719                     switch (reply.getRunnerStatus()) {
720                         case RUNNER_STATUS_FINISHED_OK:
721                             CLog.v("Received message that runner finished successfully");
722                             break mainLoop;
723                         case RUNNER_STATUS_FINISHED_ERROR:
724                             CLog.e("Received message that runner errored");
725                             CLog.e("From Runner: " + reply.getMessage());
726                             if (!runStarted) {
727                                 listener.testRunStarted(this.getClass().getCanonicalName(), 0);
728                             }
729                             FailureDescription failure =
730                                     FailureDescription.create(
731                                             reply.getMessage(), FailureStatus.INFRA_FAILURE);
732                             listener.testRunFailed(failure);
733                             listener.testRunEnded(0L, new HashMap<String, Metric>());
734                             break mainLoop;
735                         case RUNNER_STATUS_STARTING:
736                             CLog.v("Received message that runner is starting");
737                             break;
738                         default:
739                             if (reply.hasTestEvent()) {
740                                 JUnitEvent event = reply.getTestEvent();
741                                 TestDescription desc;
742                                 switch (event.getTopic()) {
743                                     case TOPIC_FAILURE:
744                                         desc =
745                                                 new TestDescription(
746                                                         event.getClassName(),
747                                                         event.getMethodName());
748                                         listener.testFailed(desc, event.getMessage());
749                                         break;
750                                     case TOPIC_ASSUMPTION_FAILURE:
751                                         desc =
752                                                 new TestDescription(
753                                                         event.getClassName(),
754                                                         event.getMethodName());
755                                         listener.testAssumptionFailure(desc, reply.getMessage());
756                                         break;
757                                     case TOPIC_STARTED:
758                                         desc =
759                                                 new TestDescription(
760                                                         event.getClassName(),
761                                                         event.getMethodName());
762                                         listener.testStarted(desc, event.getStartTime());
763                                         currentTest = desc;
764                                         methodScope = new CloseableTraceScope(desc.toString());
765                                         break;
766                                     case TOPIC_FINISHED:
767                                         desc =
768                                                 new TestDescription(
769                                                         event.getClassName(),
770                                                         event.getMethodName());
771                                         listener.testEnded(
772                                                 desc,
773                                                 event.getEndTime(),
774                                                 new HashMap<String, Metric>());
775                                         currentTest = null;
776                                         if (methodScope != null) {
777                                             methodScope.close();
778                                             methodScope = null;
779                                         }
780                                         break;
781                                     case TOPIC_IGNORED:
782                                         desc =
783                                                 new TestDescription(
784                                                         event.getClassName(),
785                                                         event.getMethodName());
786                                         // Use endTime for both events since
787                                         // ignored test do not really run.
788                                         listener.testStarted(desc, event.getEndTime());
789                                         listener.testIgnored(desc);
790                                         listener.testEnded(
791                                                 desc,
792                                                 event.getEndTime(),
793                                                 new HashMap<String, Metric>());
794                                         break;
795                                     case TOPIC_RUN_STARTED:
796                                         runStarted = true;
797                                         listener.testRunStarted(
798                                                 event.getClassName(), event.getTestCount());
799                                         runScope = new CloseableTraceScope(event.getClassName());
800                                         break;
801                                     case TOPIC_RUN_FINISHED:
802                                         listener.testRunEnded(
803                                                 event.getElapsedTime(),
804                                                 new HashMap<String, Metric>());
805                                         if (runScope != null) {
806                                             runScope.close();
807                                             runScope = null;
808                                         }
809                                         break;
810                                     default:
811                                 }
812                             }
813                     }
814                 } catch (SocketTimeoutException e) {
815                     mReportedFailure = true;
816                     FailureDescription failure =
817                             FailureDescription.create(
818                                     StreamUtil.getStackTrace(e), FailureStatus.INFRA_FAILURE);
819                     listener.testRunFailed(failure);
820                     listener.testRunEnded(
821                             Duration.between(start, Instant.now()).toMillis(),
822                             new HashMap<String, Metric>());
823                     break mainLoop;
824                 }
825             }
826         } finally {
827             // This will get associated with the module since it can contains several test runs
828             try (FileInputStreamSource source = new FileInputStreamSource(mSubprocessLog, true)) {
829                 listener.testLog("isolated-java-logs", LogDataType.TEXT, source);
830             }
831         }
832     }
833 
834     /**
835      * Utility method to searh for absolute paths for JAR files. Largely the same as in the HostTest
836      * implementation, but somewhat difficult to extract well due to the various method calls it
837      * uses.
838      */
getJarPaths(Set<String> jars)839     private List<String> getJarPaths(Set<String> jars) throws FileNotFoundException {
840         Set<String> output = new HashSet<>();
841 
842         for (String jar : jars) {
843             File jarFile = getJarFile(jar, mBuildInfo);
844             output.add(jarFile.getAbsolutePath());
845         }
846 
847         return output.stream().collect(Collectors.toList());
848     }
849 
850     /**
851      * Inspect several location where the artifact are usually located for different use cases to
852      * find our jar.
853      */
getJarFile(String jarName, IBuildInfo buildInfo)854     private File getJarFile(String jarName, IBuildInfo buildInfo) throws FileNotFoundException {
855         // Check tests dir
856         File testDir = buildInfo.getFile(BuildInfoFileKey.TESTDIR_IMAGE);
857         File jarFile = searchJarFile(testDir, jarName);
858         if (jarFile != null) {
859             return jarFile;
860         }
861 
862         // Check ROOT_DIR
863         if (buildInfo.getBuildAttributes().get(ROOT_DIR) != null) {
864             jarFile =
865                     searchJarFile(new File(buildInfo.getBuildAttributes().get(ROOT_DIR)), jarName);
866         }
867         if (jarFile != null) {
868             return jarFile;
869         }
870         throw new FileNotFoundException(String.format("Could not find jar: %s", jarName));
871     }
872 
873     /**
874      * Copied over from HostTest to mimic its unit test harnessing.
875      *
876      * <p>Inspect several location where the artifact are usually located for different use cases to
877      * find our jar.
878      */
879     @VisibleForTesting
getJarFile(String jarName, TestInformation testInfo)880     protected File getJarFile(String jarName, TestInformation testInfo)
881             throws FileNotFoundException {
882         return testInfo.getDependencyFile(jarName, /* target first*/ false);
883     }
884 
885     /** Looks for a jar file given a place to start and a filename. */
searchJarFile(File baseSearchFile, String jarName)886     private File searchJarFile(File baseSearchFile, String jarName) {
887         if (baseSearchFile != null && baseSearchFile.isDirectory()) {
888             File jarFile = FileUtil.findFile(baseSearchFile, jarName);
889             if (jarFile != null && jarFile.isFile()) {
890                 return jarFile;
891             }
892         }
893         return null;
894     }
895 
logCoverageExecFile(ITestInvocationListener listener)896     private void logCoverageExecFile(ITestInvocationListener listener) {
897         if (mCoverageExecFile == null) {
898             CLog.e("Coverage execution file is null.");
899             return;
900         }
901         if (mCoverageExecFile.length() == 0) {
902             CLog.e("Coverage execution file has 0 length.");
903             return;
904         }
905         try (FileInputStreamSource source = new FileInputStreamSource(mCoverageExecFile, true)) {
906             listener.testLog("coverage", LogDataType.COVERAGE, source);
907         }
908     }
909 
isCoverageEnabled()910     private boolean isCoverageEnabled() {
911         return mConfig != null && mConfig.getCoverageOptions().isCoverageEnabled();
912     }
913 
914     /** {@inheritDoc} */
915     @Override
setBuild(IBuildInfo build)916     public void setBuild(IBuildInfo build) {
917         mBuildInfo = build;
918     }
919 
920     /** {@inheritDoc} */
921     @Override
addIncludeFilter(String filter)922     public void addIncludeFilter(String filter) {
923         mIncludeFilters.add(filter);
924     }
925 
926     /** {@inheritDoc} */
927     @Override
addAllIncludeFilters(Set<String> filters)928     public void addAllIncludeFilters(Set<String> filters) {
929         mIncludeFilters.addAll(filters);
930     }
931 
932     /** {@inheritDoc} */
933     @Override
addExcludeFilter(String filter)934     public void addExcludeFilter(String filter) {
935         mExcludeFilters.add(filter);
936     }
937 
938     /** {@inheritDoc} */
939     @Override
addAllExcludeFilters(Set<String> filters)940     public void addAllExcludeFilters(Set<String> filters) {
941         mExcludeFilters.addAll(filters);
942     }
943 
944     /** {@inheritDoc} */
945     @Override
getIncludeFilters()946     public Set<String> getIncludeFilters() {
947         return mIncludeFilters;
948     }
949 
950     /** {@inheritDoc} */
951     @Override
getExcludeFilters()952     public Set<String> getExcludeFilters() {
953         return mExcludeFilters;
954     }
955 
956     /** {@inheritDoc} */
957     @Override
clearIncludeFilters()958     public void clearIncludeFilters() {
959         mIncludeFilters.clear();
960     }
961 
962     /** {@inheritDoc} */
963     @Override
clearExcludeFilters()964     public void clearExcludeFilters() {
965         mExcludeFilters.clear();
966     }
967 
968     /** {@inheritDoc} */
969     @Override
setCollectTestsOnly(boolean shouldCollectTest)970     public void setCollectTestsOnly(boolean shouldCollectTest) {
971         mCollectTestsOnly = shouldCollectTest;
972     }
973 
974     /** {@inheritDoc} */
975     @Override
addIncludeAnnotation(String annotation)976     public void addIncludeAnnotation(String annotation) {
977         mIncludeAnnotations.add(annotation);
978     }
979 
980     /** {@inheritDoc} */
981     @Override
addExcludeAnnotation(String notAnnotation)982     public void addExcludeAnnotation(String notAnnotation) {
983         mExcludeAnnotations.add(notAnnotation);
984     }
985 
986     /** {@inheritDoc} */
987     @Override
addAllIncludeAnnotation(Set<String> annotations)988     public void addAllIncludeAnnotation(Set<String> annotations) {
989         mIncludeAnnotations.addAll(annotations);
990     }
991 
992     /** {@inheritDoc} */
993     @Override
addAllExcludeAnnotation(Set<String> notAnnotations)994     public void addAllExcludeAnnotation(Set<String> notAnnotations) {
995         mExcludeAnnotations.addAll(notAnnotations);
996     }
997 
998     /** {@inheritDoc} */
999     @Override
getIncludeAnnotations()1000     public Set<String> getIncludeAnnotations() {
1001         return mIncludeAnnotations;
1002     }
1003 
1004     /** {@inheritDoc} */
1005     @Override
getExcludeAnnotations()1006     public Set<String> getExcludeAnnotations() {
1007         return mExcludeAnnotations;
1008     }
1009 
1010     /** {@inheritDoc} */
1011     @Override
clearIncludeAnnotations()1012     public void clearIncludeAnnotations() {
1013         mIncludeAnnotations.clear();
1014     }
1015 
1016     /** {@inheritDoc} */
1017     @Override
clearExcludeAnnotations()1018     public void clearExcludeAnnotations() {
1019         mExcludeAnnotations.clear();
1020     }
1021 
1022     @Override
setConfiguration(IConfiguration configuration)1023     public void setConfiguration(IConfiguration configuration) {
1024         mConfig = configuration;
1025     }
1026 
getCoverageExecFile()1027     public File getCoverageExecFile() {
1028         return mCoverageExecFile;
1029     }
1030 
1031     @VisibleForTesting
setServer(ServerSocket server)1032     protected void setServer(ServerSocket server) {
1033         mServer = server;
1034     }
1035 
useRobolectricResources()1036     public boolean useRobolectricResources() {
1037         return mRobolectricResources;
1038     }
1039 
useRavenwoodResources()1040     public boolean useRavenwoodResources() {
1041         return mRavenwoodResources;
1042     }
1043 
wrapListener(ITestInvocationListener listener)1044     private ITestInvocationListener wrapListener(ITestInvocationListener listener) {
1045         if (mTestCaseTimeout.toMillis() > 0L) {
1046             listener =
1047                     new TestTimeoutEnforcer(
1048                             mTestCaseTimeout.toMillis(), TimeUnit.MILLISECONDS, listener);
1049         }
1050         return listener;
1051     }
1052 
getIsolationJar(File workDir)1053     private File getIsolationJar(File workDir) throws IOException {
1054         File isolationJar = FileUtil.createTempFile("tradefed-isolation", ".jar", workDir);
1055         boolean res =
1056                 ResourceUtil.extractResourceWithAltAsFile(
1057                         "/tradefed-isolation.jar",
1058                         QUALIFIED_PATH + "/tradefed-isolation_deploy.jar",
1059                         isolationJar);
1060         if (!res) {
1061             FileUtil.deleteFile(isolationJar);
1062             throw new RuntimeException("/tradefed-isolation.jar not found.");
1063         }
1064         return isolationJar;
1065     }
1066 
deleteTempFiles()1067     public void deleteTempFiles() {
1068         if (mIsolationJar != null) {
1069             FileUtil.deleteFile(mIsolationJar);
1070         }
1071     }
1072 }
1073