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