Rewrite TestStrategy.getArgs
This changes command-line computation for tests:
- run the coverage collector before the run_under command
- no shell escaping: if all tools just call "$@", then this should work
Note that we still wrap the command in a sub-shell to support shell built-ins
and PATH lookup if the command does not contain a slash character '/'.
A side effect of this change is that the --run_under command now executes in
the test's runfiles directory, rather than in the exec root, if coverage is
enabled at the same time. Inside Google, it's very rare for --run_under to be
used in combination with coverage, and it seems likely to be rare externally
as well, so I don't think it warrants covering it in the release notes.
Also set TEST_BINARY to the root-relative path of the test executable for
all tests (in TestRunnerAction.java).
--
PiperOrigin-RevId: 149275688
MOS_MIGRATED_REVID=149275688
diff --git a/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java
index 904bfb2..1a31d5c 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java
@@ -16,7 +16,6 @@
import com.google.common.annotations.VisibleForTesting;
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.common.io.Closeables;
@@ -33,7 +32,6 @@
import com.google.devtools.build.lib.rules.test.TestRunnerAction;
import com.google.devtools.build.lib.rules.test.TestTargetExecutionSettings;
import com.google.devtools.build.lib.util.OS;
-import com.google.devtools.build.lib.util.ShellEscaper;
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;
@@ -49,7 +47,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -159,10 +156,14 @@
* collect coverage data. If this is an empty string, it is ignored.
* @param testAction The test action.
* @return the command line as string list.
+ * @throws ExecException
*/
protected List<String> getArgs(String coverageScript, TestRunnerAction testAction)
throws ExecException {
List<String> args = Lists.newArrayList();
+ // TODO(ulfjack): This 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
if (OS.getCurrent() == OS.WINDOWS) {
args.add(testAction.getShExecutable().getPathString());
args.add("-c");
@@ -170,41 +171,46 @@
}
Artifact testSetup = testAction.getRuntimeArtifact(TEST_SETUP_BASENAME);
- args.add(testSetup.getExecPathString());
+ args.add(testSetup.getExecPath().getCallablePathString());
+
+ if (testAction.isCoverageMode()) {
+ args.add(coverageScript);
+ }
+
TestTargetExecutionSettings execSettings = testAction.getExecutionSettings();
- List<String> execArgs = new ArrayList<>();
- if (!coverageScript.isEmpty() && testAction.isCoverageMode()) {
- execArgs.add(coverageScript);
+ // Insert the command prefix specified by the "--run_under=<command-prefix>" option, if any.
+ if (execSettings.getRunUnder() != null) {
+ addRunUnderArgs(testAction, args);
}
- // Execute the test using the alias in the runfiles tree, as mandated by
- // the Test Encyclopedia.
- execArgs.add(execSettings.getExecutable().getRootRelativePath().getPathString());
- execArgs.addAll(execSettings.getArgs());
-
- // Insert the command prefix specified by the "--run_under=<command-prefix>" option,
- // if any.
- if (execSettings.getRunUnder() == null) {
- args.addAll(execArgs);
- } else if (execSettings.getRunUnderExecutable() != null) {
- args.add(execSettings.getRunUnderExecutable().getRootRelativePath().getPathString());
- args.addAll(execSettings.getRunUnder().getOptions());
- args.addAll(execArgs);
- } else {
- args.add(testAction.getConfiguration().getShellExecutable().getPathString());
- args.add("-c");
-
- String runUnderCommand = ShellEscaper.escapeString(execSettings.getRunUnder().getCommand());
- args.add(
- runUnderCommand
- + ' '
- + ShellEscaper.escapeJoinAll(
- Iterables.concat(execSettings.getRunUnder().getOptions(), execArgs)));
- }
+ // Execute the test using the alias in the runfiles tree, as mandated by the Test Encyclopedia.
+ args.add(execSettings.getExecutable().getRootRelativePath().getCallablePathString());
+ args.addAll(execSettings.getArgs());
return args;
}
+ private static void addRunUnderArgs(TestRunnerAction testAction, List<String> args) {
+ TestTargetExecutionSettings execSettings = testAction.getExecutionSettings();
+ if (execSettings.getRunUnderExecutable() != null) {
+ args.add(execSettings.getRunUnderExecutable().getRootRelativePath().getCallablePathString());
+ } else {
+ String command = execSettings.getRunUnder().getCommand();
+ // --run_under commands that do not contain '/' are either shell built-ins or need to be
+ // located on the PATH env, so we wrap them in a shell invocation. Note that we shell tokenize
+ // the --run_under parameter and getCommand only returns the first such token.
+ boolean needsShell = !command.contains("/");
+ if (needsShell) {
+ args.add(testAction.getConfiguration().getShellExecutable().getPathString());
+ args.add("-c");
+ args.add("\"$@\"");
+ args.add("/bin/sh"); // Sets $0.
+ }
+ args.add(command);
+ }
+ args.addAll(testAction.getExecutionSettings().getRunUnder().getOptions());
+ }
+
/**
* Returns the number of attempts specific test action can be retried.
*
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestRunnerAction.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestRunnerAction.java
index 06cb421..ac9ba6a 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/test/TestRunnerAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestRunnerAction.java
@@ -362,6 +362,9 @@
env.put("TEST_SIZE", getTestProperties().getSize().toString());
env.put("TEST_TIMEOUT", Integer.toString(timeoutInSeconds));
env.put("TEST_WORKSPACE", getRunfilesPrefix());
+ env.put(
+ "TEST_BINARY",
+ getExecutionSettings().getExecutable().getRootRelativePath().getCallablePathString());
// When we run test multiple times, set different TEST_RANDOM_SEED values for each run.
// Don't override any previous setting.
diff --git a/src/main/java/com/google/devtools/build/lib/worker/WorkerTestStrategy.java b/src/main/java/com/google/devtools/build/lib/worker/WorkerTestStrategy.java
index d4a0fa8..36ab7bf 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/WorkerTestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/WorkerTestStrategy.java
@@ -87,6 +87,10 @@
throw new UserExecException("Tests that do not use the default test runner are incompatible"
+ " with the persistent worker test strategy. Please use another test strategy");
}
+ if (!action.isCoverageMode()) {
+ throw new UserExecException("Coverage is currently incompatible"
+ + " with the persistent worker test strategy. Please use another test strategy");
+ }
List<String> startupArgs = getStartUpArgs(action);
return execInWorker(
@@ -192,7 +196,7 @@
}
private List<String> getStartUpArgs(TestRunnerAction action) throws ExecException {
- List<String> args = getArgs(/*coverageScript=*/ "", action);
+ List<String> args = getArgs(/*coverageScript=*/ "coverage-is-not-supported", action);
ImmutableList.Builder<String> startupArgs = ImmutableList.builder();
// Add test setup with no echo to prevent stdout corruption.
startupArgs.add(args.get(0)).add("--no_echo");