aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar ulfjack <ulfjack@google.com>2018-07-27 02:37:53 -0700
committerGravatar Copybara-Service <copybara-piper@google.com>2018-07-27 02:39:03 -0700
commit0858ae1f6eb890c1e203a2aa21130ba34ca36a27 (patch)
tree98dd4c592049bf545ecabd6582f2a93d8d43f1d7
parentd0190d3503f3adb0655579312ee359c67291992d (diff)
Add a flag to split test.xml generation into a separate Spawn
At this time, this is only implemented for the StandaloneTestStrategy. This solves a race condition on Posix-like systems, where we cannot guarantee that the pipes are actually fully flushed to disk when the test process exits, and this can cause the test.xml to be empty, which makes it hard to debug issues. (The test.log files do not show up in normal CI systems, only the test.xml files.) Progress on #4608. PiperOrigin-RevId: 206292179
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java5
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java6
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java4
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java7
-rw-r--r--src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java11
-rw-r--r--src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java111
-rw-r--r--src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java3
-rw-r--r--src/test/java/com/google/devtools/build/lib/exec/StandaloneTestStrategyTest.java126
-rwxr-xr-xsrc/test/shell/shell_utils_symlinks_test.sh12
-rwxr-xr-xsrc/test/shell/shell_utils_test.sh12
-rwxr-xr-xsrc/test/shell/unittest_test.sh12
-rw-r--r--tools/test/BUILD6
-rw-r--r--tools/test/generate-xml.sh55
-rwxr-xr-xtools/test/test-setup.sh21
14 files changed, 338 insertions, 53 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java
index 4c4c501d07..2bd7daf1b9 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java
@@ -175,6 +175,11 @@ public class BaseRuleClasses {
.singleArtifact()
.value(env.getToolsLabel("//tools/test:test_setup")))
.add(
+ attr("$xml_generator_script", LABEL)
+ .cfg(HostTransition.INSTANCE)
+ .singleArtifact()
+ .value(env.getToolsLabel("//tools/test:test_xml_generator")))
+ .add(
attr("$collect_coverage_script", LABEL)
.cfg(HostTransition.INSTANCE)
.singleArtifact()
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
index 34cd54fd8c..fffbe7a5ea 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
@@ -177,6 +177,12 @@ public class SkylarkRuleClassFunctions implements SkylarkRuleFunctionsApi<Artifa
.singleArtifact()
.value(labelCache.getUnchecked(toolsRepository + "//tools/test:test_setup")))
.add(
+ attr("$xml_generator_script", LABEL)
+ .cfg(HostTransition.INSTANCE)
+ .singleArtifact()
+ .value(
+ labelCache.getUnchecked(toolsRepository + "//tools/test:test_xml_generator")))
+ .add(
attr("$collect_coverage_script", LABEL)
.cfg(HostTransition.INSTANCE)
.singleArtifact()
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java
index 7fe18a2300..40725da69d 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java
@@ -204,6 +204,9 @@ public final class TestActionBuilder {
Artifact testSetupScript = ruleContext.getHostPrerequisiteArtifact("$test_setup_script");
inputsBuilder.add(testSetupScript);
+ Artifact testXmlGeneratorScript =
+ ruleContext.getHostPrerequisiteArtifact("$xml_generator_script");
+ inputsBuilder.add(testXmlGeneratorScript);
Artifact collectCoverageScript = null;
TreeMap<String, String> extraTestEnv = new TreeMap<>();
@@ -307,6 +310,7 @@ public final class TestActionBuilder {
ruleContext.getActionOwner(),
inputs,
testSetupScript,
+ testXmlGeneratorScript,
collectCoverageScript,
testLog,
cacheStatus,
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
index 3548b9aafc..5827488c09 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
@@ -73,6 +73,7 @@ public class TestRunnerAction extends AbstractAction implements NotifyOnActionCa
private static final String GUID = "cc41f9d0-47a6-11e7-8726-eb6ce83a8cc8";
private final Artifact testSetupScript;
+ private final Artifact testXmlGeneratorScript;
private final Artifact collectCoverageScript;
private final BuildConfiguration configuration;
private final TestConfiguration testConfiguration;
@@ -135,6 +136,7 @@ public class TestRunnerAction extends AbstractAction implements NotifyOnActionCa
ActionOwner owner,
Iterable<Artifact> inputs,
Artifact testSetupScript, // Must be in inputs
+ Artifact testXmlGeneratorScript, // Must be in inputs
@Nullable Artifact collectCoverageScript, // Must be in inputs, if not null
Artifact testLog,
Artifact cacheStatus,
@@ -157,6 +159,7 @@ public class TestRunnerAction extends AbstractAction implements NotifyOnActionCa
configuration.getActionEnvironment());
Preconditions.checkState((collectCoverageScript == null) == (coverageArtifact == null));
this.testSetupScript = testSetupScript;
+ this.testXmlGeneratorScript = testXmlGeneratorScript;
this.collectCoverageScript = collectCoverageScript;
this.configuration = Preconditions.checkNotNull(configuration);
this.testConfiguration =
@@ -741,6 +744,10 @@ public class TestRunnerAction extends AbstractAction implements NotifyOnActionCa
return testSetupScript;
}
+ public Artifact getTestXmlGeneratorScript() {
+ return testXmlGeneratorScript;
+ }
+
@Nullable
public Artifact getCollectCoverageScript() {
return collectCoverageScript;
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
index 953639932f..0f9e2c27cb 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
@@ -309,6 +309,17 @@ public class ExecutionOptions extends OptionsBase {
)
public String executionLogFile;
+ @Option(
+ name = "experimental_split_xml_generation",
+ defaultValue = "false",
+ documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
+ effectTags = {OptionEffectTag.EXECUTION},
+ help = "If this flag is set, and a test action does not generate a test.xml file, then "
+ + "Bazel uses a separate action to generate a dummy test.xml file containing the test log. "
+ + "Otherwise, Bazel generates the test.xml as part of the test action."
+ )
+ public boolean splitXmlGeneration;
+
/** Converter for the --flaky_test_attempts option. */
public static class TestAttemptsConverter extends PerLabelOptions.PerLabelOptionsConverter {
private static final int MIN_VALUE = 1;
diff --git a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
index 6c034d97b9..cb3a4af6d9 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
@@ -17,7 +17,9 @@ package com.google.devtools.build.lib.exec;
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.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionInputHelper;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.EnvironmentalExecException;
import com.google.devtools.build.lib.actions.ExecException;
@@ -30,15 +32,16 @@ import com.google.devtools.build.lib.actions.SpawnActionContext;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.TestExecException;
import com.google.devtools.build.lib.analysis.RunfilesSupplierImpl;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.test.TestActionContext;
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.buildeventstream.BuildEventStreamProtos;
import com.google.devtools.build.lib.buildeventstream.TestFileNameConstants;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
@@ -50,6 +53,7 @@ import com.google.devtools.build.lib.view.test.TestStatus.TestResultData;
import java.io.Closeable;
import java.io.IOException;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@@ -99,10 +103,11 @@ public class StandaloneTestStrategy extends TestStrategy {
Path tmpDir = tmpDirRoot.getChild(TestStrategy.getTmpDirName(action));
Map<String, String> env = setupEnvironment(
action, actionExecutionContext.getClientEnv(), execRoot, runfilesDir, tmpDir);
+ if (executionOptions.splitXmlGeneration) {
+ env.put("EXPERIMENTAL_SPLIT_XML_GENERATION", "1");
+ }
Path workingDirectory = runfilesDir.getRelative(action.getRunfilesPrefix());
- ResolvedPaths resolvedPaths = action.resolve(execRoot);
-
Map<String, String> executionInfo =
new TreeMap<>(action.getTestProperties().getExecutionInfo());
if (!action.shouldCacheResult()) {
@@ -336,7 +341,8 @@ public class StandaloneTestStrategy extends TestStrategy {
long startTime = actionExecutionContext.getClock().currentTimeMillis();
SpawnActionContext spawnActionContext =
actionExecutionContext.getContext(SpawnActionContext.class);
- List<SpawnResult> spawnResults = ImmutableList.of();
+ Path xmlOutputPath = action.resolve(actionExecutionContext.getExecRoot()).getXmlOutputPath();
+ List<SpawnResult> spawnResults = new ArrayList<>();
BuildEventStreamProtos.TestResult.ExecutionInfo.Builder executionInfo =
BuildEventStreamProtos.TestResult.ExecutionInfo.newBuilder();
try {
@@ -347,28 +353,40 @@ public class StandaloneTestStrategy extends TestStrategy {
Reporter.outErrForReporter(actionExecutionContext.getEventHandler()),
testLogPath);
}
- spawnResults = spawnActionContext.exec(spawn, actionExecutionContext);
-
- builder
- .setTestPassed(true)
- .setStatus(BlazeTestStatus.PASSED)
- .setPassedLog(testLogPath.getPathString());
- } catch (SpawnExecException e) {
- // If this method returns normally, then the higher level will rerun the test (up to
- // --flaky_test_attempts times).
- if (e.isCatastrophic()) {
- // Rethrow as the error was catastrophic and thus the build has to be halted.
- throw e;
+ try {
+ spawnResults.addAll(spawnActionContext.exec(spawn, actionExecutionContext));
+ builder
+ .setTestPassed(true)
+ .setStatus(BlazeTestStatus.PASSED)
+ .setPassedLog(testLogPath.getPathString());
+ } catch (SpawnExecException e) {
+ // If this method returns normally, then the higher level will rerun the test (up to
+ // --flaky_test_attempts times).
+ if (e.isCatastrophic()) {
+ // Rethrow as the error was catastrophic and thus the build has to be halted.
+ throw e;
+ }
+ if (!e.getSpawnResult().setupSuccess()) {
+ // Rethrow as the test could not be run and thus there's no point in retrying.
+ throw e;
+ }
+ builder
+ .setTestPassed(false)
+ .setStatus(e.hasTimedOut() ? BlazeTestStatus.TIMEOUT : BlazeTestStatus.FAILED)
+ .addFailedLogs(testLogPath.getPathString());
+ spawnResults.add(e.getSpawnResult());
}
- if (!e.getSpawnResult().setupSuccess()) {
- // Rethrow as the test could not be run and thus there's no point in retrying.
- throw e;
+ // If the test did not create a test.xml, and --experimental_split_xml_generation is
+ // enabled, then we run a separate action to create a test.xml from test.log.
+ if (executionOptions.splitXmlGeneration
+ && action.getTestLog().getPath().exists()
+ && !xmlOutputPath.exists()) {
+ SpawnResult result = Iterables.getOnlyElement(spawnResults);
+ Spawn xmlGeneratingSpawn = createXmlGeneratingSpawn(action, result);
+ // We treat all failures to generate the test.xml here as catastrophic, and won't rerun
+ // the test if this fails.
+ spawnResults.addAll(spawnActionContext.exec(xmlGeneratingSpawn, actionExecutionContext));
}
- builder
- .setTestPassed(false)
- .setStatus(e.hasTimedOut() ? BlazeTestStatus.TIMEOUT : BlazeTestStatus.FAILED)
- .addFailedLogs(testLogPath.getPathString());
- spawnResults = ImmutableList.of(e.getSpawnResult());
} finally {
long endTime = actionExecutionContext.getClock().currentTimeMillis();
long duration = endTime - startTime;
@@ -376,7 +394,7 @@ public class StandaloneTestStrategy extends TestStrategy {
if (!spawnResults.isEmpty()) {
// The SpawnResult of a remotely cached or remotely executed action may not have walltime
// set. We fall back to the time measured here for backwards compatibility.
- SpawnResult primaryResult = Iterables.getOnlyElement(spawnResults);
+ SpawnResult primaryResult = spawnResults.iterator().next();
duration = primaryResult.getWallTime().orElse(Duration.ofMillis(duration)).toMillis();
extractExecutionInfo(primaryResult, builder, executionInfo);
}
@@ -390,11 +408,7 @@ public class StandaloneTestStrategy extends TestStrategy {
}
}
- TestCase details =
- parseTestResult(
- action
- .resolve(actionExecutionContext.getExecRoot())
- .getXmlOutputPath());
+ TestCase details = parseTestResult(xmlOutputPath);
if (details != null) {
builder.setTestCase(details);
}
@@ -432,6 +446,43 @@ public class StandaloneTestStrategy extends TestStrategy {
}
/**
+ * A spawn to generate a test.xml file from the test log. This is only used if the test does not
+ * generate a test.xml file itself.
+ */
+ private Spawn createXmlGeneratingSpawn(TestRunnerAction action, SpawnResult result) {
+ 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().getExecOS() == OS.WINDOWS
+ if (OS.getCurrent() == OS.WINDOWS) {
+ args.add(action.getShExecutable().getPathString());
+ args.add("-c");
+ args.add("$0 $*");
+ }
+ args.add(action.getTestXmlGeneratorScript().getExecPath().getCallablePathString());
+ args.add(action.getTestLog().getExecPathString());
+ args.add(action.getXmlOutputPath().getPathString());
+ args.add(Long.toString(result.getWallTime().orElse(Duration.ZERO).getSeconds()));
+ args.add(Integer.toString(result.exitCode()));
+
+ return new SimpleSpawn(
+ action,
+ ImmutableList.copyOf(args),
+ ImmutableMap.of(
+ "PATH", "/usr/bin:/bin",
+ "TEST_SHARD_INDEX", Integer.toString(action.getShardNum()),
+ "TEST_TOTAL_SHARDS", Integer.toString(action.getExecutionSettings().getTotalShards()),
+ "TEST_NAME", action.getTestName()),
+ ImmutableMap.of(),
+ null,
+ ImmutableMap.of(),
+ /*inputs=*/ ImmutableList.of(action.getTestXmlGeneratorScript(), action.getTestLog()),
+ /*tools=*/ ImmutableList.<Artifact>of(),
+ /*outputs=*/ ImmutableList.of(ActionInputHelper.fromPath(action.getXmlOutputPath())),
+ SpawnAction.DEFAULT_RESOURCE_SET);
+ }
+
+ /**
* 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.
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
index 0d399f2db3..123b58d3ba 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
@@ -126,8 +126,9 @@ public final class BazelAnalysisMock extends AnalysisMock {
"/bazel_tools_workspace/tools/genrule/BUILD", "exports_files(['genrule-setup.sh'])");
config.create("/bazel_tools_workspace/tools/test/BUILD",
- "filegroup(name = 'runtime', srcs = ['test-setup.sh'])",
+ "filegroup(name = 'runtime', srcs = ['test-setup.sh', 'test-xml-generator.sh'])",
"filegroup(name = 'test_setup', srcs = ['test-setup.sh'])",
+ "filegroup(name = 'test_xml_generator', srcs = ['test-xml-generator.sh'])",
"filegroup(name = 'collect_coverage', srcs = ['collect_coverage.sh'])",
"filegroup(name='coverage_support', srcs=['collect_coverage.sh'])",
"filegroup(name = 'coverage_report_generator', srcs = ['coverage_report_generator.sh'])");
diff --git a/src/test/java/com/google/devtools/build/lib/exec/StandaloneTestStrategyTest.java b/src/test/java/com/google/devtools/build/lib/exec/StandaloneTestStrategyTest.java
index ab07910d60..5351a956c3 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/StandaloneTestStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/StandaloneTestStrategyTest.java
@@ -19,6 +19,7 @@ import static com.google.devtools.build.lib.testutil.TestConstants.WORKSPACE_NAM
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.when;
@@ -30,6 +31,7 @@ import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactPathResolver;
+import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnActionContext;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnResult.Status;
@@ -55,6 +57,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
@@ -429,6 +432,7 @@ public final class StandaloneTestStrategyTest extends BuildViewTestCase {
ExecutionOptions executionOptions = Options.getDefaults(ExecutionOptions.class);
executionOptions.testOutput = TestOutputFormat.ERRORS;
+ executionOptions.splitXmlGeneration = false;
Path tmpDirRoot = TestStrategy.getTmpRoot(rootDirectory, outputBase, executionOptions);
BinTools binTools = BinTools.forUnitTesting(directories, analysisMock.getEmbeddedTools());
TestedStandaloneTestStrategy standaloneTestStrategy =
@@ -534,6 +538,128 @@ public final class StandaloneTestStrategyTest extends BuildViewTestCase {
}
@Test
+ public void testThatTestLogAndOutputAreReturnedWithSplitXmlGeneration() throws Exception {
+
+ // setup a StandaloneTestStrategy
+
+ ExecutionOptions executionOptions = Options.getDefaults(ExecutionOptions.class);
+ executionOptions.testOutput = TestOutputFormat.ERRORS;
+ executionOptions.splitXmlGeneration = true;
+ Path tmpDirRoot = TestStrategy.getTmpRoot(rootDirectory, outputBase, executionOptions);
+ BinTools binTools = BinTools.forUnitTesting(directories, analysisMock.getEmbeddedTools());
+ TestedStandaloneTestStrategy standaloneTestStrategy =
+ new TestedStandaloneTestStrategy(executionOptions, binTools, tmpDirRoot);
+
+ // setup a test action
+
+ scratch.file("standalone/failing_test.sh", "this does not get executed, it is mocked out");
+
+ scratch.file(
+ "standalone/BUILD",
+ "sh_test(",
+ " name = \"failing_test\",",
+ " size = \"small\",",
+ " srcs = [\"failing_test.sh\"],",
+ ")");
+
+ ConfiguredTarget configuredTarget = getConfiguredTarget("//standalone:failing_test");
+ List<Artifact> testStatusArtifacts =
+ configuredTarget.getProvider(TestProvider.class).getTestParams().getTestStatusArtifacts();
+ Artifact testStatusArtifact = Iterables.getOnlyElement(testStatusArtifacts);
+ TestRunnerAction testRunnerAction = (TestRunnerAction) getGeneratingAction(testStatusArtifact);
+ FileSystemUtils.createDirectoryAndParents(
+ testRunnerAction.getTestLog().getPath().getParentDirectory());
+ // setup a mock ActionExecutionContext
+
+ when(actionExecutionContext.getClock()).thenReturn(BlazeClock.instance());
+ when(actionExecutionContext.withFileOutErr(any()))
+ .thenAnswer(
+ new Answer<ActionExecutionContext>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public ActionExecutionContext answer(InvocationOnMock invocation) throws Throwable {
+ FileOutErr outErr = (FileOutErr) invocation.getArguments()[0];
+ try (OutputStream stream = outErr.getOutputStream()) {
+ stream.write("This will not appear in the test output: bla\n".getBytes(UTF_8));
+ stream.write((TestLogHelper.HEADER_DELIMITER + "\n").getBytes(UTF_8));
+ stream.write("This will appear in the test output: foo\n".getBytes(UTF_8));
+ }
+ return actionExecutionContext;
+ }
+ });
+ reporter.removeHandler(failFastHandler);
+ when(actionExecutionContext.getExecRoot()).thenReturn(outputBase.getRelative("execroot"));
+ when(actionExecutionContext.getClientEnv()).thenReturn(ImmutableMap.of());
+ when(actionExecutionContext.getEventHandler()).thenReturn(reporter);
+ when(actionExecutionContext.getEventBus()).thenReturn(eventBus);
+ when(actionExecutionContext.getInputPath(any())).thenAnswer(this::getInputPathMock);
+ when(actionExecutionContext.getPathResolver()).thenReturn(ArtifactPathResolver.IDENTITY);
+
+ Path outPath = tmpDirRoot.getRelative("test-out.txt");
+ Path errPath = tmpDirRoot.getRelative("test-err.txt");
+ FileOutErr outErr = new FileOutErr(outPath, errPath);
+ when(actionExecutionContext.getFileOutErr()).thenReturn(outErr);
+
+ SpawnResult testSpawnResult =
+ new SpawnResult.Builder()
+ .setStatus(Status.NON_ZERO_EXIT)
+ .setExitCode(1)
+ .setRunnerName("test")
+ .build();
+ when(spawnActionContext.exec(argThat(new ArgumentMatcher<Spawn>() {
+ @Override
+ public boolean matches(Object argument) {
+ return (argument instanceof Spawn) && ((Spawn) argument).getOutputFiles().size() != 1;
+ }
+ }), any()))
+ .thenThrow(
+ new SpawnExecException(
+ "Failure!!",
+ testSpawnResult,
+ /*forciblyRunRemotely=*/ false,
+ /*catastrophe=*/ false));
+
+ SpawnResult xmlGeneratorSpawnResult =
+ new SpawnResult.Builder()
+ .setStatus(Status.SUCCESS)
+ .setRunnerName("test")
+ .build();
+ when(spawnActionContext.exec(argThat(new ArgumentMatcher<Spawn>() {
+ @Override
+ public boolean matches(Object argument) {
+ return (argument instanceof Spawn) && ((Spawn) argument).getOutputFiles().size() == 1;
+ }
+ }), any()))
+ .thenReturn(ImmutableList.of(xmlGeneratorSpawnResult));
+ when(actionExecutionContext.getContext(same(SpawnActionContext.class)))
+ .thenReturn(spawnActionContext);
+
+ // actual StandaloneTestStrategy execution
+ List<SpawnResult> spawnResults =
+ standaloneTestStrategy.exec(testRunnerAction, actionExecutionContext);
+
+ // check that the rigged SpawnResult was returned
+ assertThat(spawnResults).containsExactly(testSpawnResult, xmlGeneratorSpawnResult);
+ // check that the test log contains all the output
+ String logData = FileSystemUtils.readContent(testRunnerAction.getTestLog().getPath(), UTF_8);
+ assertThat(logData).contains("bla");
+ assertThat(logData).contains(TestLogHelper.HEADER_DELIMITER);
+ assertThat(logData).contains("foo");
+ // check that the test stdout contains all the expected output
+ outErr.close(); // Create the output files.
+ String outData = FileSystemUtils.readContent(outPath, UTF_8);
+ assertThat(outData)
+ .contains("==================== Test output for //standalone:failing_test:");
+ assertThat(outData).doesNotContain("bla");
+ assertThat(outData).doesNotContain(TestLogHelper.HEADER_DELIMITER);
+ assertThat(outData).contains("foo");
+ assertThat(outData)
+ .contains(
+ "================================================================================");
+ assertThat(errPath.exists()).isFalse();
+ }
+
+ @Test
public void testEmptyOutputCreatesEmptyLogFile() throws Exception {
// setup a StandaloneTestStrategy
ExecutionOptions executionOptions = Options.getDefaults(ExecutionOptions.class);
diff --git a/src/test/shell/shell_utils_symlinks_test.sh b/src/test/shell/shell_utils_symlinks_test.sh
index dc822ac620..a673c67ad8 100755
--- a/src/test/shell/shell_utils_symlinks_test.sh
+++ b/src/test/shell/shell_utils_symlinks_test.sh
@@ -23,16 +23,16 @@ if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/
export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
elif [[ -f "$0.runfiles/MANIFEST" ]]; then
export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
- elif [[ -f "$0.runfiles/io_bazel/tools/bash/runfiles/runfiles.bash" ]]; then
+ elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+ export RUNFILES_DIR="$0.runfiles"
+ elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
export RUNFILES_DIR="$0.runfiles"
- elif [[ -f "$TEST_SRCDIR/io_bazel/tools/bash/runfiles/runfiles.bash" ]]; then
- export RUNFILES_DIR="$TEST_SRCDIR"
fi
fi
-if [[ -f "${RUNFILES_DIR:-/dev/null}/io_bazel/tools/bash/runfiles/runfiles.bash" ]]; then
- source "${RUNFILES_DIR}/io_bazel/tools/bash/runfiles/runfiles.bash"
+if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+ source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
- source "$(grep -m1 "^io_bazel/tools/bash/runfiles/runfiles.bash " \
+ source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
"$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
else
echo >&2 "ERROR: cannot find //tools/bash/runfiles:runfiles.bash"
diff --git a/src/test/shell/shell_utils_test.sh b/src/test/shell/shell_utils_test.sh
index daf1e8a9f6..4a008971cf 100755
--- a/src/test/shell/shell_utils_test.sh
+++ b/src/test/shell/shell_utils_test.sh
@@ -23,16 +23,16 @@ if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/
export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
elif [[ -f "$0.runfiles/MANIFEST" ]]; then
export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
- elif [[ -f "$0.runfiles/io_bazel/tools/bash/runfiles/runfiles.bash" ]]; then
+ elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+ export RUNFILES_DIR="$0.runfiles"
+ elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
export RUNFILES_DIR="$0.runfiles"
- elif [[ -f "$TEST_SRCDIR/io_bazel/tools/bash/runfiles/runfiles.bash" ]]; then
- export RUNFILES_DIR="$TEST_SRCDIR"
fi
fi
-if [[ -f "${RUNFILES_DIR:-/dev/null}/io_bazel/tools/bash/runfiles/runfiles.bash" ]]; then
- source "${RUNFILES_DIR}/io_bazel/tools/bash/runfiles/runfiles.bash"
+if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+ source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
- source "$(grep -m1 "^io_bazel/tools/bash/runfiles/runfiles.bash " \
+ source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
"$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
else
echo >&2 "ERROR: cannot find //tools/bash/runfiles:runfiles.bash"
diff --git a/src/test/shell/unittest_test.sh b/src/test/shell/unittest_test.sh
index ecca241772..b65b7aaacc 100755
--- a/src/test/shell/unittest_test.sh
+++ b/src/test/shell/unittest_test.sh
@@ -24,16 +24,16 @@ if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/
export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
elif [[ -f "$0.runfiles/MANIFEST" ]]; then
export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
- elif [[ -f "$0.runfiles/io_bazel/tools/bash/runfiles/runfiles.bash" ]]; then
+ elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+ export RUNFILES_DIR="$0.runfiles"
+ elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
export RUNFILES_DIR="$0.runfiles"
- elif [[ -f "$TEST_SRCDIR/io_bazel/tools/bash/runfiles/runfiles.bash" ]]; then
- export RUNFILES_DIR="$TEST_SRCDIR"
fi
fi
-if [[ -f "${RUNFILES_DIR:-/dev/null}/io_bazel/tools/bash/runfiles/runfiles.bash" ]]; then
- source "${RUNFILES_DIR}/io_bazel/tools/bash/runfiles/runfiles.bash"
+if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+ source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
- source "$(grep -m1 "^io_bazel/tools/bash/runfiles/runfiles.bash " \
+ source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
"$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
else
echo >&2 "ERROR: cannot find //tools/bash/runfiles:runfiles.bash"
diff --git a/tools/test/BUILD b/tools/test/BUILD
index 4a4e56b284..c6a8311033 100644
--- a/tools/test/BUILD
+++ b/tools/test/BUILD
@@ -2,6 +2,7 @@ package(default_visibility = ["//visibility:public"])
# Members of this filegroup shouldn't have duplicate basenames, otherwise
# TestRunnerAction#getRuntimeArtifact() will get confused.
+# Deprecated, do not use.
filegroup(
name = "runtime",
srcs = ["test-setup.sh"],
@@ -13,6 +14,11 @@ filegroup(
)
filegroup(
+ name = "test_xml_generator",
+ srcs = ["generate-xml.sh"],
+)
+
+filegroup(
name = "collect_coverage",
srcs = ["collect_coverage.sh"],
)
diff --git a/tools/test/generate-xml.sh b/tools/test/generate-xml.sh
new file mode 100644
index 0000000000..730b131f38
--- /dev/null
+++ b/tools/test/generate-xml.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+# Copyright 2018 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.
+
+TEST_LOG="$1"
+XML_OUTPUT_FILE="$2"
+DURATION_IN_SECONDS="$3"
+EXIT_CODE="$4"
+
+# Keep this in sync with test-setup.sh!
+function encode_output_file {
+ if [[ -f "$1" ]]; then
+ # Replace invalid XML characters and invalid sequence in CDATA
+ # cf. https://stackoverflow.com/a/7774512/4717701
+ perl -CSDA -pe's/[^\x9\xA\xD\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]+/?/g;' "$1" \
+ | sed 's|]]>|]]>]]<![CDATA[>|g'
+ fi
+}
+
+test_name="${TEST_BINARY#./}"
+errors=0
+error_msg=""
+if (( $EXIT_CODE != 0 )); then
+ errors=1
+ error_msg="<error message=\"exited with error code $EXIT_CODE\"></error>"
+fi
+
+# Ensure that test shards have unique names in the xml output.
+if [[ -n "${TEST_TOTAL_SHARDS+x}" ]] && ((TEST_TOTAL_SHARDS != 0)); then
+ ((shard_num=TEST_SHARD_INDEX+1))
+ test_name="${test_name}"_shard_"${shard_num}"/"${TEST_TOTAL_SHARDS}"
+fi
+
+cat <<EOF >${XML_OUTPUT_FILE}
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites>
+<testsuite name="${test_name}" tests="1" failures="0" errors="${errors}">
+ <testcase name="${test_name}" status="run" duration="${DURATION_IN_SECONDS}" time="${DURATION_IN_SECONDS}">${error_msg}</testcase>
+ <system-out><![CDATA[$(encode_output_file "${TEST_LOG}")]]></system-out>
+</testsuite>
+</testsuites>
+EOF
+
diff --git a/tools/test/test-setup.sh b/tools/test/test-setup.sh
index d8f2a47b67..d7bb931b5f 100755
--- a/tools/test/test-setup.sh
+++ b/tools/test/test-setup.sh
@@ -152,6 +152,7 @@ if [[ -z "$no_echo" ]]; then
echo "-----------------------------------------------------------------------------"
fi
+# Unused if EXPERIMENTAL_SPLIT_XML_GENERATION is set.
function encode_output_file {
if [ -f "$1" ]; then
# Replace invalid XML characters and invalid sequence in CDATA
@@ -161,6 +162,8 @@ function encode_output_file {
fi
}
+# Unused if EXPERIMENTAL_SPLIT_XML_GENERATION is set.
+# Keep this in sync with generate-xml.sh!
function write_xml_output_file {
local duration=$(expr $(date +%s) - $start)
local errors=0
@@ -268,17 +271,27 @@ if [ "$has_tail" == true ] && [ -z "$no_echo" ]; then
wait $pid
exitCode=$?
else
- if [ -z "$COVERAGE_DIR" ]; then
- "${TEST_PATH}" "$@" 2> >(tee -a "${XML_OUTPUT_FILE}.log" >&2) 1> >(tee -a "${XML_OUTPUT_FILE}.log") 2>&1 || exitCode=$?
+ if [[ "${EXPERIMENTAL_SPLIT_XML_GENERATION}" == "1" ]]; then
+ if [ -z "$COVERAGE_DIR" ]; then
+ "${TEST_PATH}" "$@" 2>&1 || exitCode=$?
+ else
+ "$1" "$TEST_PATH" "${@:3}" 2>&1 || exitCode=$?
+ fi
else
- "$1" "$TEST_PATH" "${@:3}" 2> >(tee -a "${XML_OUTPUT_FILE}.log" >&2) 1> >(tee -a "${XML_OUTPUT_FILE}.log") 2>&1 || exitCode=$?
+ if [ -z "$COVERAGE_DIR" ]; then
+ "${TEST_PATH}" "$@" 2> >(tee -a "${XML_OUTPUT_FILE}.log" >&2) 1> >(tee -a "${XML_OUTPUT_FILE}.log") 2>&1 || exitCode=$?
+ else
+ "$1" "$TEST_PATH" "${@:3}" 2> >(tee -a "${XML_OUTPUT_FILE}.log" >&2) 1> >(tee -a "${XML_OUTPUT_FILE}.log") 2>&1 || exitCode=$?
+ fi
fi
fi
for signal in $signals; do
trap - ${signal}
done
-write_xml_output_file
+if [[ "${EXPERIMENTAL_SPLIT_XML_GENERATION}" != "1" ]]; then
+ write_xml_output_file
+fi
# Add all of the files from the undeclared outputs directory to the manifest.
if [[ -n "$TEST_UNDECLARED_OUTPUTS_DIR" && -n "$TEST_UNDECLARED_OUTPUTS_MANIFEST" ]]; then