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