blob: 78211064772f4dd1b173ae22da9450c18a466c2f [file] [log] [blame]
// Copyright 2020 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.buildtool;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.devtools.build.lib.testutil.MoreAsserts.assertNoEvents;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.actions.ActionConflictException;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.BuildFailedException;
import com.google.devtools.build.lib.analysis.AnalysisFailureEvent;
import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.TargetCompletedId;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.Analysis.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.vfs.Path;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for action conflicts. */
@RunWith(TestParameterInjector.class)
public class OutputArtifactConflictTest extends BuildIntegrationTestCase {
@TestParameter boolean skymeld;
@TestParameter boolean minimizeMemory;
static class AnalysisFailureEventListener extends BlazeModule {
private final List<TargetCompletedId> eventIds = new ArrayList<>();
private final List<String> failedTargetNames = new ArrayList<>();
@Override
public void beforeCommand(CommandEnvironment env) {
env.getEventBus().register(this);
}
@Subscribe
public void onAnalysisFailure(AnalysisFailureEvent event) {
eventIds.add(event.getEventId().getTargetCompleted());
failedTargetNames.add(event.getFailedTarget().getLabel().toString());
}
}
private final AnalysisFailureEventListener eventListener = new AnalysisFailureEventListener();
@Override
protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
return super.getRuntimeBuilder().addBlazeModule(eventListener);
}
@Before
public void setup() {
addOptions("--experimental_merged_skyframe_analysis_execution=" + skymeld);
if (minimizeMemory) {
addOptions(
"--notrack_incremental_state",
"--discard_analysis_cache",
"--nokeep_state_after_build",
"--heuristically_drop_nodes",
"--nouse_action_cache");
}
}
private void writeConflictBzl() throws IOException {
write(
"foo/conflict.bzl",
"""
def _conflict_impl(ctx):
inputs = depset(
ctx.files.srcs,
transitive = [dep[DefaultInfo].files for dep in ctx.attr.deps],
)
conflict_output = ctx.actions.declare_file("conflict_output")
other = ctx.actions.declare_file("other" + ctx.attr.name)
ctx.actions.run_shell(
inputs = inputs,
outputs = [conflict_output, other],
command = "touch %s %s" % (conflict_output.path, other.path),
)
return [DefaultInfo(files = depset([conflict_output, other]))]
my_rule = rule(
implementation = _conflict_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
"deps": attr.label_list(providers = [DefaultInfo]),
},
)
""");
}
/**
* Builds the provided targets and asserts expected exceptions.
*
* @return the exit code extracted from the failure detail.
*/
private Code assertThrowsExceptionWhenBuildingTargets(boolean keepGoing, String... targets) {
FailureDetail failureDetail =
keepGoing
? assertThrows(BuildFailedException.class, () -> buildTarget(targets))
.getDetailedExitCode()
.getFailureDetail()
: assertThrows(ViewCreationFailedException.class, () -> buildTarget(targets))
.getFailureDetail();
return Preconditions.checkNotNull(failureDetail).getAnalysis().getCode();
}
@Test
public void testArtifactPrefix(
@TestParameter boolean keepGoing, @TestParameter boolean modifyBuildFile) throws Exception {
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
if (modifyBuildFile) {
write("x/BUILD", "genrule(name = 'y', outs = ['not_y'], cmd = 'touch $@')");
buildTarget("//x:y", "//x/y:y");
write("x/BUILD", "genrule(name = 'y', outs = ['y'], cmd = 'touch $@')");
} else {
write("x/BUILD", "genrule(name = 'y', outs = ['y'], cmd = 'touch $@')");
buildTarget("//x/y:y");
}
assertNoEvents(events.errors());
assertThat(eventListener.failedTargetNames).isEmpty();
addOptions("--keep_going=" + keepGoing);
Code errorCode = assertThrowsExceptionWhenBuildingTargets(keepGoing, "//x/y:y", "//x:y");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ARTIFACT_PREFIX_CONFLICT);
if (keepGoing) {
assertThat(eventListener.failedTargetNames).containsExactly("//x:y", "//x/y:y");
} else {
assertThat(eventListener.failedTargetNames).containsAnyOf("//x:y", "//x/y:y");
}
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("/bin/x/y/whatever' (belonging to //x/y:y)");
events.assertContainsError("/bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
assertThat(events.errors()).hasSize(1);
}
@Test
public void testAspectArtifactSharesPrefixWithTargetArtifact(
@TestParameter boolean keepGoing, @TestParameter boolean modifyBuildFile) throws Exception {
if (modifyBuildFile) {
write("x/BUILD", "genrule(name = 'y', outs = ['y.out'], cmd = 'touch $@')");
} else {
write("x/BUILD", "genrule(name = 'y', outs = ['y.bad'], cmd = 'touch $@')");
}
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
write(
"x/aspect.bzl",
"""
def _aspect_impl(target, ctx):
if not getattr(ctx.rule.attr, "outs", None):
return struct(output_groups = {})
conflict_outputs = list()
for out in ctx.rule.attr.outs:
if out.name[1:] == ".bad":
aspect_out = ctx.actions.declare_file(out.name[:1])
conflict_outputs.append(aspect_out)
cmd = "echo %s > %s" % (out.name, aspect_out.path)
ctx.actions.run_shell(
outputs = [aspect_out],
command = cmd,
)
return [OutputGroupInfo(
files = depset(conflict_outputs),
)]
my_aspect = aspect(implementation = _aspect_impl)
""");
if (modifyBuildFile) {
buildTarget("//x/y", "//x:y");
write("x/BUILD", "genrule(name = 'y', outs = ['y.bad'], cmd = 'touch $@')");
} else {
buildTarget("//x/y");
}
assertNoEvents(events.errors());
assertThat(eventListener.failedTargetNames).isEmpty();
addOptions("--aspects=//x:aspect.bzl%my_aspect", "--output_groups=files");
addOptions("--keep_going=" + keepGoing);
Code errorCode = assertThrowsExceptionWhenBuildingTargets(keepGoing, "//x/y", "//x:y");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ARTIFACT_PREFIX_CONFLICT);
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("/bin/x/y/whatever' (belonging to //x/y:y)");
events.assertContainsError("/bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
assertThat(events.errors()).hasSize(1);
assertThat(eventListener.failedTargetNames).containsAnyOf("//x:y", "//x/y:y");
// We can't be sure if aspect(//x:y) or //x/y:y would trigger the conflict.
skipTheRestIfSkymeldAndMinimizeMemory();
// As we have --output_groups=file, the CTs won't actually be built. Only the
// AnalysisFailureEvent from Aspect(//x:y) is expected even though there are 2 conflicting
// actions.
assertThat(eventListener.eventIds.get(0).getAspect()).isEqualTo("//x:aspect.bzl%my_aspect");
}
@Test
public void testAspectArtifactPrefix(
@TestParameter boolean keepGoing, @TestParameter boolean modifyBuildFile) throws Exception {
if (modifyBuildFile) {
write(
"x/BUILD",
"""
genrule(
name = "y",
outs = ["y.out"],
cmd = "touch $@",
)
genrule(
name = "ydir",
outs = ["y.dir"],
cmd = "touch $@",
)
""");
} else {
write(
"x/BUILD",
"""
genrule(
name = "y",
outs = ["y.bad"],
cmd = "touch $@",
)
genrule(
name = "ydir",
outs = ["y.dir"],
cmd = "touch $@",
)
""");
}
write(
"x/aspect.bzl",
"""
def _aspect_impl(target, ctx):
if not getattr(ctx.rule.attr, "outs", None):
return struct(output_groups = {})
conflict_outputs = list()
for out in ctx.rule.attr.outs:
if out.name[1:] == ".bad":
aspect_out = ctx.actions.declare_file(out.name[:1])
conflict_outputs.append(aspect_out)
cmd = "echo %s > %s" % (out.name, aspect_out.path)
ctx.actions.run_shell(
outputs = [aspect_out],
command = cmd,
)
elif out.name[1:] == ".dir":
aspect_out = ctx.actions.declare_file(out.name[:1] + "/" + out.name)
conflict_outputs.append(aspect_out)
out_dir = aspect_out.path[:len(aspect_out.path) - len(out.name) + 1]
cmd = "mkdir %s && echo %s > %s" % (out_dir, out.name, aspect_out.path)
ctx.actions.run_shell(
outputs = [aspect_out],
command = cmd,
)
return [OutputGroupInfo(
files = depset(conflict_outputs),
)]
my_aspect = aspect(implementation = _aspect_impl)
""");
if (modifyBuildFile) {
buildTarget("//x:y", "//x:ydir");
write(
"x/BUILD",
"""
genrule(
name = "y",
outs = ["y.bad"],
cmd = "touch $@",
)
genrule(
name = "ydir",
outs = ["y.dir"],
cmd = "touch $@",
)
""");
} else {
buildTarget("//x:y");
}
assertNoEvents(events.errors());
assertThat(eventListener.failedTargetNames).isEmpty();
addOptions("--aspects=//x:aspect.bzl%my_aspect", "--output_groups=files");
addOptions("--keep_going=" + keepGoing);
Code errorCode = assertThrowsExceptionWhenBuildingTargets(keepGoing, "//x:ydir", "//x:y");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ARTIFACT_PREFIX_CONFLICT);
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("bin/x/y' (belonging to //x:y)");
events.assertContainsError("bin/x/y/y.dir' (belonging to //x:ydir)");
events.assertContainsError("is a prefix of the other");
assertThat(events.errors()).hasSize(1);
assertThat(eventListener.eventIds.get(0).getAspect()).isEqualTo("//x:aspect.bzl%my_aspect");
if (keepGoing) {
assertThat(eventListener.failedTargetNames).containsExactly("//x:y", "//x:ydir");
} else {
assertThat(eventListener.failedTargetNames).containsAnyOf("//x:y", "//x:ydir");
}
}
@Test
public void testInvalidatedConflict() throws Exception {
writeConflictBzl();
write(
"foo/BUILD",
"""
load("//foo:conflict.bzl", "my_rule")
my_rule(name = "first")
my_rule(name = "second")
""");
assertThrows(
ViewCreationFailedException.class, () -> buildTarget("//foo:first", "//foo:second"));
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
write(
"foo/BUILD",
"""
load("//foo:conflict.bzl", "my_rule")
my_rule(name = "first")
""");
buildTarget("//foo:first");
events.assertNoWarningsOrErrors();
}
@Test
public void testNewTargetConflict(@TestParameter boolean keepGoing) throws Exception {
addOptions("--keep_going=" + keepGoing);
writeConflictBzl();
write(
"foo/BUILD",
"""
load("//foo:conflict.bzl", "my_rule")
my_rule(name = "first")
my_rule(name = "second")
""");
buildTarget("//foo:first");
events.assertNoWarningsOrErrors();
Code errorCode =
assertThrowsExceptionWhenBuildingTargets(keepGoing, "//foo:first", "//foo:second");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ACTION_CONFLICT);
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
assertThat(eventListener.failedTargetNames).hasSize(1);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
}
@Test
public void testTwoOverlappingBuildsHasNoConflict(@TestParameter boolean keepGoing)
throws Exception {
addOptions("--keep_going=" + keepGoing);
writeConflictBzl();
write(
"foo/BUILD",
"""
load("//foo:conflict.bzl", "my_rule")
my_rule(name = "first")
my_rule(name = "second")
""");
// Verify that together they fail, even though no new targets have been analyzed
Code errorCode =
assertThrowsExceptionWhenBuildingTargets(keepGoing, "//foo:first", "//foo:second");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ACTION_CONFLICT);
// Verify that they still don't fail individually, so no state remains
buildTarget("//foo:first");
events.assertNoWarningsOrErrors();
buildTarget("//foo:second");
events.assertNoWarningsOrErrors();
}
@Test
public void testFailingTargetsDoNotCauseActionConflicts() throws Exception {
addOptions("--keep_going");
write(
"x/bad_rule.bzl",
"""
def _impl(ctx):
return list().this_method_does_not_exist()
bad_rule = rule(_impl, attrs = {"deps": attr.label_list()})
""");
write(
"x/BUILD",
"""
load("//x:bad_rule.bzl", "bad_rule")
cc_binary(
name = "y",
srcs = ["y.cc"],
malloc = "//base:system_malloc",
)
bad_rule(
name = "bad",
deps = [":y"],
)
""");
write("x/y/y.cc", "");
write("x/y/BUILD", "cc_library(name = 'y', srcs=['y.cc'])");
write("x/y.cc", "int main() { return 0; }");
try {
buildTarget("//x:y", "//x/y");
fail();
} catch (ViewCreationFailedException e) {
fail("Unexpected artifact prefix conflict: " + e);
} catch (BuildFailedException e) {
// Expected.
}
}
// Regression test for b/184944522.
@Test
public void testConflictErrorAndAnalysisError() throws Exception {
addOptions("--keep_going");
writeConflictBzl();
write(
"foo/BUILD",
"""
load("//foo:conflict.bzl", "my_rule")
my_rule(name = "first")
my_rule(name = "second")
""");
write("x/BUILD", "sh_library(name = 'x', deps = ['//y:y'])");
write("y/BUILD", "sh_library(name = 'y', visibility = ['//visibility:private'])");
assertThrows(
BuildFailedException.class, () -> buildTarget("//x:x", "//foo:first", "//foo:second"));
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
// When targets have conflicting artifacts, one of them "wins" and is successfully built. All
// of the other targets with conflicting artifacts fail.
assertThat(eventListener.failedTargetNames).contains("//x:x");
assertThat(eventListener.failedTargetNames).hasSize(2);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
}
// Verify that an aspect whose analysis is unfinished doesn't fail the conflict reporting process.
@Test
public void testConflictErrorAndUnfinishedAspectAnalysis_mergedAnalysisExecution(
@TestParameter boolean keepGoing) throws Exception {
addOptions("--keep_going=" + keepGoing);
write(
"x/aspect.bzl",
"""
def _aspect_impl(target, ctx):
if not getattr(ctx.rule.attr, "outs", None):
return struct(output_groups = {})
conflict_outputs = list()
for out in ctx.rule.attr.outs:
if out.name[1:] == ".bad":
aspect_out = ctx.actions.declare_file(out.name[:1])
conflict_outputs.append(aspect_out)
cmd = "echo %s > %s" % (out.name, aspect_out.path)
ctx.actions.run_shell(
outputs = [aspect_out],
command = cmd,
)
return [OutputGroupInfo(
files = depset(conflict_outputs),
)]
my_aspect = aspect(implementation = _aspect_impl)
""");
write(
"x/BUILD",
"""
genrule(
name = "y",
outs = ["y.bad"],
cmd = "touch $@",
)
sh_library(
name = "fail_analysis",
deps = ["//private:y"],
)
""");
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
write("private/BUILD", "sh_library(name = 'y', visibility = ['//visibility:private'])");
addOptions("--aspects=//x:aspect.bzl%my_aspect", "--output_groups=files");
Code errorCode =
assertThrowsExceptionWhenBuildingTargets(
keepGoing, "//x/y:y", "//x:y", "//x:fail_analysis");
if (keepGoing) {
assertThat(errorCode).isEqualTo(Code.NOT_ALL_TARGETS_ANALYZED);
events.assertContainsError(
"One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("/bin/x/y/whatever' (belonging to //x/y:y)");
events.assertContainsError("/bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
events.assertContainsError("Analysis of target '//x:fail_analysis' failed");
assertThat(eventListener.failedTargetNames).containsAtLeast("//x:y", "//x:fail_analysis");
} else {
assertThat(errorCode)
.isAnyOf(Code.ARTIFACT_PREFIX_CONFLICT, Code.CONFIGURED_VALUE_CREATION_FAILED);
assertThat(eventListener.failedTargetNames)
.containsAnyOf("//x:y", "//x/y:y", "//x:fail_analysis");
}
}
// This test is documenting current behavior more than enforcing a contract: it might be ok for
// Bazel to suppress the error message about an action conflict, since the relevant actions are
// not run in this build. However, that might cause problems for users who aren't immediately
// alerted when they introduce an action conflict. We already skip exhaustive checks for action
// conflicts in the name of performance and that has prompted complaints, so suppressing actual
// conflicts seems like a bad idea.
//
// While this test is written with aspects, any actions that generate conflicting outputs but
// aren't run would exhibit this behavior.
@Test
public void unusedActionsStillConflict() throws Exception {
// TODO(b/245923465) Limitation with Skymeld.
assume().that(skymeld).isFalse();
write(
"foo/aspect.bzl",
"def _aspect1_impl(target, ctx):",
" outfile = ctx.actions.declare_file('aspect.out')",
" ctx.actions.run_shell(",
" outputs = [outfile],",
" progress_message = 'Action for aspect 1',",
" command = 'echo \"1\" > ' + outfile.path,",
" )",
" return [OutputGroupInfo(files1 = [outfile])]",
"",
"def _aspect2_impl(target, ctx):",
" outfile = ctx.actions.declare_file('aspect.out')",
" ctx.actions.run_shell(",
" outputs = [outfile],",
" progress_message = 'Action for aspect 2',",
" command = 'echo \"2\" > ' + outfile.path,",
" )",
" return [OutputGroupInfo(files2 = [outfile])]",
"",
"def _rule_impl(ctx):",
" outfile = ctx.actions.declare_file('file.out')",
" ctx.actions.run_shell(",
" outputs = [outfile],",
" progress_message = 'Action for target',",
" command = 'touch ' + outfile.path,",
" )",
" return [DefaultInfo(files = depset([outfile]))]",
"aspect1 = aspect(implementation = _aspect1_impl)",
"aspect2 = aspect(implementation = _aspect2_impl)",
"",
"bad_rule = rule(implementation = _rule_impl, attrs = {'deps' : attr.label_list(aspects ="
+ " [aspect1, aspect2])})");
write(
"foo/BUILD",
"""
load("//foo:aspect.bzl", "bad_rule")
sh_library(
name = "dep",
srcs = ["dep.sh"],
)
bad_rule(
name = "foo",
deps = [":dep"],
)
""");
addOptions("--keep_going");
// If Bazel decides to permit this scenario, the build should succeed instead of throwing here.
BuildFailedException buildFailedException =
assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo"));
assertThat(buildFailedException)
.hasMessageThat()
.contains("command succeeded, but not all targets were analyzed");
// We successfully built the output file despite the supposed failure.
Iterable<Artifact> artifacts = getArtifacts("//foo:foo");
assertThat(artifacts).hasSize(1);
assertThat(Iterables.getOnlyElement(artifacts).getPath().exists()).isTrue();
assertThat(
buildFailedException.getDetailedExitCode().getFailureDetail().getAnalysis().getCode())
.isEqualTo(FailureDetails.Analysis.Code.NOT_ALL_TARGETS_ANALYZED);
events.assertContainsError("file 'foo/aspect.out' is generated by these conflicting actions:");
events.assertContainsError(
Pattern.compile(
"Aspects: \\[//foo:aspect.bzl%aspect[12]], \\[//foo:aspect.bzl%aspect[12]]"));
}
@Test
public void testMultipleConflictErrors() throws Exception {
writeConflictBzl();
write(
"foo/BUILD",
"""
load("//foo:conflict.bzl", "my_rule")
my_rule(name = "first")
my_rule(name = "second")
""");
write("x/BUILD", "genrule(name = 'y', outs = ['y'], cmd = 'touch $@')");
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
addOptions("--keep_going");
assertThrows(
BuildFailedException.class,
() -> buildTarget("//x/y", "//x:y", "//foo:first", "//foo:second"));
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
// When targets have conflicting artifacts, one of them "wins" and is successfully built. All
// of the other targets with conflicting artifacts fail.
assertThat(eventListener.failedTargetNames).containsAtLeast("//x:y", "//x/y:y");
assertThat(eventListener.failedTargetNames).hasSize(3);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
}
@Test
public void repeatedConflictBuild() throws Exception {
writeConflictBzl();
write(
"foo/BUILD",
"""
load("//foo:conflict.bzl", "my_rule")
my_rule(name = "first")
my_rule(name = "second")
""");
ViewCreationFailedException e =
assertThrows(
ViewCreationFailedException.class, () -> buildTarget("//foo:first", "//foo:second"));
assertThat(e).hasCauseThat().isInstanceOf(ActionConflictException.class);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
eventListener.failedTargetNames.clear();
e =
assertThrows(
ViewCreationFailedException.class, () -> buildTarget("//foo:first", "//foo:second"));
assertThat(e).hasCauseThat().isInstanceOf(ActionConflictException.class);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
}
@Test
public void testConflictAfterNullBuild(@TestParameter boolean keepGoing) throws Exception {
addOptions("--aspects=//x:aspect.bzl%my_aspect", "--output_groups=files");
addOptions("--keep_going=" + keepGoing);
write("x/BUILD", "genrule(name = 'y', outs = ['y.out'], cmd = 'touch $@')");
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
write(
"x/aspect.bzl",
"""
def _aspect_impl(target, ctx):
if not getattr(ctx.rule.attr, "outs", None):
return struct(output_groups = {})
conflict_outputs = list()
for out in ctx.rule.attr.outs:
if out.name[1:] == ".bad":
aspect_out = ctx.actions.declare_file(out.name[:1])
conflict_outputs.append(aspect_out)
cmd = "echo %s > %s" % (out.name, aspect_out.path)
ctx.actions.run_shell(
outputs = [aspect_out],
command = cmd,
)
return [OutputGroupInfo(
files = depset(conflict_outputs),
)]
my_aspect = aspect(implementation = _aspect_impl)
""");
// First build: no conflict expected.
buildTarget("//x/y", "//x:y");
// Null build
buildTarget("//x/y", "//x:y");
assertNoEvents(events.errors());
assertThat(eventListener.failedTargetNames).isEmpty();
// Modify BUILD file to introduce a conflict.
write("x/BUILD", "genrule(name = 'y', outs = ['y.bad'], cmd = 'touch $@')");
Code errorCode = assertThrowsExceptionWhenBuildingTargets(keepGoing, "//x/y", "//x:y");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ARTIFACT_PREFIX_CONFLICT);
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("/bin/x/y/whatever' (belonging to //x/y:y)");
events.assertContainsError("/bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
assertThat(events.errors()).hasSize(1);
assertThat(eventListener.failedTargetNames).containsAnyOf("//x:y", "//x/y:y");
// We don't know if the conflict is triggered by the aspect(//x:y) or //x/y:y
skipTheRestIfSkymeldAndMinimizeMemory();
assertThat(eventListener.eventIds.get(0).getAspect()).isEqualTo("//x:aspect.bzl%my_aspect");
}
// There exists a discrepancy between skymeld and noskymeld modes in case of --keep_going.
// noskymeld: bazel would stop at the end of the analysis phase and build nothing.
// skymeld: we either finish building one of the 2 conflicting artifacts, or none at all.
//
// The overall build would still fail in both cases.
@Test
public void testTwoConflictingTargets_keepGoing_behaviorDifferences() throws Exception {
addOptions("--keep_going");
write("x/BUILD", "genrule(name = 'y', outs = ['y'], cmd = 'touch $@')");
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
Code errorCode =
assertThrowsExceptionWhenBuildingTargets(/*keepGoing=*/ true, "//x:y", "//x/y:y");
assertThat(errorCode).isEqualTo(Code.NOT_ALL_TARGETS_ANALYZED);
if (minimizeMemory) {
// The states might have been dropped, so we can't check further here.
return;
}
Path outputXY = Iterables.getOnlyElement(getArtifacts("//x:y")).getPath();
Path outputXYY = Iterables.getOnlyElement(getArtifacts("//x/y:y")).getPath();
if (skymeld) {
// Verify that these 2 conflicting artifacts can't both exist.
assertThat(outputXYY.isFile() && outputXY.isFile()).isFalse();
} else {
// Verify that none of the output artifacts were built.
assertThat(outputXY.exists()).isFalse();
assertThat(outputXYY.exists()).isFalse();
}
}
@Test
public void dependencyHasConflict_keepGoing_bothTopLevelTargetsFail() throws Exception {
addOptions("--keep_going");
writeConflictBzl();
write(
"foo/dummy.bzl",
"""
def _path(file):
return file.path
def _impl(ctx):
inputs = depset(
ctx.files.srcs,
transitive = [dep[DefaultInfo].files for dep in ctx.attr.deps],
)
output = ctx.actions.declare_file(ctx.attr.name + ".out")
command = "echo $@ > %s" % (output.path)
args = ctx.actions.args()
args.add_all(inputs, map_each = _path)
ctx.actions.run_shell(
inputs = inputs,
outputs = [output],
command = command,
arguments = [args],
)
return [DefaultInfo(files = depset([output]))]
dummy = rule(
implementation = _impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
"deps": attr.label_list(providers = [DefaultInfo]),
},
)
""");
write(
"foo/BUILD",
"""
load("//foo:conflict.bzl", "my_rule")
load("//foo:dummy.bzl", "dummy")
my_rule(name = "conflict_first")
my_rule(
name = "conflict_second",
deps = [":conflict_first"],
)
dummy(
name = "top_level_a",
deps = [":conflict_second"],
)
dummy(
name = "top_level_b",
deps = [":conflict_second"],
)
""");
assertThrows(
BuildFailedException.class, () -> buildTarget("//foo:top_level_a", "//foo:top_level_b"));
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
assertThat(eventListener.failedTargetNames)
.containsExactly("//foo:top_level_a", "//foo:top_level_b");
}
@Test
public void conflict_noTrackIncrementalState_detected() throws Exception {
assume().that(minimizeMemory).isTrue();
writeConflictBzl();
write(
"foo/BUILD",
"""
load("//foo:conflict.bzl", "my_rule")
my_rule(name = "first")
my_rule(name = "second")
""");
assertThrows(
ViewCreationFailedException.class, () -> buildTarget("//foo:first", "//foo:second"));
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
}
@Test
public void directoryWithNestedFile() throws Exception {
write(
"foo/conflict.bzl",
"""
def _impl(ctx):
dir = ctx.actions.declare_directory(ctx.label.name + ".dir")
file = ctx.actions.declare_file(ctx.label.name + ".dir/file.txt")
ctx.actions.run_shell(
outputs = [dir, file],
command = "mkdir -p $1 && touch $2",
arguments = [dir.path, file.path],
)
return [DefaultInfo(files = depset([dir, file]))]
my_rule = rule(implementation = _impl)
""");
write(
"foo/BUILD",
"""
load(":conflict.bzl", "my_rule")
my_rule(name = "bar")
""");
assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:bar"));
events.assertContainsError("One of the output paths");
events.assertContainsError("is a prefix of the other");
}
@Test
public void directoryWithNestedDirectory() throws Exception {
write(
"foo/conflict.bzl",
"""
def _impl(ctx):
dir = ctx.actions.declare_directory(ctx.label.name + ".dir")
subdir = ctx.actions.declare_directory(ctx.label.name + ".dir/subdir")
ctx.actions.run_shell(
outputs = [dir, subdir],
command = "mkdir -p $1 && mkdir -p $2",
arguments = [dir.path, subdir.path],
)
return [DefaultInfo(files = depset([dir, subdir]))]
my_rule = rule(implementation = _impl)
""");
write(
"foo/BUILD",
"""
load(":conflict.bzl", "my_rule")
my_rule(name = "bar")
""");
assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:bar"));
events.assertContainsError("One of the output paths");
events.assertContainsError("is a prefix of the other");
}
private void skipTheRestIfSkymeldAndMinimizeMemory() {
assume().that(skymeld && minimizeMemory).isFalse();
}
}