Internal change
PiperOrigin-RevId: 476133368
Change-Id: I137993aaeb12ea646416315cd1d56c83074f37f7
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/ExecutionGraphModuleTest.java b/src/test/java/com/google/devtools/build/lib/runtime/ExecutionGraphModuleTest.java
new file mode 100644
index 0000000..f7ca8ea
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/runtime/ExecutionGraphModuleTest.java
@@ -0,0 +1,686 @@
+// Copyright 2022 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.runtime;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import com.github.luben.zstd.ZstdInputStream;
+import com.github.luben.zstd.ZstdOutputStream;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCompletionEvent;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.ArtifactRoot.RootType;
+import com.google.devtools.build.lib.actions.ExecutionGraph;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.SimpleSpawn;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnExecutedEvent;
+import com.google.devtools.build.lib.actions.SpawnMetrics;
+import com.google.devtools.build.lib.actions.SpawnResult;
+import com.google.devtools.build.lib.actions.SpawnResult.Status;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.bugreport.BugReporter;
+import com.google.devtools.build.lib.buildtool.BuildResult;
+import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
+import com.google.devtools.build.lib.clock.BlazeClock;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.exec.util.FakeOwner;
+import com.google.devtools.build.lib.exec.util.SpawnBuilder;
+import com.google.devtools.build.lib.runtime.ExecutionGraphModule.ActionDumpWriter;
+import com.google.devtools.build.lib.runtime.ExecutionGraphModule.DependencyInfo;
+import com.google.devtools.build.lib.testutil.FoundationTestCase;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.UUID;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+/** Unit tests for {@link ExecutionGraphModule}. */
+@RunWith(TestParameterInjector.class)
+public class ExecutionGraphModuleTest extends FoundationTestCase {
+ private ExecutionGraphModule module;
+ private ArtifactRoot artifactRoot;
+
+ @Before
+ public void createModule() {
+ module = new ExecutionGraphModule();
+ }
+
+ @Before
+ public final void initializeRoots() throws Exception {
+ artifactRoot = ArtifactRoot.asDerivedRoot(scratch.resolve("/"), RootType.Output, "output");
+ }
+
+ private static ImmutableList<ExecutionGraph.Node> parse(ByteArrayOutputStream buffer)
+ throws IOException {
+ byte[] data = buffer.toByteArray();
+ try (InputStream in = new ZstdInputStream(new ByteArrayInputStream(data))) {
+ ImmutableList.Builder<ExecutionGraph.Node> nodeListBuilder = new ImmutableList.Builder<>();
+ ExecutionGraph.Node node;
+ while ((node = ExecutionGraph.Node.parseDelimitedFrom(in)) != null) {
+ nodeListBuilder.add(node);
+ }
+ return nodeListBuilder.build();
+ }
+ }
+
+ @Test
+ public void testOneSpawn() throws IOException {
+ // zstd is broken on Windows: https://github.com/bazelbuild/bazel/issues/16041
+ assumeTrue(OS.getCurrent() != OS.WINDOWS);
+
+ UUID uuid = UUID.randomUUID();
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ Spawn spawn =
+ new SimpleSpawn(
+ new FakeOwnerWithPrimaryOutput(
+ "Mnemonic", "Progress message", "//foo", "output/foo/out"),
+ ImmutableList.of("cmd"),
+ ImmutableMap.of("env", "value"),
+ ImmutableMap.of("exec", "value"),
+ /* inputs= */ NestedSetBuilder.emptySet(Order.STABLE_ORDER),
+ /* outputs= */ ImmutableSet.of(ActionInputHelper.fromPath("output/foo/out")),
+ ResourceSet.ZERO);
+ SpawnResult result =
+ new SpawnResult.Builder()
+ .setRunnerName("local")
+ .setStatus(Status.SUCCESS)
+ .setExitCode(0)
+ .setSpawnMetrics(
+ SpawnMetrics.Builder.forLocalExec()
+ .setTotalTime(Duration.ofMillis(1234L))
+ .setExecutionWallTime(Duration.ofMillis(2345L))
+ .setProcessOutputsTime(Duration.ofMillis(3456L))
+ .build())
+ .build();
+ startLogging(eventBus, uuid, buffer, DependencyInfo.NONE);
+ Instant startTimeInstant = Instant.now();
+ module.spawnExecuted(new SpawnExecutedEvent(spawn, result, startTimeInstant));
+ module.buildComplete(
+ new BuildCompleteEvent(new BuildResult(startTimeInstant.toEpochMilli() + 1000)));
+
+ ImmutableList<ExecutionGraph.Node> nodes = parse(buffer);
+ assertThat(nodes).hasSize(1);
+ assertThat(nodes.get(0).getTargetLabel()).isEqualTo("//foo:foo");
+ assertThat(nodes.get(0).getMnemonic()).isEqualTo("Mnemonic");
+ assertThat(nodes.get(0).getMetrics().getDurationMillis()).isEqualTo(1234L);
+ assertThat(nodes.get(0).getMetrics().getFetchMillis()).isEqualTo(0);
+ assertThat(nodes.get(0).getMetrics().getProcessOutputsMillis()).isEqualTo(3456);
+ assertThat(nodes.get(0).getMetrics().getStartTimestampMillis())
+ .isEqualTo(startTimeInstant.toEpochMilli());
+ assertThat(nodes.get(0).getIndex()).isEqualTo(0);
+ assertThat(nodes.get(0).getDependentIndexList()).isEmpty();
+ }
+
+ @Test
+ public void actionDepsWithThreeSpawns() throws IOException {
+ // zstd is broken on Windows: https://github.com/bazelbuild/bazel/issues/16041
+ assumeTrue(OS.getCurrent() != OS.WINDOWS);
+
+ UUID uuid = UUID.randomUUID();
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ ActionInput out1 = ActionInputHelper.fromPath("output/foo/out1");
+ ActionInput out2 = ActionInputHelper.fromPath("output/foo/out2");
+ ActionInput outTop = ActionInputHelper.fromPath("output/foo/out.top");
+
+ Spawn spawnOut1 =
+ new SimpleSpawn(
+ new FakeOwnerWithPrimaryOutput(
+ "Mnemonic", "Progress message", "//foo", out1.getExecPathString()),
+ ImmutableList.of("cmd"),
+ ImmutableMap.of("env", "value"),
+ ImmutableMap.of("exec", "value"),
+ /* inputs= */ NestedSetBuilder.emptySet(Order.STABLE_ORDER),
+ /* outputs= */ ImmutableSet.of(out1),
+ ResourceSet.ZERO);
+ Spawn spawnOut2 =
+ new SimpleSpawn(
+ new FakeOwnerWithPrimaryOutput(
+ "Mnemonic", "Progress message", "//foo", out2.getExecPathString()),
+ ImmutableList.of("cmd"),
+ ImmutableMap.of("env", "value"),
+ ImmutableMap.of("exec", "value"),
+ /* inputs= */ NestedSetBuilder.emptySet(Order.STABLE_ORDER),
+ /* outputs= */ ImmutableSet.of(out2),
+ ResourceSet.ZERO);
+ Spawn spawnTop =
+ new SimpleSpawn(
+ new FakeOwnerWithPrimaryOutput(
+ "Mnemonic", "Progress message", "//foo", outTop.getExecPathString()),
+ ImmutableList.of("cmd"),
+ ImmutableMap.of("env", "value"),
+ ImmutableMap.of("exec", "value"),
+ /* inputs= */ NestedSetBuilder.create(Order.COMPILE_ORDER, out1, out2),
+ /* outputs= */ ImmutableSet.of(outTop),
+ ResourceSet.ZERO);
+ SpawnResult result =
+ new SpawnResult.Builder()
+ .setRunnerName("local")
+ .setStatus(Status.SUCCESS)
+ .setExitCode(0)
+ .setSpawnMetrics(
+ SpawnMetrics.Builder.forLocalExec()
+ .setTotalTime(Duration.ofMillis(1234L))
+ .setExecutionWallTime(Duration.ofMillis(2345L))
+ .setProcessOutputsTime(Duration.ofMillis(3456L))
+ .build())
+ .build();
+ startLogging(eventBus, uuid, buffer, DependencyInfo.ALL);
+ Instant startTimeInstant = Instant.now();
+ module.spawnExecuted(new SpawnExecutedEvent(spawnOut1, result, startTimeInstant));
+ module.spawnExecuted(new SpawnExecutedEvent(spawnOut2, result, startTimeInstant));
+ module.spawnExecuted(new SpawnExecutedEvent(spawnTop, result, startTimeInstant));
+ module.buildComplete(
+ new BuildCompleteEvent(new BuildResult(startTimeInstant.plusMillis(1000).toEpochMilli())));
+
+ ImmutableList<ExecutionGraph.Node> nodes = parse(buffer);
+ assertThat(nodes).hasSize(3);
+
+ assertThat(nodes.get(0).getIndex()).isEqualTo(0);
+ assertThat(nodes.get(0).getDependentIndexList()).isEmpty();
+
+ assertThat(nodes.get(1).getIndex()).isEqualTo(1);
+ assertThat(nodes.get(1).getDependentIndexList()).isEmpty();
+
+ assertThat(nodes.get(2).getIndex()).isEqualTo(2);
+ assertThat(nodes.get(2).getDependentIndexList()).containsExactly(0, 1);
+ }
+
+ private enum FailingOutputStreamFactory {
+ CLOSE {
+ @Override
+ public ZstdOutputStream get() throws IOException {
+ return new ZstdOutputStream(OutputStream.nullOutputStream()) {
+ @Override
+ public synchronized void close() throws IOException {
+ throw new IOException("Simulated close failure");
+ }
+ };
+ }
+ },
+ /** Called from {@link com.google.protobuf.CodedOutputStream#flush}. */
+ WRITE {
+ @Override
+ public ZstdOutputStream get() throws IOException {
+ return new ZstdOutputStream(OutputStream.nullOutputStream()) {
+ @Override
+ public synchronized void write(byte[] b, int off, int len) throws IOException {
+ throw new IOException("oh no!");
+ }
+ };
+ }
+ };
+
+ abstract ZstdOutputStream get() throws IOException;
+ }
+
+ /** Regression test for b/218721483. */
+ @Test(timeout = 30_000)
+ public void failureInOutputDoesNotHang(
+ @TestParameter FailingOutputStreamFactory failingOutputStream) {
+ // zstd is broken on Windows: https://github.com/bazelbuild/bazel/issues/16041
+ assumeTrue(OS.getCurrent() != OS.WINDOWS);
+
+ UUID uuid = UUID.randomUUID();
+ ActionDumpWriter writer =
+ new ActionDumpWriter(
+ BugReporter.defaultInstance(),
+ /*localLockFreeOutputEnabled=*/ false,
+ OutputStream.nullOutputStream(),
+ uuid,
+ DependencyInfo.NONE,
+ -1) {
+ @Override
+ protected void updateLogs(BuildToolLogCollection logs) {}
+
+ @Override
+ protected ZstdOutputStream createCompressingOutputStream() throws IOException {
+ return failingOutputStream.get();
+ }
+ };
+ module.setWriter(writer);
+ eventBus.register(module);
+
+ Instant startTimeInstant = Instant.now();
+ eventBus.post(new BuildCompleteEvent(new BuildResult(startTimeInstant.toEpochMilli() + 1000)));
+ }
+
+ private void startLogging(
+ EventBus eventBus, UUID uuid, OutputStream buffer, DependencyInfo depType) {
+ startLogging(
+ eventBus,
+ BugReporter.defaultInstance(),
+ /*localLockFreeOutputEnabled=*/ false,
+ uuid,
+ buffer,
+ depType);
+ }
+
+ private void startLogging(
+ EventBus eventBus,
+ BugReporter bugReporter,
+ boolean localLockFreeOutputEnabled,
+ UUID uuid,
+ OutputStream buffer,
+ DependencyInfo depType) {
+ ActionDumpWriter writer =
+ new ActionDumpWriter(bugReporter, localLockFreeOutputEnabled, buffer, uuid, depType, -1) {
+ @Override
+ protected void updateLogs(BuildToolLogCollection logs) {}
+ };
+ module.setWriter(writer);
+ eventBus.register(module);
+ }
+
+ @Test
+ public void shutDownWithoutStartTolerated() {
+ eventBus.register(module);
+ Instant startTimeInstant = Instant.now();
+ // Doesn't crash.
+ eventBus.post(new BuildCompleteEvent(new BuildResult(startTimeInstant.toEpochMilli() + 1000)));
+ }
+
+ @Test
+ public void testSpawnWithNullOwnerLabel() throws IOException {
+ // zstd is broken on Windows: https://github.com/bazelbuild/bazel/issues/16041
+ assumeTrue(OS.getCurrent() != OS.WINDOWS);
+
+ UUID uuid = UUID.randomUUID();
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ Spawn spawn =
+ new SimpleSpawn(
+ new FakeOwnerWithPrimaryOutput(
+ "Mnemonic", "Progress message", "//unused:label", "output/foo/out") {
+ @Override
+ public ActionOwner getOwner() {
+ return ActionOwner.SYSTEM_ACTION_OWNER;
+ }
+ },
+ ImmutableList.of("cmd"),
+ ImmutableMap.of("env", "value"),
+ ImmutableMap.of("exec", "value"),
+ /* inputs= */ NestedSetBuilder.emptySet(Order.STABLE_ORDER),
+ /* outputs= */ ImmutableSet.of(ActionInputHelper.fromPath("output/foo/out")),
+ ResourceSet.ZERO);
+ SpawnResult result =
+ new SpawnResult.Builder()
+ .setRunnerName("local")
+ .setStatus(Status.SUCCESS)
+ .setExitCode(0)
+ .setSpawnMetrics(
+ SpawnMetrics.Builder.forLocalExec()
+ .setTotalTime(Duration.ofMillis(1234L))
+ .setExecutionWallTime(Duration.ofMillis(2345L))
+ .setProcessOutputsTime(Duration.ofMillis(3456L))
+ .build())
+ .build();
+ startLogging(eventBus, uuid, buffer, DependencyInfo.NONE);
+ Instant startTimeInstant = Instant.now();
+ module.spawnExecuted(new SpawnExecutedEvent(spawn, result, startTimeInstant));
+ module.buildComplete(
+ new BuildCompleteEvent(new BuildResult(startTimeInstant.toEpochMilli() + 1000)));
+
+ ImmutableList<ExecutionGraph.Node> nodes = parse(buffer);
+ assertThat(nodes).hasSize(1);
+ assertThat(nodes.get(0).getTargetLabel()).isEmpty();
+ }
+
+ @Test
+ public void spawnAndAction_withSameOutputs() throws Exception {
+ // zstd is broken on Windows: https://github.com/bazelbuild/bazel/issues/16041
+ assumeTrue(OS.getCurrent() != OS.WINDOWS);
+
+ var buffer = new ByteArrayOutputStream();
+ startLogging(eventBus, UUID.randomUUID(), buffer, DependencyInfo.ALL);
+ var options = new ExecutionGraphModule.ExecutionGraphOptions();
+ options.logMissedActions = true;
+ module.setOptions(options);
+
+ module.spawnExecuted(
+ new SpawnExecutedEvent(
+ new SpawnBuilder().withOwnerPrimaryOutput(createOutputArtifact("foo/out")).build(),
+ createRemoteSpawnResult(Duration.ofMillis(200)),
+ Instant.ofEpochMilli(100)));
+ module.actionComplete(
+ new ActionCompletionEvent(
+ 0, new ActionsTestUtil.NullAction(createOutputArtifact("foo/out")), null));
+ module.buildComplete(new BuildCompleteEvent(new BuildResult(1000)));
+
+ assertThat(parse(buffer))
+ .containsExactly(
+ executionGraphNodeBuilderForSpawnBuilderSpawn()
+ .setIndex(0)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(100)
+ .setDurationMillis(200)
+ .setOtherMillis(200))
+ .setRunner("remote")
+ .build());
+ }
+
+ @Test
+ public void spawnAndAction_withDifferentOutputs() throws Exception {
+ // zstd is broken on Windows: https://github.com/bazelbuild/bazel/issues/16041
+ assumeTrue(OS.getCurrent() != OS.WINDOWS);
+
+ var buffer = new ByteArrayOutputStream();
+ startLogging(eventBus, UUID.randomUUID(), buffer, DependencyInfo.ALL);
+ var options = new ExecutionGraphModule.ExecutionGraphOptions();
+ options.logMissedActions = true;
+ module.setOptions(options);
+ var nanosToMillis = BlazeClock.createNanosToMillisSinceEpochConverter();
+ module.setNanosToMillis(nanosToMillis);
+
+ module.spawnExecuted(
+ new SpawnExecutedEvent(
+ new SpawnBuilder().withOwnerPrimaryOutput(createOutputArtifact("foo/out")).build(),
+ createRemoteSpawnResult(Duration.ofMillis(200)),
+ Instant.ofEpochMilli(100)));
+ var action = new ActionsTestUtil.NullAction(createOutputArtifact("bar/out"));
+ module.actionComplete(new ActionCompletionEvent(0, action, null));
+ module.buildComplete(new BuildCompleteEvent(new BuildResult(1000)));
+
+ assertThat(parse(buffer))
+ .containsExactly(
+ executionGraphNodeBuilderForSpawnBuilderSpawn()
+ .setIndex(0)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(100)
+ .setDurationMillis(200)
+ .setOtherMillis(200))
+ .setRunner("remote")
+ .build(),
+ executionGraphNodeBuilderForAction(action)
+ .setIndex(1)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(nanosToMillis.toEpochMillis(0)))
+ .build());
+ }
+
+ @Test
+ public void multipleSpawnsWithSameOutput_recordsBothSpawnsWithRetry() throws Exception {
+ // zstd is broken on Windows: https://github.com/bazelbuild/bazel/issues/16041
+ assumeTrue(OS.getCurrent() != OS.WINDOWS);
+
+ var buffer = new ByteArrayOutputStream();
+ startLogging(eventBus, UUID.randomUUID(), buffer, DependencyInfo.ALL);
+ SpawnResult localResult = createLocalSpawnResult(Duration.ofMillis(100));
+ SpawnResult remoteResult = createRemoteSpawnResult(Duration.ofMillis(200));
+ Spawn spawn =
+ new SpawnBuilder().withOwnerPrimaryOutput(createOutputArtifact("foo/out")).build();
+
+ module.spawnExecuted(new SpawnExecutedEvent(spawn, localResult, Instant.EPOCH));
+ module.spawnExecuted(new SpawnExecutedEvent(spawn, remoteResult, Instant.ofEpochMilli(100)));
+ module.buildComplete(new BuildCompleteEvent(new BuildResult(1000)));
+
+ ImmutableList<ExecutionGraph.Node> nodes = parse(buffer);
+ assertThat(nodes)
+ .containsExactly(
+ executionGraphNodeBuilderForSpawnBuilderSpawn()
+ .setIndex(0)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(0)
+ .setDurationMillis(100)
+ .setOtherMillis(100))
+ .setRunner("local")
+ .build(),
+ executionGraphNodeBuilderForSpawnBuilderSpawn()
+ .setIndex(1)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(100)
+ .setDurationMillis(200)
+ .setOtherMillis(200))
+ .setRunner("remote")
+ .setRetryOf(0)
+ .build())
+ .inOrder();
+ }
+
+ enum LocalLockFreeOutput {
+ LOCAL_LOCK_FREE_OUTPUT_ENABLED(/*optionValue=*/ true) {
+ @Override
+ void assertBugReport(BugReporter bugReporter) {
+ verify(bugReporter, never()).sendNonFatalBugReport(any());
+ }
+ },
+ LOCAL_LOCK_FREE_OUTPUT_DISABLED(/*optionValue=*/ false) {
+ @Override
+ void assertBugReport(BugReporter bugReporter) {
+ var captor = ArgumentCaptor.forClass(Exception.class);
+ verify(bugReporter).sendNonFatalBugReport(captor.capture());
+ assertThat(captor.getValue())
+ .hasMessageThat()
+ .contains("Multiple spawns produced 'output/foo/out' with overlapping execution time.");
+ }
+ };
+
+ LocalLockFreeOutput(boolean optionValue) {
+ this.optionValue = optionValue;
+ }
+
+ private final boolean optionValue;
+
+ abstract void assertBugReport(BugReporter bugReporter);
+ }
+
+ @Test
+ public void multipleSpawnsWithSameOutput_overlapping_recordsBothSpawnsWithoutRetry(
+ @TestParameter LocalLockFreeOutput localLockFreeOutput) throws Exception {
+ // zstd is broken on Windows: https://github.com/bazelbuild/bazel/issues/16041
+ assumeTrue(OS.getCurrent() != OS.WINDOWS);
+
+ var buffer = new ByteArrayOutputStream();
+ BugReporter bugReporter = mock(BugReporter.class);
+ startLogging(
+ eventBus,
+ bugReporter,
+ localLockFreeOutput.optionValue,
+ UUID.randomUUID(),
+ buffer,
+ DependencyInfo.ALL);
+ SpawnResult localResult = createLocalSpawnResult(Duration.ofMillis(100));
+ SpawnResult remoteResult = createRemoteSpawnResult(Duration.ofMillis(200));
+ Spawn spawn =
+ new SpawnBuilder().withOwnerPrimaryOutput(createOutputArtifact("foo/out")).build();
+
+ module.spawnExecuted(new SpawnExecutedEvent(spawn, localResult, Instant.EPOCH));
+ module.spawnExecuted(new SpawnExecutedEvent(spawn, remoteResult, Instant.ofEpochMilli(10)));
+ module.buildComplete(new BuildCompleteEvent(new BuildResult(1000)));
+
+ ImmutableList<ExecutionGraph.Node> nodes = parse(buffer);
+ assertThat(nodes)
+ .containsExactly(
+ executionGraphNodeBuilderForSpawnBuilderSpawn()
+ .setIndex(0)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(0)
+ .setDurationMillis(100)
+ .setOtherMillis(100))
+ .setRunner("local")
+ .build(),
+ executionGraphNodeBuilderForSpawnBuilderSpawn()
+ .setIndex(1)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(10)
+ .setDurationMillis(200)
+ .setOtherMillis(200))
+ .setRunner("remote")
+ .build())
+ .inOrder();
+ localLockFreeOutput.assertBugReport(bugReporter);
+ }
+
+ @Test
+ public void multipleSpawnsWithSameOutput_overlapping_ignoresSecondSpawnForDependencies()
+ throws Exception {
+ // zstd is broken on Windows: https://github.com/bazelbuild/bazel/issues/16041
+ assumeTrue(OS.getCurrent() != OS.WINDOWS);
+
+ var buffer = new ByteArrayOutputStream();
+ startLogging(
+ eventBus,
+ BugReporter.defaultInstance(),
+ /*localLockFreeOutputEnabled=*/ true,
+ UUID.randomUUID(),
+ buffer,
+ DependencyInfo.ALL);
+ SpawnResult localResult = createLocalSpawnResult(Duration.ofMillis(100));
+ SpawnResult remoteResult = createRemoteSpawnResult(Duration.ofMillis(200));
+ Artifact input = createOutputArtifact("foo/input");
+ Spawn spawn = new SpawnBuilder().withOwnerPrimaryOutput(input).build();
+ Spawn dependentSpawn =
+ new SpawnBuilder()
+ .withOwnerPrimaryOutput(createOutputArtifact("foo/output"))
+ .withInput(input)
+ .build();
+ SpawnResult dependentResult = createRemoteSpawnResult(Duration.ofMillis(300));
+
+ module.spawnExecuted(new SpawnExecutedEvent(spawn, localResult, Instant.EPOCH));
+ module.spawnExecuted(new SpawnExecutedEvent(spawn, remoteResult, Instant.ofEpochMilli(10)));
+ module.spawnExecuted(
+ new SpawnExecutedEvent(dependentSpawn, dependentResult, Instant.ofEpochMilli(300)));
+ module.buildComplete(new BuildCompleteEvent(new BuildResult(1000)));
+
+ ImmutableList<ExecutionGraph.Node> nodes = parse(buffer);
+ assertThat(nodes)
+ .containsExactly(
+ executionGraphNodeBuilderForSpawnBuilderSpawn()
+ .setIndex(0)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(0)
+ .setDurationMillis(100)
+ .setOtherMillis(100))
+ .setRunner("local")
+ .build(),
+ executionGraphNodeBuilderForSpawnBuilderSpawn()
+ .setIndex(1)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(10)
+ .setDurationMillis(200)
+ .setOtherMillis(200))
+ .setRunner("remote")
+ .build(),
+ executionGraphNodeBuilderForSpawnBuilderSpawn()
+ .setIndex(2)
+ .setMetrics(
+ ExecutionGraph.Metrics.newBuilder()
+ .setStartTimestampMillis(300)
+ .setDurationMillis(300)
+ .setOtherMillis(300))
+ .setRunner("remote")
+ .addDependentIndex(0)
+ .build())
+ .inOrder();
+ }
+
+ private class FakeOwnerWithPrimaryOutput extends FakeOwner {
+
+ private final String primaryOutput;
+
+ public FakeOwnerWithPrimaryOutput(
+ String mnemonic, String progressMessage, String ownerLabel, String primaryOutput) {
+ super(mnemonic, progressMessage, ownerLabel);
+ this.primaryOutput = primaryOutput;
+ }
+
+ @Override
+ public Artifact getPrimaryOutput() {
+ return ActionsTestUtil.createArtifactWithExecPath(
+ artifactRoot, PathFragment.create(primaryOutput));
+ }
+ }
+
+ private Artifact createOutputArtifact(String rootRelativePath) {
+ return ActionsTestUtil.createArtifactWithExecPath(
+ artifactRoot, artifactRoot.getExecPath().getRelative(rootRelativePath));
+ }
+
+ private SpawnResult createLocalSpawnResult(Duration totalTime) {
+ return new SpawnResult.Builder()
+ .setRunnerName("local")
+ .setStatus(Status.SUCCESS)
+ .setExitCode(0)
+ .setSpawnMetrics(SpawnMetrics.Builder.forLocalExec().setTotalTime(totalTime).build())
+ .build();
+ }
+
+ private SpawnResult createRemoteSpawnResult(Duration totalTime) {
+ return new SpawnResult.Builder()
+ .setRunnerName("remote")
+ .setStatus(Status.SUCCESS)
+ .setExitCode(0)
+ .setSpawnMetrics(SpawnMetrics.Builder.forRemoteExec().setTotalTime(totalTime).build())
+ .build();
+ }
+
+ /**
+ * Creates a {@link ExecutionGraph.Node.Builder} with pre-populated defaults for spawns created
+ * using {@link SpawnBuilder}.
+ */
+ private ExecutionGraph.Node.Builder executionGraphNodeBuilderForSpawnBuilderSpawn() {
+ return ExecutionGraph.Node.newBuilder()
+ .setDescription("action 'progress message'")
+ .setTargetLabel("//dummy:label")
+ .setMnemonic("Mnemonic")
+ // This comes from SpawnResult.Builder, which defaults to an empty string.
+ .setRunnerSubtype("");
+ }
+
+ /**
+ * Creates a {@link ExecutionGraph.Node.Builder} with pre-populated defaults for action events.
+ */
+ private ExecutionGraph.Node.Builder executionGraphNodeBuilderForAction(Action action) {
+ return ExecutionGraph.Node.newBuilder()
+ .setDescription(action.prettyPrint())
+ .setTargetLabel(action.getOwner().getLabel().toString())
+ .setMnemonic(action.getMnemonic());
+ }
+}