• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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