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