1 /* 2 * Copyright (C) 2022 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.bazel; 17 18 import com.android.annotations.VisibleForTesting; 19 import com.android.tradefed.config.Option; 20 import com.android.tradefed.config.OptionClass; 21 import com.android.tradefed.device.DeviceNotAvailableException; 22 import com.android.tradefed.invoker.TestInformation; 23 import com.android.tradefed.invoker.tracing.CloseableTraceScope; 24 import com.android.tradefed.invoker.tracing.TracePropagatingExecutorService; 25 import com.android.tradefed.log.ITestLogger; 26 import com.android.tradefed.log.LogUtil.CLog; 27 import com.android.tradefed.result.FailureDescription; 28 import com.android.tradefed.result.FileInputStreamSource; 29 import com.android.tradefed.result.ITestInvocationListener; 30 import com.android.tradefed.result.LogDataType; 31 import com.android.tradefed.result.error.ErrorIdentifier; 32 import com.android.tradefed.result.error.TestErrorIdentifier; 33 import com.android.tradefed.result.proto.LogFileProto.LogFileInfo; 34 import com.android.tradefed.result.proto.ProtoResultParser; 35 import com.android.tradefed.result.proto.TestRecordProto.ChildReference; 36 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus; 37 import com.android.tradefed.result.proto.TestRecordProto.TestRecord; 38 import com.android.tradefed.testtype.IRemoteTest; 39 import com.android.tradefed.util.ZipUtil; 40 import com.android.tradefed.util.proto.TestRecordProtoUtil; 41 42 import com.google.common.collect.HashMultimap; 43 import com.google.common.collect.ImmutableMap; 44 import com.google.common.collect.Maps; 45 import com.google.common.collect.SetMultimap; 46 import com.google.common.io.CharStreams; 47 import com.google.common.io.MoreFiles; 48 import com.google.common.io.Resources; 49 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos; 50 import com.google.protobuf.Any; 51 import com.google.protobuf.InvalidProtocolBufferException; 52 53 import java.io.File; 54 import java.io.IOException; 55 import java.io.FileOutputStream; 56 import java.lang.ProcessBuilder.Redirect; 57 import java.net.URI; 58 import java.net.URISyntaxException; 59 import java.nio.file.Files; 60 import java.nio.file.Path; 61 import java.nio.file.Paths; 62 import java.time.Duration; 63 import java.util.ArrayList; 64 import java.util.Collection; 65 import java.util.Collections; 66 import java.util.HashMap; 67 import java.util.List; 68 import java.util.Map.Entry; 69 import java.util.Map; 70 import java.util.Properties; 71 import java.util.Set; 72 import java.util.concurrent.ExecutorService; 73 import java.util.concurrent.Executors; 74 import java.util.concurrent.TimeUnit; 75 import java.util.stream.Collectors; 76 import java.util.zip.ZipFile; 77 78 /** Test runner for executing Bazel tests. */ 79 @OptionClass(alias = "bazel-test") 80 public final class BazelTest implements IRemoteTest { 81 82 public static final String QUERY_ALL_TARGETS = "query_all_targets"; 83 public static final String QUERY_MAP_MODULES_TO_TARGETS = "query_map_modules_to_targets"; 84 public static final String RUN_TESTS = "run_tests"; 85 86 // Add method excludes to TF's global filters since Bazel doesn't support target-specific 87 // arguments. See https://github.com/bazelbuild/rules_go/issues/2784. 88 // TODO(b/274787592): Integrate with Bazel's test filtering to filter specific test cases. 89 public static final String GLOBAL_EXCLUDE_FILTER_TEMPLATE = 90 "--test_arg=--global-filters:exclude-filter=%s"; 91 92 private static final Duration BAZEL_QUERY_TIMEOUT = Duration.ofMinutes(5); 93 private static final String TEST_NAME = BazelTest.class.getName(); 94 // Bazel internally calls the test output archive file "test.outputs__outputs.zip", the double 95 // underscore is part of this name. 96 private static final String TEST_UNDECLARED_OUTPUTS_ARCHIVE_NAME = "test.outputs__outputs.zip"; 97 private static final String PROTO_RESULTS_FILE_NAME = "proto-results"; 98 99 private final List<Path> mTemporaryPaths = new ArrayList<>(); 100 private final List<Path> mLogFiles = new ArrayList<>(); 101 private final Properties mProperties; 102 private final ProcessStarter mProcessStarter; 103 private final Path mTemporaryDirectory; 104 private final ExecutorService mExecutor; 105 106 private Path mRunTemporaryDirectory; 107 108 private enum FilterType { 109 MODULE, 110 TEST_CASE 111 }; 112 113 @Option( 114 name = "bazel-test-command-timeout", 115 description = "Timeout for running the Bazel test.") 116 private Duration mBazelCommandTimeout = Duration.ofHours(1L); 117 118 @Option( 119 name = "bazel-test-suite-root-dir", 120 description = 121 "Name of the environment variable set by CtsTestLauncher indicating the" 122 + " location of the root bazel-test-suite dir.") 123 private String mSuiteRootDirEnvVar = "BAZEL_SUITE_ROOT"; 124 125 @Option( 126 name = "bazel-startup-options", 127 description = "List of startup options to be passed to Bazel.") 128 private final List<String> mBazelStartupOptions = new ArrayList<>(); 129 130 @Option( 131 name = "bazel-test-extra-args", 132 description = "List of extra arguments to be passed to Bazel") 133 private final List<String> mBazelTestExtraArgs = new ArrayList<>(); 134 135 @Option( 136 name = "bazel-max-idle-timout", 137 description = "Max idle timeout in seconds for bazel commands.") 138 private Duration mBazelMaxIdleTimeout = Duration.ofSeconds(5L); 139 140 @Option(name = "exclude-filter", description = "Test modules to exclude when running tests.") 141 private final List<String> mExcludeTargets = new ArrayList<>(); 142 143 @Option(name = "include-filter", description = "Test modules to include when running tests.") 144 private final List<String> mIncludeTargets = new ArrayList<>(); 145 146 @Option( 147 name = "report-cached-test-results", 148 description = "Whether or not to report cached test results.") 149 private boolean mReportCachedTestResults = true; 150 BazelTest()151 public BazelTest() { 152 this(new DefaultProcessStarter(), System.getProperties()); 153 } 154 155 @VisibleForTesting BazelTest(ProcessStarter processStarter, Properties properties)156 BazelTest(ProcessStarter processStarter, Properties properties) { 157 mProcessStarter = processStarter; 158 mExecutor = TracePropagatingExecutorService.create(Executors.newCachedThreadPool()); 159 mProperties = properties; 160 mTemporaryDirectory = Paths.get(properties.getProperty("java.io.tmpdir")); 161 } 162 163 @Override run(TestInformation testInfo, ITestInvocationListener listener)164 public void run(TestInformation testInfo, ITestInvocationListener listener) 165 throws DeviceNotAvailableException { 166 167 List<FailureDescription> runFailures = new ArrayList<>(); 168 long startTime = System.currentTimeMillis(); 169 170 try { 171 initialize(); 172 runTestsAndParseResults(testInfo, listener, runFailures); 173 } catch (AbortRunException e) { 174 runFailures.add(e.getFailureDescription()); 175 } catch (IOException | InterruptedException e) { 176 runFailures.add(throwableToTestFailureDescription(e)); 177 } 178 179 listener.testModuleStarted(testInfo.getContext()); 180 listener.testRunStarted(TEST_NAME, 0); 181 reportRunFailures(runFailures, listener); 182 listener.testRunEnded(System.currentTimeMillis() - startTime, Collections.emptyMap()); 183 listener.testModuleEnded(); 184 185 addTestLogs(listener); 186 cleanup(); 187 } 188 initialize()189 private void initialize() throws IOException { 190 mRunTemporaryDirectory = Files.createTempDirectory(mTemporaryDirectory, "bazel-test-"); 191 } 192 runTestsAndParseResults( TestInformation testInfo, ITestInvocationListener listener, List<FailureDescription> runFailures)193 private void runTestsAndParseResults( 194 TestInformation testInfo, 195 ITestInvocationListener listener, 196 List<FailureDescription> runFailures) 197 throws IOException, InterruptedException { 198 199 Path workspaceDirectory = resolveWorkspacePath(); 200 201 Collection<String> testTargets = listTestTargets(workspaceDirectory); 202 if (testTargets.isEmpty()) { 203 throw new AbortRunException( 204 "No targets found, aborting", 205 FailureStatus.DEPENDENCY_ISSUE, 206 TestErrorIdentifier.TEST_ABORTED); 207 } 208 209 Path bepFile = createTemporaryFile("BEP_output"); 210 211 Process bazelTestProcess = 212 startTests(testInfo, listener, testTargets, workspaceDirectory, bepFile); 213 214 try (BepFileTailer tailer = BepFileTailer.create(bepFile)) { 215 bazelTestProcess.onExit().thenRun(() -> tailer.stop()); 216 reportTestResults(listener, testInfo, runFailures, tailer); 217 } 218 219 // Note that if Bazel exits without writing the 'last' BEP message marker we won't get to 220 // here since the above reporting code throws. 221 waitForProcess(bazelTestProcess, RUN_TESTS); 222 } 223 reportTestResults( ITestInvocationListener listener, TestInformation testInfo, List<FailureDescription> runFailures, BepFileTailer tailer)224 void reportTestResults( 225 ITestInvocationListener listener, 226 TestInformation testInfo, 227 List<FailureDescription> runFailures, 228 BepFileTailer tailer) 229 throws InterruptedException, IOException { 230 231 try (CloseableTraceScope ignored = new CloseableTraceScope("reportTestResults")) { 232 reportTestResultsNoTrace(listener, testInfo, runFailures, tailer); 233 } 234 } 235 reportTestResultsNoTrace( ITestInvocationListener listener, TestInformation testInfo, List<FailureDescription> runFailures, BepFileTailer tailer)236 void reportTestResultsNoTrace( 237 ITestInvocationListener listener, 238 TestInformation testInfo, 239 List<FailureDescription> runFailures, 240 BepFileTailer tailer) 241 throws InterruptedException, IOException { 242 243 ProtoResultParser resultParser = 244 new ProtoResultParser(listener, testInfo.getContext(), false, "tf-test-process-"); 245 246 BuildEventStreamProtos.BuildEvent event; 247 while ((event = tailer.nextEvent()) != null) { 248 if (event.getLastMessage()) { 249 return; 250 } 251 252 if (!event.hasTestResult()) { 253 continue; 254 } 255 256 if (!mReportCachedTestResults && isTestResultCached(event.getTestResult())) { 257 continue; 258 } 259 260 try { 261 reportEventsInTestOutputsArchive(event.getTestResult(), resultParser); 262 } catch (IOException | InterruptedException | URISyntaxException e) { 263 runFailures.add( 264 throwableToInfraFailureDescription(e) 265 .setErrorIdentifier(TestErrorIdentifier.OUTPUT_PARSER_ERROR)); 266 } 267 } 268 269 throw new AbortRunException( 270 "Unexpectedly hit end of BEP file without receiving last message", 271 FailureStatus.INFRA_FAILURE, 272 TestErrorIdentifier.OUTPUT_PARSER_ERROR); 273 } 274 isTestResultCached(BuildEventStreamProtos.TestResult result)275 private static boolean isTestResultCached(BuildEventStreamProtos.TestResult result) { 276 return result.getCachedLocally() || result.getExecutionInfo().getCachedRemotely(); 277 } 278 createBazelCommand(Path workspaceDirectory, String tmpDirPrefix)279 private ProcessBuilder createBazelCommand(Path workspaceDirectory, String tmpDirPrefix) 280 throws IOException { 281 282 Path javaTmpDir = createTemporaryDirectory("%s-java-tmp-out".formatted(tmpDirPrefix)); 283 Path bazelTmpDir = createTemporaryDirectory("%s-bazel-tmp-out".formatted(tmpDirPrefix)); 284 285 List<String> command = new ArrayList<>(); 286 287 command.add(workspaceDirectory.resolve("bazel.sh").toAbsolutePath().toString()); 288 command.add( 289 "--host_jvm_args=-Djava.io.tmpdir=%s" 290 .formatted(javaTmpDir.toAbsolutePath().toString())); 291 command.add("--output_user_root=%s".formatted(bazelTmpDir.toAbsolutePath().toString())); 292 command.add("--max_idle_secs=%d".formatted(mBazelMaxIdleTimeout.toSeconds())); 293 294 ProcessBuilder builder = new ProcessBuilder(command); 295 296 builder.directory(workspaceDirectory.toFile()); 297 298 return builder; 299 } 300 listTestTargets(Path workspaceDirectory)301 private Collection<String> listTestTargets(Path workspaceDirectory) 302 throws IOException, InterruptedException { 303 304 try (CloseableTraceScope ignored = new CloseableTraceScope("listTestTargets")) { 305 return listTestTargetsNoTrace(workspaceDirectory); 306 } 307 } 308 listTestTargetsNoTrace(Path workspaceDirectory)309 private Collection<String> listTestTargetsNoTrace(Path workspaceDirectory) 310 throws IOException, InterruptedException { 311 312 // We need to query all tests targets first in a separate Bazel query call since 'cquery 313 // tests(...)' doesn't work in the Atest Bazel workspace. 314 List<String> allTestTargets = queryAllTestTargets(workspaceDirectory); 315 CLog.i("Found %d test targets in workspace", allTestTargets.size()); 316 317 Map<String, String> moduleToTarget = 318 queryModulesToTestTargets(workspaceDirectory, allTestTargets); 319 320 Set<String> moduleExcludes = groupTargetsByType(mExcludeTargets).get(FilterType.MODULE); 321 Set<String> moduleIncludes = groupTargetsByType(mIncludeTargets).get(FilterType.MODULE); 322 323 if (!moduleIncludes.isEmpty() && !moduleExcludes.isEmpty()) { 324 throw new AbortRunException( 325 "Invalid options: cannot set both module-level include filters and module-level" 326 + " exclude filters.", 327 FailureStatus.DEPENDENCY_ISSUE, 328 TestErrorIdentifier.TEST_ABORTED); 329 } 330 331 if (!moduleIncludes.isEmpty()) { 332 return Maps.filterKeys(moduleToTarget, s -> moduleIncludes.contains(s)).values(); 333 } 334 335 if (!moduleExcludes.isEmpty()) { 336 return Maps.filterKeys(moduleToTarget, s -> !moduleExcludes.contains(s)).values(); 337 } 338 339 return moduleToTarget.values(); 340 } 341 queryAllTestTargets(Path workspaceDirectory)342 private List<String> queryAllTestTargets(Path workspaceDirectory) 343 throws IOException, InterruptedException { 344 345 Path logFile = createLogFile("%s-log".formatted(QUERY_ALL_TARGETS)); 346 347 ProcessBuilder builder = createBazelCommand(workspaceDirectory, QUERY_ALL_TARGETS); 348 349 builder.command().add("query"); 350 builder.command().add("tests(...)"); 351 builder.redirectError(Redirect.appendTo(logFile.toFile())); 352 353 Process queryProcess = startProcess(QUERY_ALL_TARGETS, builder, BAZEL_QUERY_TIMEOUT); 354 List<String> queryLines = readProcessLines(queryProcess); 355 356 waitForProcess(queryProcess, QUERY_ALL_TARGETS); 357 358 return queryLines; 359 } 360 queryModulesToTestTargets( Path workspaceDirectory, List<String> allTestTargets)361 private Map<String, String> queryModulesToTestTargets( 362 Path workspaceDirectory, List<String> allTestTargets) 363 throws IOException, InterruptedException { 364 365 Path cqueryTestTargetsFile = createTemporaryFile("test_targets"); 366 Files.write(cqueryTestTargetsFile, String.join("+", allTestTargets).getBytes()); 367 368 Path cqueryFormatFile = createTemporaryFile("format_module_name_to_test_target"); 369 try (FileOutputStream os = new FileOutputStream(cqueryFormatFile.toFile())) { 370 Resources.copy( 371 Resources.getResource("config/format_module_name_to_test_target.cquery"), os); 372 } 373 374 Path logFile = createLogFile("%s-log".formatted(QUERY_MAP_MODULES_TO_TARGETS)); 375 ProcessBuilder builder = 376 createBazelCommand(workspaceDirectory, QUERY_MAP_MODULES_TO_TARGETS); 377 378 builder.command().add("cquery"); 379 builder.command().add("--query_file=%s".formatted(cqueryTestTargetsFile.toAbsolutePath())); 380 builder.command().add("--output=starlark"); 381 builder.command().add("--starlark:file=%s".formatted(cqueryFormatFile.toAbsolutePath())); 382 builder.redirectError(Redirect.appendTo(logFile.toFile())); 383 384 Process process = startProcess(QUERY_MAP_MODULES_TO_TARGETS, builder, BAZEL_QUERY_TIMEOUT); 385 386 List<String> queryLines = readProcessLines(process); 387 388 waitForProcess(process, QUERY_MAP_MODULES_TO_TARGETS); 389 390 return parseModulesToTargets(queryLines); 391 } 392 readProcessLines(Process process)393 private List<String> readProcessLines(Process process) throws IOException { 394 return CharStreams.readLines(process.inputReader()); 395 } 396 parseModulesToTargets(Collection<String> lines)397 private Map<String, String> parseModulesToTargets(Collection<String> lines) { 398 Map<String, String> moduleToTarget = new HashMap<>(); 399 StringBuilder errorMessage = new StringBuilder(); 400 for (String line : lines) { 401 // Query output format is: "module_name //bazel/test:target" if a test target is a 402 // TF test, "" otherwise, so only count proper targets. 403 if (line.isEmpty()) { 404 continue; 405 } 406 407 String[] splitLine = line.split(" "); 408 409 if (splitLine.length != 2) { 410 throw new AbortRunException( 411 String.format( 412 "Unrecognized output from %s command: %s", 413 QUERY_MAP_MODULES_TO_TARGETS, line), 414 FailureStatus.DEPENDENCY_ISSUE, 415 TestErrorIdentifier.TEST_ABORTED); 416 } 417 418 String moduleName = splitLine[0]; 419 String targetName = splitLine[1]; 420 421 String duplicateEntry; 422 if ((duplicateEntry = moduleToTarget.get(moduleName)) != null) { 423 errorMessage.append( 424 "Multiple test targets found for module %s: %s, %s\n" 425 .formatted(moduleName, duplicateEntry, targetName)); 426 } 427 428 moduleToTarget.put(moduleName, targetName); 429 } 430 431 if (errorMessage.length() != 0) { 432 throw new AbortRunException( 433 errorMessage.toString(), 434 FailureStatus.DEPENDENCY_ISSUE, 435 TestErrorIdentifier.TEST_ABORTED); 436 } 437 return ImmutableMap.copyOf(moduleToTarget); 438 } 439 startTests( TestInformation testInfo, ITestInvocationListener listener, Collection<String> testTargets, Path workspaceDirectory, Path bepFile)440 private Process startTests( 441 TestInformation testInfo, 442 ITestInvocationListener listener, 443 Collection<String> testTargets, 444 Path workspaceDirectory, 445 Path bepFile) 446 throws IOException { 447 448 Path logFile = createLogFile("%s-log".formatted(RUN_TESTS)); 449 450 ProcessBuilder builder = createBazelCommand(workspaceDirectory, RUN_TESTS); 451 452 builder.command().addAll(mBazelStartupOptions); 453 builder.command().add("test"); 454 builder.command().addAll(testTargets); 455 456 builder.command().add("--build_event_binary_file=%s".formatted(bepFile.toAbsolutePath())); 457 458 builder.command().addAll(mBazelTestExtraArgs); 459 460 Set<String> testFilters = groupTargetsByType(mExcludeTargets).get(FilterType.TEST_CASE); 461 for (String test : testFilters) { 462 builder.command().add(GLOBAL_EXCLUDE_FILTER_TEMPLATE.formatted(test)); 463 } 464 builder.redirectErrorStream(true); 465 builder.redirectOutput(Redirect.appendTo(logFile.toFile())); 466 467 return startProcess(RUN_TESTS, builder, mBazelCommandTimeout); 468 } 469 groupTargetsByType(List<String> targets)470 private static SetMultimap<FilterType, String> groupTargetsByType(List<String> targets) { 471 Map<FilterType, List<String>> groupedMap = 472 targets.stream() 473 .collect( 474 Collectors.groupingBy( 475 s -> 476 s.contains(" ") 477 ? FilterType.TEST_CASE 478 : FilterType.MODULE)); 479 480 SetMultimap<FilterType, String> groupedMultiMap = HashMultimap.create(); 481 for (Entry<FilterType, List<String>> entry : groupedMap.entrySet()) { 482 groupedMultiMap.putAll(entry.getKey(), entry.getValue()); 483 } 484 485 return groupedMultiMap; 486 } 487 startAndWaitForProcess( String processTag, ProcessBuilder builder, Duration processTimeout)488 private Process startAndWaitForProcess( 489 String processTag, ProcessBuilder builder, Duration processTimeout) 490 throws InterruptedException, IOException { 491 492 Process process = startProcess(processTag, builder, processTimeout); 493 waitForProcess(process, processTag); 494 return process; 495 } 496 startProcess(String processTag, ProcessBuilder builder, Duration timeout)497 private Process startProcess(String processTag, ProcessBuilder builder, Duration timeout) 498 throws IOException { 499 500 CLog.i("Running command for %s: %s", processTag, new ProcessDebugString(builder)); 501 String traceTag = "Process:" + processTag; 502 Process process = mProcessStarter.start(processTag, builder); 503 504 // We wait for the process in a separate thread so that we can trace its execution time. 505 // Another alternative could be to start/stop tracing with explicit calls but these would 506 // have to be done on the same thread as required by the tracing facility. 507 mExecutor.submit( 508 () -> { 509 try (CloseableTraceScope unused = new CloseableTraceScope(traceTag)) { 510 if (waitForProcessUninterruptibly(process, timeout)) { 511 return; 512 } 513 514 CLog.e("%s command timed out and is being destroyed", processTag); 515 process.destroy(); 516 517 // Give the process a grace period to properly shut down before forcibly 518 // terminating it. We _could_ deduct this time from the total timeout but 519 // it's overkill. 520 if (!waitForProcessUninterruptibly(process, Duration.ofSeconds(5))) { 521 CLog.w( 522 "%s command did not terminate normally after the grace period" 523 + " and is being forcibly destroyed", 524 processTag); 525 process.destroyForcibly(); 526 } 527 528 // We wait for the process as it may take it some time to terminate and 529 // otherwise skew the trace results. 530 waitForProcessUninterruptibly(process); 531 CLog.i("%s command timed out and was destroyed", processTag); 532 } 533 }); 534 535 return process; 536 } 537 waitForProcess(Process process, String processTag)538 private void waitForProcess(Process process, String processTag) throws InterruptedException { 539 540 if (process.waitFor() == 0) { 541 return; 542 } 543 544 throw new AbortRunException( 545 String.format("%s command failed. Exit code: %d", processTag, process.exitValue()), 546 FailureStatus.DEPENDENCY_ISSUE, 547 TestErrorIdentifier.TEST_ABORTED); 548 } 549 reportEventsInTestOutputsArchive( BuildEventStreamProtos.TestResult result, ProtoResultParser resultParser)550 private void reportEventsInTestOutputsArchive( 551 BuildEventStreamProtos.TestResult result, ProtoResultParser resultParser) 552 throws IOException, InvalidProtocolBufferException, InterruptedException, 553 URISyntaxException { 554 555 try (CloseableTraceScope ignored = 556 new CloseableTraceScope("reportEventsInTestOutputsArchive")) { 557 reportEventsInTestOutputsArchiveNoTrace(result, resultParser); 558 } 559 } 560 reportEventsInTestOutputsArchiveNoTrace( BuildEventStreamProtos.TestResult result, ProtoResultParser resultParser)561 private void reportEventsInTestOutputsArchiveNoTrace( 562 BuildEventStreamProtos.TestResult result, ProtoResultParser resultParser) 563 throws IOException, InvalidProtocolBufferException, InterruptedException, 564 URISyntaxException { 565 566 BuildEventStreamProtos.File outputsFile = 567 result.getTestActionOutputList().stream() 568 .filter(file -> file.getName().equals(TEST_UNDECLARED_OUTPUTS_ARCHIVE_NAME)) 569 .findAny() 570 .orElseThrow(() -> new IOException("No test output archive found")); 571 572 URI uri = new URI(outputsFile.getUri()); 573 574 File zipFile = new File(uri.getPath()); 575 Path outputFilesDir = Files.createTempDirectory(mRunTemporaryDirectory, "output_zip-"); 576 577 try { 578 ZipUtil.extractZip(new ZipFile(zipFile), outputFilesDir.toFile()); 579 580 File protoResult = outputFilesDir.resolve(PROTO_RESULTS_FILE_NAME).toFile(); 581 TestRecord record = TestRecordProtoUtil.readFromFile(protoResult); 582 583 TestRecord.Builder recordBuilder = record.toBuilder(); 584 //recursivelyUpdateArtifactsRootPath(recordBuilder, outputFilesDir); 585 //moveRootRecordArtifactsToFirstChild(recordBuilder); 586 resultParser.processFinalizedProto(recordBuilder.build()); 587 } finally { 588 MoreFiles.deleteRecursively(outputFilesDir); 589 } 590 } 591 592 /*private void recursivelyUpdateArtifactsRootPath(TestRecord.Builder recordBuilder, Path newRoot) 593 throws InvalidProtocolBufferException { 594 595 Map<String, Any> updatedMap = new HashMap<>(); 596 for (Entry<String, Any> entry : recordBuilder.getArtifactsMap().entrySet()) { 597 LogFileInfo info = entry.getValue().unpack(LogFileInfo.class); 598 599 Path relativePath = findRelativeArtifactPath(Paths.get(info.getPath())); 600 601 LogFileInfo updatedInfo = 602 info.toBuilder() 603 .setPath(newRoot.resolve(relativePath).toAbsolutePath().toString()) 604 .build(); 605 updatedMap.put(entry.getKey(), Any.pack(updatedInfo)); 606 } 607 608 recordBuilder.putAllArtifacts(updatedMap); 609 610 for (ChildReference.Builder childBuilder : recordBuilder.getChildrenBuilderList()) { 611 recursivelyUpdateArtifactsRootPath(childBuilder.getInlineTestRecordBuilder(), newRoot); 612 } 613 }*/ 614 findRelativeArtifactPath(Path originalPath)615 private Path findRelativeArtifactPath(Path originalPath) { 616 // The log files are stored under 617 // ${EXTRACTED_UNDECLARED_OUTPUTS}/stub/-1/stub/inv_xxx/inv_xxx/logfile so the new path is 618 // found by trimming down the original path until it starts with "stub/-1/stub" and 619 // appending that to our extracted directory. 620 // TODO(b/251279690) Create a directory within undeclared outputs which we can more 621 // reliably look for to calculate this relative path. 622 Path delimiter = Paths.get("stub/-1/stub"); 623 624 Path relativePath = originalPath; 625 while (!relativePath.startsWith(delimiter) 626 && relativePath.getNameCount() > delimiter.getNameCount()) { 627 relativePath = relativePath.subpath(1, relativePath.getNameCount()); 628 } 629 630 if (!relativePath.startsWith(delimiter)) { 631 throw new IllegalArgumentException( 632 String.format( 633 "Artifact path '%s' does not contain delimiter '%s' and therefore" 634 + " cannot be found", 635 originalPath, delimiter)); 636 } 637 638 return relativePath; 639 } 640 641 /*private void moveRootRecordArtifactsToFirstChild(TestRecord.Builder recordBuilder) { 642 if (recordBuilder.getChildrenCount() == 0) { 643 return; 644 } 645 646 TestRecord.Builder childTestRecordBuilder = 647 recordBuilder.getChildrenBuilder(0).getInlineTestRecordBuilder(); 648 for (Entry<String, Any> entry : recordBuilder.getArtifactsMap().entrySet()) { 649 childTestRecordBuilder.putArtifacts(entry.getKey(), entry.getValue()); 650 } 651 652 recordBuilder.clearArtifacts(); 653 }*/ 654 reportRunFailures( List<FailureDescription> runFailures, ITestInvocationListener listener)655 private void reportRunFailures( 656 List<FailureDescription> runFailures, ITestInvocationListener listener) { 657 658 if (runFailures.isEmpty()) { 659 return; 660 } 661 662 for (FailureDescription runFailure : runFailures) { 663 CLog.e(runFailure.getErrorMessage()); 664 } 665 666 FailureDescription reportedFailure = runFailures.get(0); 667 listener.testRunFailed( 668 FailureDescription.create( 669 String.format( 670 "The run had %d failures, the first of which was: %s\n" 671 + "See the subprocess-host_log for more details.", 672 runFailures.size(), reportedFailure.getErrorMessage()), 673 reportedFailure.getFailureStatus()) 674 .setErrorIdentifier(reportedFailure.getErrorIdentifier())); 675 } 676 resolveWorkspacePath()677 private Path resolveWorkspacePath() { 678 String suiteRootPath = mProperties.getProperty(mSuiteRootDirEnvVar); 679 if (suiteRootPath == null || suiteRootPath.isEmpty()) { 680 throw new AbortRunException( 681 "Bazel Test Suite root directory not set, aborting", 682 FailureStatus.DEPENDENCY_ISSUE, 683 TestErrorIdentifier.TEST_ABORTED); 684 } 685 686 // TODO(b/233885171): Remove resolve once workspace archive is updated. 687 return Paths.get(suiteRootPath).resolve("android-bazel-suite/out/atest_bazel_workspace"); 688 } 689 addTestLogs(ITestLogger logger)690 private void addTestLogs(ITestLogger logger) { 691 for (Path logFile : mLogFiles) { 692 try (FileInputStreamSource source = new FileInputStreamSource(logFile.toFile(), true)) { 693 logger.testLog(logFile.toFile().getName(), LogDataType.TEXT, source); 694 } 695 } 696 } 697 cleanup()698 private void cleanup() { 699 try { 700 MoreFiles.deleteRecursively(mRunTemporaryDirectory); 701 } catch (IOException e) { 702 CLog.e(e); 703 } 704 } 705 706 interface ProcessStarter { start(String processTag, ProcessBuilder builder)707 Process start(String processTag, ProcessBuilder builder) throws IOException; 708 } 709 710 private static final class DefaultProcessStarter implements ProcessStarter { 711 @Override start(String processTag, ProcessBuilder builder)712 public Process start(String processTag, ProcessBuilder builder) throws IOException { 713 return builder.start(); 714 } 715 } 716 createTemporaryDirectory(String prefix)717 private Path createTemporaryDirectory(String prefix) throws IOException { 718 return Files.createTempDirectory(mRunTemporaryDirectory, prefix); 719 } 720 createTemporaryFile(String prefix)721 private Path createTemporaryFile(String prefix) throws IOException { 722 return Files.createTempFile(mRunTemporaryDirectory, prefix, ""); 723 } 724 createLogFile(String name)725 private Path createLogFile(String name) throws IOException { 726 Path logFile = Files.createTempFile(mRunTemporaryDirectory, name, ".txt"); 727 728 mLogFiles.add(logFile); 729 730 return logFile; 731 } 732 throwableToTestFailureDescription(Throwable t)733 private static FailureDescription throwableToTestFailureDescription(Throwable t) { 734 return FailureDescription.create(t.getMessage()) 735 .setCause(t) 736 .setFailureStatus(FailureStatus.TEST_FAILURE); 737 } 738 throwableToInfraFailureDescription(Exception e)739 private static FailureDescription throwableToInfraFailureDescription(Exception e) { 740 return FailureDescription.create(e.getMessage()) 741 .setCause(e) 742 .setFailureStatus(FailureStatus.INFRA_FAILURE); 743 } 744 waitForProcessUninterruptibly(Process process, Duration timeout)745 private static boolean waitForProcessUninterruptibly(Process process, Duration timeout) { 746 long remainingNanos = timeout.toNanos(); 747 long end = System.nanoTime() + remainingNanos; 748 boolean interrupted = false; 749 750 try { 751 while (true) { 752 try { 753 return process.waitFor(remainingNanos, TimeUnit.NANOSECONDS); 754 } catch (InterruptedException e) { 755 interrupted = true; 756 remainingNanos = end - System.nanoTime(); 757 } 758 } 759 } finally { 760 if (interrupted) { 761 Thread.currentThread().interrupt(); 762 } 763 } 764 } 765 waitForProcessUninterruptibly(Process process)766 private static int waitForProcessUninterruptibly(Process process) { 767 boolean interrupted = false; 768 769 try { 770 while (true) { 771 try { 772 return process.waitFor(); 773 } catch (InterruptedException e) { 774 interrupted = true; 775 } 776 } 777 } finally { 778 if (interrupted) { 779 Thread.currentThread().interrupt(); 780 } 781 } 782 } 783 784 private static final class AbortRunException extends RuntimeException { 785 private final FailureDescription mFailureDescription; 786 AbortRunException( String errorMessage, FailureStatus failureStatus, ErrorIdentifier errorIdentifier)787 public AbortRunException( 788 String errorMessage, FailureStatus failureStatus, ErrorIdentifier errorIdentifier) { 789 this( 790 FailureDescription.create(errorMessage, failureStatus) 791 .setErrorIdentifier(errorIdentifier)); 792 } 793 AbortRunException(FailureDescription failureDescription)794 public AbortRunException(FailureDescription failureDescription) { 795 super(failureDescription.getErrorMessage()); 796 mFailureDescription = failureDescription; 797 } 798 getFailureDescription()799 public FailureDescription getFailureDescription() { 800 return mFailureDescription; 801 } 802 } 803 804 private static final class ProcessDebugString { 805 806 private final ProcessBuilder mBuilder; 807 ProcessDebugString(ProcessBuilder builder)808 ProcessDebugString(ProcessBuilder builder) { 809 mBuilder = builder; 810 } 811 toString()812 public String toString() { 813 return String.join(" ", mBuilder.command()); 814 } 815 } 816 } 817