aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/rules/test/TestStrategy.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/rules/test/TestStrategy.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/test/TestStrategy.java388
1 files changed, 388 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestStrategy.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestStrategy.java
new file mode 100644
index 0000000000..4905e15293
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestStrategy.java
@@ -0,0 +1,388 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Closeables;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.exec.SymlinkTreeHelper;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.runtime.BlazeServerStartupOptions;
+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.FileStatus;
+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.TestCase;
+import com.google.devtools.common.options.Converters.RangeConverter;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.OptionsClassProvider;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * A strategy for executing a {@link TestRunnerAction}.
+ */
+public abstract class TestStrategy implements TestActionContext {
+ /**
+ * Converter for the --flaky_test_attempts option.
+ */
+ public static class TestAttemptsConverter extends RangeConverter {
+ public TestAttemptsConverter() {
+ super(1, 10);
+ }
+
+ @Override
+ public Integer convert(String input) throws OptionsParsingException {
+ if ("default".equals(input)) {
+ return -1;
+ } else {
+ return super.convert(input);
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return super.getTypeDescription() + " or the string \"default\"";
+ }
+ }
+
+ 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");
+ }
+ }
+ }
+
+ public enum TestSummaryFormat {
+ SHORT, // Print information only about tests.
+ TERSE, // Like "SHORT", but even shorter: Do not print PASSED tests.
+ DETAILED, // Print information only about failed test cases.
+ NONE; // Do not print summary.
+
+ /**
+ * 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 = new PathFragment("_tmp");
+
+ // Used for selecting subset of testcase / testmethods.
+ private static final String TEST_BRIDGE_TEST_FILTER_ENV = "TESTBRIDGE_TEST_ONLY";
+
+ private final boolean statusServerRunning;
+ protected final ExecutionOptions executionOptions;
+ protected final BinTools binTools;
+
+ public TestStrategy(OptionsClassProvider requestOptionsProvider,
+ OptionsClassProvider startupOptionsProvider, BinTools binTools) {
+ this.executionOptions = requestOptionsProvider.getOptions(ExecutionOptions.class);
+ this.binTools = binTools;
+ BlazeServerStartupOptions startupOptions =
+ startupOptionsProvider.getOptions(BlazeServerStartupOptions.class);
+ statusServerRunning = startupOptions != null && startupOptions.useWebStatusServer > 0;
+ }
+
+ @Override
+ public abstract void exec(TestRunnerAction action, ActionExecutionContext actionExecutionContext)
+ throws ExecException, InterruptedException;
+
+ @Override
+ public abstract String strategyLocality(TestRunnerAction action);
+
+ /**
+ * Callback for determining the strategy locality.
+ *
+ * @param action the test action
+ * @param localRun whether to run it locally
+ */
+ protected String strategyLocality(TestRunnerAction action, boolean localRun) {
+ return strategyLocality(action);
+ }
+
+ /**
+ * Returns mutable map of default testing shell environment. By itself it is incomplete and is
+ * modified further by the specific test strategy implementations (mostly due to the fact that
+ * environments used locally and remotely are different).
+ */
+ protected Map<String, String> getDefaultTestEnvironment(TestRunnerAction action) {
+ Map<String, String> env = new HashMap<>();
+
+ env.putAll(action.getConfiguration().getDefaultShellEnvironment());
+ env.remove("LANG");
+ env.put("TZ", "UTC");
+ env.put("TEST_SIZE", action.getTestProperties().getSize().toString());
+ env.put("TEST_TIMEOUT", Integer.toString(getTimeout(action)));
+
+ if (action.isSharded()) {
+ env.put("TEST_SHARD_INDEX", Integer.toString(action.getShardNum()));
+ env.put("TEST_TOTAL_SHARDS",
+ Integer.toString(action.getExecutionSettings().getTotalShards()));
+ }
+
+ // When we run test multiple times, set different TEST_RANDOM_SEED values for each run.
+ if (action.getConfiguration().getRunsPerTestForLabel(action.getOwner().getLabel()) > 1) {
+ env.put("TEST_RANDOM_SEED", Integer.toString(action.getRunNumber() + 1));
+ }
+
+ String testFilter = action.getExecutionSettings().getTestFilter();
+ if (testFilter != null) {
+ env.put(TEST_BRIDGE_TEST_FILTER_ENV, testFilter);
+ }
+
+ return env;
+ }
+
+ /**
+ * 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) {
+ if (executionOptions.testAttempts == -1) {
+ return action.getTestProperties().isFlaky() ? 3 : 1;
+ } else {
+ return executionOptions.testAttempts;
+ }
+ }
+
+ /**
+ * 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 int getTimeout(TestRunnerAction testAction) {
+ return executionOptions.testTimeout.get(testAction.getTestProperties().getTimeout());
+ }
+
+ /**
+ * Returns a subset of the environment from the current shell.
+ *
+ * <p>Warning: Since these variables are not part of the configuration's fingerprint, they
+ * MUST NOT be used by any rule or action in such a way as to affect the semantics of that
+ * build step.
+ */
+ public Map<String, String> getAdmissibleShellEnvironment(BuildConfiguration config,
+ Iterable<String> variables) {
+ return getMapping(variables, config.getClientEnv());
+ }
+
+ /*
+ * Finalize test run: persist the result, and post on the event bus.
+ */
+ protected void postTestResult(Executor executor, TestResult result) throws IOException {
+ result.getTestAction().saveCacheStatus(result.getData());
+ executor.getEventBus().post(result);
+ }
+
+ /**
+ * 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 && !statusServerRunning) {
+ return null;
+ }
+
+ try (InputStream fileStream = resultFile.getInputStream()) {
+ return new TestXmlOutputParser().parseXmlIntoTestResult(fileStream);
+ } catch (IOException | TestXmlOutputParserException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 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) throws ExecException,
+ InterruptedException {
+ TestTargetExecutionSettings execSettings = testAction.getExecutionSettings();
+
+ // --nobuild_runfile_links disables runfiles generation only for C++ rules.
+ // In that case, getManifest returns the .runfiles_manifest (input) file,
+ // not the MANIFEST output file of the build-runfiles action. So the
+ // extension ".runfiles_manifest" indicates no runfiles tree.
+ if (!execSettings.getManifest().equals(execSettings.getInputManifest())) {
+ return execSettings.getManifest().getPath().getParentDirectory();
+ }
+
+ // We might need to build runfiles tree now, since it was not created yet
+ // local testing is needed.
+ Path program = execSettings.getExecutable().getPath();
+ Path runfilesDir = program.getParentDirectory().getChild(program.getBaseName() + ".runfiles");
+
+ // 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.getManifest()) {
+ Profiler.instance().logSimpleTask(startTime, ProfilerTask.WAIT, testAction);
+ updateLocalRunfilesDirectory(testAction, runfilesDir, actionExecutionContext, binTools);
+ }
+
+ 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) throws ExecException,
+ InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+
+ TestTargetExecutionSettings execSettings = testAction.getExecutionSettings();
+ try {
+ if (Arrays.equals(runfilesDir.getRelative("MANIFEST").getMD5Digest(),
+ execSettings.getManifest().getPath().getMD5Digest())) {
+ return;
+ }
+ } catch (IOException e1) {
+ // Ignore it - we will just try to create runfiles directory.
+ }
+
+ executor.getEventHandler().handle(Event.progress(
+ "Building runfiles directory for '" + execSettings.getExecutable().prettyPrint() + "'."));
+
+ new SymlinkTreeHelper(execSettings.getManifest().getExecPath(),
+ runfilesDir.relativeTo(executor.getExecRoot()), /* filesetTree= */ false)
+ .createSymlinks(testAction, actionExecutionContext, binTools);
+
+ executor.getEventHandler().handle(Event.progress(testAction.getProgressMessage()));
+ }
+
+ /**
+ * In rare cases, we might write something to stderr. Append it to the real test.log.
+ */
+ protected static void appendStderr(Path stdOut, Path stdErr) throws IOException {
+ FileStatus stat = stdErr.statNullable();
+ OutputStream out = null;
+ InputStream in = null;
+ if (stat != null) {
+ try {
+ if (stat.getSize() > 0) {
+ if (stdOut.exists()) {
+ stdOut.setWritable(true);
+ }
+ out = stdOut.getOutputStream(true);
+ in = stdErr.getInputStream();
+ ByteStreams.copy(in, out);
+ }
+ } finally {
+ Closeables.close(out, true);
+ Closeables.close(in, true);
+ stdErr.delete();
+ }
+ }
+ }
+
+ /**
+ * 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()) {
+ InputStream input = testLogPath.getInputStream();
+ try {
+ ByteStreams.copy(input, outErr.getOutputStream());
+ } finally {
+ input.close();
+ }
+ }
+ }
+ }
+
+}