| // 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.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.Artifact; |
| import com.google.devtools.build.lib.actions.BuildFailedException; |
| import com.google.devtools.build.lib.actions.MutableActionGraph; |
| 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.Ignore; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| /** Tests for action conflicts. */ |
| @RunWith(TestParameterInjector.class) |
| public class OutputArtifactConflictTest extends BuildIntegrationTestCase { |
| |
| 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); |
| } |
| |
| 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, |
| @TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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, |
| @TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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"); |
| |
| // 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(events.errors()).hasSize(1); |
| assertThat(eventListener.failedTargetNames).containsExactly("//x:y"); |
| assertThat(eventListener.eventIds.get(0).getAspect()).isEqualTo("//x:aspect.bzl%my_aspect"); |
| } |
| |
| @Test |
| public void testAspectArtifactPrefix( |
| @TestParameter boolean keepGoing, |
| @TestParameter boolean modifyBuildFile, |
| @TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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(@TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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')"); |
| events.clear(); |
| buildTarget("//foo:first"); |
| |
| events.assertNoWarningsOrErrors(); |
| } |
| |
| @Test |
| public void testNewTargetConflict( |
| @TestParameter boolean keepGoing, @TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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, @TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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); |
| events.clear(); |
| |
| // 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( |
| @TestParameter boolean mergedAnalysisExecution) throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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; }"); |
| |
| runtimeWrapper.addOptions("--keep_going"); |
| 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(@TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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'])"); |
| addOptions("--keep_going"); |
| |
| 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 |
| @Ignore("b/216490090 - flaky") |
| public void testConflictErrorAndUnfinishedAspectAnalysis_mergedAnalysisExecution( |
| @TestParameter boolean keepGoing) throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution"); |
| 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).containsExactly("//x:y", "//x:fail_analysis"); |
| } else { |
| assertThat(errorCode) |
| .isAnyOf(Code.ARTIFACT_PREFIX_CONFLICT, Code.CONFIGURED_VALUE_CREATION_FAILED); |
| assertThat( |
| eventListener.failedTargetNames.contains("//x:y") |
| ^ eventListener.failedTargetNames.contains("//x:fail_analysis")) |
| .isTrue(); |
| } |
| } |
| |
| // 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 { |
| 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(@TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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(@TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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() |
| .hasCauseThat() |
| .isInstanceOf(MutableActionGraph.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() |
| .hasCauseThat() |
| .isInstanceOf(MutableActionGraph.ActionConflictException.class); |
| assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second"); |
| } |
| |
| @Test |
| public void testConflictAfterNullBuild( |
| @TestParameter boolean keepGoing, @TestParameter boolean mergedAnalysisExecution) |
| throws Exception { |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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).containsExactly("//x:y"); |
| assertThat(eventListener.eventIds.get(0).getAspect()).isEqualTo("//x:aspect.bzl%my_aspect"); |
| } |
| |
| // There exists a discrepancy between --experimental_merged_skyframe_analysis_execution and |
| // otherwise in case of --keep_going. The version with merged phases would still build one of the |
| // 2 conflicting targets, while the one without would stop at the end of the analysis phase and |
| // build nothing. The overall build would still fail. |
| @Test |
| public void testTwoConflictingTargets_keepGoing_behaviorDifferences( |
| @TestParameter boolean mergedAnalysisExecution) throws Exception { |
| addOptions("--keep_going"); |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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"); |
| Path outputXY = Iterables.getOnlyElement(getArtifacts("//x:y")).getPath(); |
| Path outputXYY = Iterables.getOnlyElement(getArtifacts("//x/y:y")).getPath(); |
| |
| if (mergedAnalysisExecution) { |
| // Verify that one and only one of the output artifacts from these 2 targets were built. |
| assertThat((outputXY.isDirectory() && outputXYY.isFile()) ^ outputXY.isFile()).isTrue(); |
| } else { |
| // Verify that none of the output artifacts were built. |
| assertThat(outputXY.exists()).isFalse(); |
| assertThat(outputXYY.exists()).isFalse(); |
| } |
| assertThat(errorCode).isEqualTo(Code.NOT_ALL_TARGETS_ANALYZED); |
| } |
| |
| @Test |
| public void dependencyHasConflict_keepGoing_bothTopLevelTargetsFail( |
| @TestParameter boolean mergedAnalysisExecution) throws Exception { |
| addOptions("--keep_going"); |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); |
| 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"); |
| } |
| } |