| // Copyright 2014 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.devtools.build.lib.exec; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.io.ByteStreams; | |
| import com.google.devtools.build.lib.actions.ActionExecutionContext; | |
| import com.google.devtools.build.lib.actions.Artifact; | |
| import com.google.devtools.build.lib.actions.CommandLineExpansionException; | |
| import com.google.devtools.build.lib.actions.ExecException; | |
| import com.google.devtools.build.lib.actions.TestExecException; | |
| import com.google.devtools.build.lib.actions.UserExecException; | |
| import com.google.devtools.build.lib.analysis.config.BuildConfiguration; | |
| import com.google.devtools.build.lib.analysis.config.PerLabelOptions; | |
| import com.google.devtools.build.lib.analysis.test.TestActionContext; | |
| import com.google.devtools.build.lib.analysis.test.TestConfiguration; | |
| import com.google.devtools.build.lib.analysis.test.TestResult; | |
| import com.google.devtools.build.lib.analysis.test.TestRunnerAction; | |
| import com.google.devtools.build.lib.analysis.test.TestRunnerAction.ResolvedPaths; | |
| import com.google.devtools.build.lib.analysis.test.TestTargetExecutionSettings; | |
| import com.google.devtools.build.lib.cmdline.Label; | |
| import com.google.devtools.build.lib.events.Event; | |
| import com.google.devtools.build.lib.events.EventKind; | |
| import com.google.devtools.build.lib.profiler.Profiler; | |
| import com.google.devtools.build.lib.profiler.ProfilerTask; | |
| import com.google.devtools.build.lib.util.Fingerprint; | |
| import com.google.devtools.build.lib.util.OS; | |
| import com.google.devtools.build.lib.util.io.FileWatcher; | |
| import com.google.devtools.build.lib.util.io.OutErr; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus; | |
| import com.google.devtools.build.lib.view.test.TestStatus.TestCase; | |
| import com.google.devtools.build.lib.view.test.TestStatus.TestResultData; | |
| import com.google.devtools.common.options.EnumConverter; | |
| import java.io.Closeable; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.time.Duration; | |
| import java.util.Arrays; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import javax.annotation.Nullable; | |
| /** A strategy for executing a {@link TestRunnerAction}. */ | |
| public abstract class TestStrategy implements TestActionContext { | |
| /** | |
| * Ensures that all directories used to run test are in the correct state and their content will | |
| * not result in stale files. | |
| */ | |
| protected void prepareFileSystem( | |
| TestRunnerAction testAction, Path execRoot, Path tmpDir, Path workingDirectory) | |
| throws IOException { | |
| if (tmpDir != null) { | |
| recreateDirectory(tmpDir); | |
| } | |
| if (workingDirectory != null) { | |
| workingDirectory.createDirectoryAndParents(); | |
| } | |
| ResolvedPaths resolvedPaths = testAction.resolve(execRoot); | |
| if (testAction.isCoverageMode()) { | |
| recreateDirectory(resolvedPaths.getCoverageDirectory()); | |
| } | |
| resolvedPaths.getBaseDir().createDirectoryAndParents(); | |
| resolvedPaths.getUndeclaredOutputsDir().createDirectoryAndParents(); | |
| resolvedPaths.getUndeclaredOutputsAnnotationsDir().createDirectoryAndParents(); | |
| resolvedPaths.getSplitLogsDir().createDirectoryAndParents(); | |
| } | |
| /** | |
| * Ensures that all directories used to run test are in the correct state and their content will | |
| * not result in stale files. Only use this if no local tmp and working directory are required. | |
| */ | |
| protected void prepareFileSystem(TestRunnerAction testAction, Path execRoot) throws IOException { | |
| prepareFileSystem(testAction, execRoot, null, null); | |
| } | |
| /** Removes directory if it exists and recreates it. */ | |
| private void recreateDirectory(Path directory) throws IOException { | |
| directory.deleteTree(); | |
| directory.createDirectoryAndParents(); | |
| } | |
| /** An enum for specifying different formats of test output. */ | |
| public enum TestOutputFormat { | |
| SUMMARY, // Provide summary output only. | |
| ERRORS, // Print output from failed tests to the stderr after the test failure. | |
| ALL, // Print output from all tests to the stderr after the test completion. | |
| STREAMED; // Stream output for each test. | |
| /** Converts to {@link TestOutputFormat}. */ | |
| public static class Converter extends EnumConverter<TestOutputFormat> { | |
| public Converter() { | |
| super(TestOutputFormat.class, "test output"); | |
| } | |
| } | |
| } | |
| /** An enum for specifying different formatting styles of test summaries. */ | |
| public enum TestSummaryFormat { | |
| SHORT, // Print information only about tests. | |
| TERSE, // Like "SHORT", but even shorter: Do not print PASSED and NO STATUS tests. | |
| DETAILED, // Print information only about failed test cases. | |
| NONE, // Do not print summary. | |
| TESTCASE; // Print summary in test case resolution, do not print detailed information about | |
| // failed test cases. | |
| /** Converts to {@link TestSummaryFormat}. */ | |
| public static class Converter extends EnumConverter<TestSummaryFormat> { | |
| public Converter() { | |
| super(TestSummaryFormat.class, "test summary"); | |
| } | |
| } | |
| } | |
| public static final PathFragment TEST_TMP_ROOT = PathFragment.create("_tmp"); | |
| // Used for generating unique temporary directory names. Contains the next numeric index for every | |
| // executable base name. | |
| private final Map<String, Integer> tmpIndex = new HashMap<>(); | |
| protected final ExecutionOptions executionOptions; | |
| protected final BinTools binTools; | |
| public TestStrategy(ExecutionOptions executionOptions, BinTools binTools) { | |
| this.executionOptions = executionOptions; | |
| this.binTools = binTools; | |
| } | |
| @Override | |
| public final boolean isTestKeepGoing() { | |
| return executionOptions.testKeepGoing; | |
| } | |
| /** | |
| * Generates a command line to run for the test action, taking into account coverage and {@code | |
| * --run_under} settings. | |
| * | |
| * @param testAction The test action. | |
| * @return the command line as string list. | |
| * @throws ExecException | |
| */ | |
| public static ImmutableList<String> getArgs(TestRunnerAction testAction) throws ExecException { | |
| List<String> args = Lists.newArrayList(); | |
| // TODO(ulfjack): `executedOnWindows` is incorrect for remote execution, where we need to | |
| // consider the target configuration, not the machine Bazel happens to run on. Change this to | |
| // something like: testAction.getConfiguration().getTargetOS() == OS.WINDOWS | |
| final boolean executedOnWindows = (OS.getCurrent() == OS.WINDOWS); | |
| final boolean useTestWrapper = testAction.isUsingTestWrapperInsteadOfTestSetupScript(); | |
| if (executedOnWindows && !useTestWrapper) { | |
| // TestActionBuilder constructs TestRunnerAction with a 'null' shell path only when we use the | |
| // native test wrapper. Something clearly went wrong. | |
| Preconditions.checkNotNull(testAction.getShExecutableMaybe(), "%s", testAction); | |
| args.add(testAction.getShExecutableMaybe().getPathString()); | |
| args.add("-c"); | |
| args.add("$0 \"$@\""); | |
| } | |
| Artifact testSetup = testAction.getTestSetupScript(); | |
| args.add(testSetup.getExecPath().getCallablePathString()); | |
| if (testAction.isCoverageMode()) { | |
| args.add(testAction.getCollectCoverageScript().getExecPathString()); | |
| } | |
| TestTargetExecutionSettings execSettings = testAction.getExecutionSettings(); | |
| // Insert the command prefix specified by the "--run_under=<command-prefix>" option, if any. | |
| if (execSettings.getRunUnder() != null) { | |
| addRunUnderArgs(testAction, args, executedOnWindows); | |
| } | |
| // Execute the test using the alias in the runfiles tree, as mandated by the Test Encyclopedia. | |
| args.add(execSettings.getExecutable().getRootRelativePath().getCallablePathString()); | |
| try { | |
| Iterables.addAll(args, execSettings.getArgs().arguments()); | |
| } catch (CommandLineExpansionException e) { | |
| throw new UserExecException(e); | |
| } | |
| return ImmutableList.copyOf(args); | |
| } | |
| private static void addRunUnderArgs( | |
| TestRunnerAction testAction, List<String> args, boolean executedOnWindows) { | |
| TestTargetExecutionSettings execSettings = testAction.getExecutionSettings(); | |
| if (execSettings.getRunUnderExecutable() != null) { | |
| args.add(execSettings.getRunUnderExecutable().getRootRelativePath().getCallablePathString()); | |
| } else { | |
| if (execSettings.needsShell(executedOnWindows)) { | |
| // TestActionBuilder constructs TestRunnerAction with a 'null' shell only when none is | |
| // required. Something clearly went wrong. | |
| Preconditions.checkNotNull(testAction.getShExecutableMaybe(), "%s", testAction); | |
| String shellExecutable = testAction.getShExecutableMaybe().getPathString(); | |
| args.add(shellExecutable); | |
| args.add("-c"); | |
| args.add("\"$@\""); | |
| args.add(shellExecutable); // Sets $0. | |
| } | |
| args.add(execSettings.getRunUnder().getCommand()); | |
| } | |
| args.addAll(testAction.getExecutionSettings().getRunUnder().getOptions()); | |
| } | |
| /** | |
| * Returns the number of attempts specific test action can be retried. | |
| * | |
| * <p>For rules with "flaky = 1" attribute, this method will return 3 unless --flaky_test_attempts | |
| * option is given and specifies another value. | |
| */ | |
| @VisibleForTesting /* protected */ | |
| public int getTestAttempts(TestRunnerAction action) { | |
| return action.getTestProperties().isFlaky() | |
| ? getTestAttemptsForFlakyTest(action) | |
| : getTestAttempts(action, /*defaultTestAttempts=*/ 1); | |
| } | |
| public int getTestAttemptsForFlakyTest(TestRunnerAction action) { | |
| return getTestAttempts(action, /*defaultTestAttempts=*/ 3); | |
| } | |
| private int getTestAttempts(TestRunnerAction action, int defaultTestAttempts) { | |
| Label testLabel = action.getOwner().getLabel(); | |
| return getTestAttemptsPerLabel(executionOptions, testLabel, defaultTestAttempts); | |
| } | |
| private static int getTestAttemptsPerLabel( | |
| ExecutionOptions options, Label label, int defaultTestAttempts) { | |
| // Check from the last provided, so that the last option provided takes precedence. | |
| for (PerLabelOptions perLabelAttempts : Lists.reverse(options.testAttempts)) { | |
| if (perLabelAttempts.isIncluded(label)) { | |
| String attempts = Iterables.getOnlyElement(perLabelAttempts.getOptions()); | |
| if ("default".equals(attempts)) { | |
| return defaultTestAttempts; | |
| } | |
| return Integer.parseInt(attempts); | |
| } | |
| } | |
| return defaultTestAttempts; | |
| } | |
| /** | |
| * Returns timeout value in seconds that should be used for the given test action. We always use | |
| * the "categorical timeouts" which are based on the --test_timeout flag. A rule picks its timeout | |
| * but ends up with the same effective value as all other rules in that bucket. | |
| */ | |
| protected final Duration getTimeout(TestRunnerAction testAction) { | |
| BuildConfiguration configuration = testAction.getConfiguration(); | |
| return configuration | |
| .getFragment(TestConfiguration.class) | |
| .getTestTimeout() | |
| .get(testAction.getTestProperties().getTimeout()); | |
| } | |
| /* | |
| * Finalize test run: persist the result, and post on the event bus. | |
| */ | |
| protected void postTestResult(ActionExecutionContext actionExecutionContext, TestResult result) | |
| throws IOException { | |
| result.getTestAction().saveCacheStatus(actionExecutionContext, result.getData()); | |
| actionExecutionContext.getEventHandler().post(result); | |
| } | |
| /** | |
| * Returns a unique name for a temporary directory a test could use. | |
| * | |
| * <p>Since each test within single Blaze run must have a unique TEST_TMPDIR, we will use rule | |
| * name and a unique (within single Blaze request) number to generate directory name. | |
| * | |
| * <p>This does not create the directory. | |
| */ | |
| protected String getTmpDirName(PathFragment execPath) { | |
| String basename = execPath.getBaseName(); | |
| synchronized (tmpIndex) { | |
| int index = tmpIndex.containsKey(basename) ? tmpIndex.get(basename) : 1; | |
| tmpIndex.put(basename, index + 1); | |
| return basename + "_" + index; | |
| } | |
| } | |
| public static String getTmpDirName(TestRunnerAction action) { | |
| Fingerprint digest = new Fingerprint(); | |
| digest.addPath(action.getExecutionSettings().getExecutable().getExecPath()); | |
| digest.addInt(action.getShardNum()); | |
| digest.addInt(action.getRunNumber()); | |
| return digest.hexDigestAndReset(); | |
| } | |
| /** Parse a test result XML file into a {@link TestCase}. */ | |
| @Nullable | |
| protected TestCase parseTestResult(Path resultFile) { | |
| /* xml files. We avoid parsing it unnecessarily, since test results can potentially consume | |
| a large amount of memory. */ | |
| if ((executionOptions.testSummary != TestSummaryFormat.DETAILED) | |
| && (executionOptions.testSummary != TestSummaryFormat.TESTCASE)) { | |
| return null; | |
| } | |
| try (InputStream fileStream = resultFile.getInputStream()) { | |
| return new TestXmlOutputParser().parseXmlIntoTestResult(fileStream); | |
| } catch (IOException | TestXmlOutputParserException e) { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Outputs test result to the stdout after test has finished (e.g. for --test_output=all or | |
| * --test_output=errors). Will also try to group output lines together (up to 10000 lines) so | |
| * parallel test outputs will not get interleaved. | |
| */ | |
| protected void processTestOutput( | |
| ActionExecutionContext actionExecutionContext, | |
| TestResultData testResultData, | |
| String testName, | |
| Path testLog) | |
| throws IOException { | |
| boolean isPassed = testResultData.getTestPassed(); | |
| try { | |
| if (TestLogHelper.shouldOutputTestLog(executionOptions.testOutput, isPassed)) { | |
| TestLogHelper.writeTestLog( | |
| testLog, testName, actionExecutionContext.getFileOutErr().getOutputStream()); | |
| } | |
| } finally { | |
| if (isPassed) { | |
| actionExecutionContext.getEventHandler().handle(Event.of(EventKind.PASS, null, testName)); | |
| } else { | |
| if (testResultData.hasStatusDetails()) { | |
| actionExecutionContext | |
| .getEventHandler() | |
| .handle(Event.error(testName + ": " + testResultData.getStatusDetails())); | |
| } | |
| if (testResultData.getStatus() == BlazeTestStatus.TIMEOUT) { | |
| actionExecutionContext | |
| .getEventHandler() | |
| .handle(Event.of(EventKind.TIMEOUT, null, testName + " (see " + testLog + ")")); | |
| } else { | |
| actionExecutionContext | |
| .getEventHandler() | |
| .handle(Event.of(EventKind.FAIL, null, testName + " (see " + testLog + ")")); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Returns a temporary directory for all tests in a workspace to use. Individual tests should | |
| * create child directories to actually use. | |
| * | |
| * <p>This either dynamically generates a directory name or uses the directory specified by | |
| * --test_tmpdir. This does not create the directory. | |
| */ | |
| public static Path getTmpRoot(Path workspace, Path execRoot, ExecutionOptions executionOptions) { | |
| return executionOptions.testTmpDir != null | |
| ? workspace.getRelative(executionOptions.testTmpDir).getRelative(TEST_TMP_ROOT) | |
| : execRoot.getRelative(TEST_TMP_ROOT); | |
| } | |
| /** | |
| * For an given environment, returns a subset containing all variables in the given list if they | |
| * are defined in the given environment. | |
| */ | |
| @VisibleForTesting | |
| public static Map<String, String> getMapping( | |
| Iterable<String> variables, Map<String, String> environment) { | |
| Map<String, String> result = new HashMap<>(); | |
| for (String var : variables) { | |
| if (environment.containsKey(var)) { | |
| result.put(var, environment.get(var)); | |
| } | |
| } | |
| return result; | |
| } | |
| /** | |
| * Returns the runfiles directory associated with the test executable, creating/updating it if | |
| * necessary and --build_runfile_links is specified. | |
| */ | |
| protected static Path getLocalRunfilesDirectory( | |
| TestRunnerAction testAction, | |
| ActionExecutionContext actionExecutionContext, | |
| BinTools binTools, | |
| ImmutableMap<String, String> shellEnvironment, | |
| boolean enableRunfiles) | |
| throws ExecException, InterruptedException { | |
| TestTargetExecutionSettings execSettings = testAction.getExecutionSettings(); | |
| if (execSettings.getInputManifest() == null) { | |
| throw new TestExecException("cannot run local tests with --nobuild_runfile_manifests"); | |
| } | |
| Path runfilesDir = | |
| actionExecutionContext.getPathResolver().convertPath(execSettings.getRunfilesDir()); | |
| // If the symlink farm is already created then return the existing directory. If not we | |
| // need to explicitly build it. This can happen when --nobuild_runfile_links is supplied | |
| // as a flag to the build. | |
| if (execSettings.getRunfilesSymlinksCreated()) { | |
| return runfilesDir; | |
| } | |
| // Synchronize runfiles tree generation on the runfiles manifest artifact. | |
| // This is necessary, because we might end up with multiple test runner actions | |
| // trying to generate same runfiles tree in case of --runs_per_test > 1 or | |
| // local test sharding. | |
| long startTime = Profiler.nanoTimeMaybe(); | |
| synchronized (execSettings.getInputManifest()) { | |
| Profiler.instance().logSimpleTask(startTime, ProfilerTask.WAIT, testAction.describe()); | |
| updateLocalRunfilesDirectory( | |
| testAction, | |
| runfilesDir, | |
| actionExecutionContext, | |
| binTools, | |
| shellEnvironment, | |
| enableRunfiles); | |
| } | |
| return runfilesDir; | |
| } | |
| /** | |
| * Ensure the runfiles tree exists and is consistent with the TestAction's manifest | |
| * ($0.runfiles_manifest), bringing it into consistency if not. The contents of the output file | |
| * $0.runfiles/MANIFEST, if it exists, are used a proxy for the set of existing symlinks, to avoid | |
| * the need for recursion. | |
| */ | |
| private static void updateLocalRunfilesDirectory( | |
| TestRunnerAction testAction, | |
| Path runfilesDir, | |
| ActionExecutionContext actionExecutionContext, | |
| BinTools binTools, | |
| ImmutableMap<String, String> shellEnvironment, | |
| boolean enableRunfiles) | |
| throws ExecException, InterruptedException { | |
| TestTargetExecutionSettings execSettings = testAction.getExecutionSettings(); | |
| Path outputManifest = runfilesDir.getRelative("MANIFEST"); | |
| try { | |
| // Avoid rebuilding the runfiles directory if the manifest in it matches the input manifest, | |
| // implying the symlinks exist and are already up to date. If the output manifest is a | |
| // symbolic link, it is likely a symbolic link to the input manifest, so we cannot trust it as | |
| // an up-to-date check. | |
| if (!outputManifest.isSymbolicLink() | |
| && Arrays.equals( | |
| outputManifest.getDigest(), | |
| actionExecutionContext.getInputPath(execSettings.getInputManifest()).getDigest())) { | |
| return; | |
| } | |
| } catch (IOException e1) { | |
| // Ignore it - we will just try to create runfiles directory. | |
| } | |
| actionExecutionContext | |
| .getEventHandler() | |
| .handle( | |
| Event.progress( | |
| "Building runfiles directory for '" | |
| + execSettings.getExecutable().prettyPrint() | |
| + "'.")); | |
| new SymlinkTreeHelper( | |
| actionExecutionContext.getInputPath(execSettings.getInputManifest()), | |
| runfilesDir, | |
| false) | |
| .createSymlinks(actionExecutionContext, binTools, shellEnvironment, enableRunfiles); | |
| actionExecutionContext.getEventHandler() | |
| .handle(Event.progress(testAction.getProgressMessage())); | |
| } | |
| protected static void closeSuppressed(Throwable e, @Nullable Closeable c) { | |
| if (c == null) { | |
| return; | |
| } | |
| try { | |
| c.close(); | |
| } catch (IOException e2) { | |
| e.addSuppressed(e2); | |
| } | |
| } | |
| /** Implements the --test_output=streamed option. */ | |
| protected static class StreamedTestOutput implements Closeable { | |
| private final TestLogHelper.FilterTestHeaderOutputStream headerFilter; | |
| private final FileWatcher watcher; | |
| private final Path testLogPath; | |
| private final OutErr outErr; | |
| public StreamedTestOutput(OutErr outErr, Path testLogPath) throws IOException { | |
| this.testLogPath = testLogPath; | |
| this.outErr = outErr; | |
| this.headerFilter = TestLogHelper.getHeaderFilteringOutputStream(outErr.getOutputStream()); | |
| this.watcher = new FileWatcher(testLogPath, OutErr.create(headerFilter, headerFilter), false); | |
| watcher.start(); | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| watcher.stopPumping(); | |
| try { | |
| // The watcher thread might leak if the following call is interrupted. | |
| // This is a relatively minor issue since the worst it could do is | |
| // write one additional line from the test.log to the console later on | |
| // in the build. | |
| watcher.join(); | |
| } catch (InterruptedException e) { | |
| Thread.currentThread().interrupt(); | |
| } | |
| if (!headerFilter.foundHeader()) { | |
| try (InputStream input = testLogPath.getInputStream()) { | |
| ByteStreams.copy(input, outErr.getOutputStream()); | |
| } | |
| } | |
| } | |
| } | |
| } |