| // Copyright 2021 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.extensions.proto.ProtoTruth.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.devtools.build.lib.actions.BuildFailedException; | |
| import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; | |
| import com.google.devtools.build.lib.server.FailureDetails; | |
| import com.google.devtools.build.lib.skyframe.DetailedException; | |
| import com.google.devtools.build.lib.util.io.RecordingOutErr; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.testing.junit.testparameterinjector.TestParameter; | |
| import com.google.testing.junit.testparameterinjector.TestParameterInjector; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| /** Tests for Starlark "unused inputs list" functionality on failures caused by unused inputs. */ | |
| @RunWith(TestParameterInjector.class) | |
| public final class UnusedInputsFailureIntegrationTest extends BuildIntegrationTestCase { | |
| @TestParameter private boolean keepGoing; | |
| @Before | |
| public void setOptions() { | |
| addOptions("--keep_going=" + keepGoing); | |
| } | |
| @Test | |
| public void incrementalFailureOnUnusedInput() throws Exception { | |
| RecordingBugReporter bugReporter = recordBugReportsAndReinitialize(); | |
| write( | |
| "foo/pruning.bzl", | |
| "def _impl(ctx):", | |
| " inputs = ctx.attr.inputs.files", | |
| " output = ctx.actions.declare_file(ctx.label.name + '.out')", | |
| " unused_file = ctx.actions.declare_file(ctx.label.name + '.unused')", | |
| " ctx.actions.run(", | |
| " # Make sure original inputs are one level down, so 'leaf unrolling' doesn't get them", | |
| " inputs = depset(transitive = [ctx.attr.filler.files, inputs]),", | |
| " outputs = [output, unused_file],", | |
| " arguments = [output.path, unused_file.path] + [f.path for f in inputs.to_list()],", | |
| " executable = ctx.executable.executable,", | |
| " unused_inputs_list = unused_file,", | |
| " )", | |
| " return DefaultInfo(files = depset([output]))", | |
| "", | |
| "build_rule = rule(", | |
| " attrs = {", | |
| " 'inputs': attr.label(allow_files = True),", | |
| " 'filler': attr.label(allow_files = True),", | |
| " 'executable': attr.label(executable = True, allow_files = True, cfg = 'exec'),", | |
| " },", | |
| " implementation = _impl,", | |
| ")"); | |
| write("foo/unused.sh", "touch $1", "shift", "unused=$1", "shift", "echo $@ > $unused") | |
| .setExecutable(true); | |
| write("foo/gen_run.sh", "true").setExecutable(true); | |
| write("foo/filler"); | |
| write( | |
| "foo/BUILD", | |
| "load('//foo:pruning.bzl', 'build_rule')", | |
| "build_rule(name = 'foo', filler = ':filler', inputs = ':in', executable = ':unused.sh')", | |
| "genrule(name = 'gen', outs = ['in'], tools = [':gen_run.sh'], cmd = '$(location" | |
| + " :gen_run.sh) && touch $@')"); | |
| buildTarget("//foo:foo"); | |
| bugReporter.assertNoExceptions(); | |
| write("foo/gen_run.sh", "false"); | |
| if (keepGoing) { | |
| buildTarget("//foo:foo"); | |
| bugReporter.assertNoExceptions(); | |
| } else { | |
| RecordingOutErr outErr = new RecordingOutErr(); | |
| this.outErr = outErr; | |
| BuildFailedException e = assertThrows(BuildFailedException.class, () -> buildTarget("//foo")); | |
| assertThat(e.getDetailedExitCode().getFailureDetail()) | |
| .comparingExpectedFieldsOnly() | |
| .isEqualTo( | |
| FailureDetails.FailureDetail.newBuilder() | |
| .setExecution( | |
| FailureDetails.Execution.newBuilder() | |
| .setCode(FailureDetails.Execution.Code.UNEXPECTED_EXCEPTION) | |
| .build()) | |
| .build()); | |
| assertThat(outErr.errAsLatin1()).contains("Executing genrule //foo:gen failed"); | |
| Throwable cause = bugReporter.getFirstCause(); | |
| assertThat(cause).hasMessageThat().contains("Error evaluating artifact nested set"); | |
| assertThat(cause).hasMessageThat().contains("foo/gen_run.sh"); | |
| } | |
| } | |
| /** | |
| * Regression test for b/185998331. | |
| * | |
| * <p>The action graph is: | |
| * | |
| * <pre> | |
| * top [consume.out] -> [top.out] | |
| * | | |
| * consume [consume.sh, prune.out] -> [consume.out] | |
| * | | |
| * prune [prune.sh, [bad.out, good.out]] -> [prune.out, unused_list] | |
| * / \ | |
| * bad [bad.sh] -> [bad.out] good [] -> [good.out] | |
| * </pre> | |
| * | |
| * where 'prune' reports 'bad' as an unused input. On the first build, 'consume' fails. On the | |
| * second build, 'bad' fails. If the error is not handled correctly by 'prune', 'top' won't know | |
| * that 'consume' is unavailable. | |
| */ | |
| @Test | |
| public void incrementalFailureOnUnusedInput_downstreamInputNotReady() throws Exception { | |
| write( | |
| "foo/defs.bzl", | |
| "def _example_rule_impl(ctx):", | |
| " bad = ctx.actions.declare_file('bad.out')", | |
| " ctx.actions.run(", | |
| " outputs = [bad],", | |
| " executable = ctx.executable.bad_sh,", | |
| " arguments = [bad.path],", | |
| " )", | |
| "", | |
| " good = ctx.actions.declare_file('good.out')", | |
| " ctx.actions.run_shell(outputs = [good], command = 'touch %s' % good.path)", | |
| "", | |
| " unused_list = ctx.actions.declare_file('unused_list')", | |
| " prune = ctx.actions.declare_file('prune.out')", | |
| " ctx.actions.run(", | |
| " outputs = [prune, unused_list],", | |
| " inputs = [bad, good],", | |
| " unused_inputs_list = unused_list,", | |
| " executable = ctx.executable.prune_sh,", | |
| " arguments = [prune.path, unused_list.path, bad.path],", | |
| " )", | |
| "", | |
| " consume = ctx.actions.declare_file('consume.out')", | |
| " ctx.actions.run(", | |
| " outputs = [consume],", | |
| " inputs = [prune],", | |
| " executable = ctx.executable.consume_sh,", | |
| " arguments = [consume.path],", | |
| " )", | |
| "", | |
| " top = ctx.actions.declare_file('top.out')", | |
| " ctx.actions.run_shell(", | |
| " outputs = [top],", | |
| " inputs = [consume],", | |
| " command = 'touch %s' % top.path,", | |
| " )", | |
| " return DefaultInfo(files = depset([top]))", | |
| "", | |
| "example_rule = rule(", | |
| " implementation = _example_rule_impl,", | |
| " attrs = {", | |
| " 'bad_sh': attr.label(", | |
| " executable = True, allow_single_file = True, cfg = 'exec', default = 'bad.sh'", | |
| " ),", | |
| " 'prune_sh': attr.label(", | |
| " executable = True, allow_single_file = True, cfg = 'exec', default = 'prune.sh'", | |
| " ),", | |
| " 'consume_sh': attr.label(", | |
| " executable = True, allow_single_file = True, cfg = 'exec', default = 'consume.sh'", | |
| " ),", | |
| " },", | |
| ")"); | |
| write("foo/BUILD", "load(':defs.bzl', 'example_rule')", "example_rule(name = 'example')"); | |
| write("foo/bad.sh", "#!/bin/bash", "touch $1").setExecutable(true); | |
| write("foo/prune.sh", "#!/bin/bash", "touch $1 && echo $3 > $2").setExecutable(true); | |
| write("foo/consume.sh", "#!/bin/bash", "exit 1").setExecutable(true); | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:example")); | |
| assertContainsError("Action foo/consume.out failed"); | |
| write("foo/bad.sh", "#!/bin/bash", "exit 1").setExecutable(true); | |
| write("foo/consume.sh", "#!/bin/bash", "touch $@").setExecutable(true); | |
| if (keepGoing) { | |
| buildTarget("//foo:example"); | |
| } else { | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:example")); | |
| assertContainsError("Action foo/bad.out failed"); | |
| } | |
| } | |
| @Test | |
| public void incrementalUnusedSymlinkCycle() throws Exception { | |
| RecordingBugReporter bugReporter = recordBugReportsAndReinitialize(); | |
| write( | |
| "foo/pruning.bzl", | |
| "def _impl(ctx):", | |
| " inputs = ctx.attr.inputs.files", | |
| " output = ctx.actions.declare_file(ctx.label.name + '.out')", | |
| " unused_inputs_list = ctx.actions.declare_file(ctx.label.name + '.unused')", | |
| " arguments = [output.path, unused_inputs_list.path]", | |
| " for input in inputs.to_list():", | |
| " arguments += [input.path]", | |
| " ctx.actions.run(", | |
| " inputs = inputs,", | |
| " outputs = [output, unused_inputs_list],", | |
| " arguments = arguments,", | |
| " executable = ctx.executable.executable,", | |
| " unused_inputs_list = unused_inputs_list,", | |
| " )", | |
| " return DefaultInfo(files = depset([output]))", | |
| "", | |
| "build_rule = rule(", | |
| " attrs = {", | |
| " 'inputs': attr.label(allow_files = True),", | |
| " 'executable': attr.label(executable = True, allow_files = True, cfg = 'exec'),", | |
| " },", | |
| " implementation = _impl,", | |
| ")"); | |
| Path unusedSh = | |
| write("foo/all_unused.sh", "touch $1", "shift", "unused=$1", "shift", "echo $@ > $unused"); | |
| unusedSh.setExecutable(true); | |
| Path inPath = write("foo/in"); | |
| write( | |
| "foo/BUILD", | |
| "load('//foo:pruning.bzl', 'build_rule')", | |
| "build_rule(name = 'prune', inputs = ':in', executable = ':all_unused.sh')"); | |
| buildTarget("//foo:prune"); | |
| bugReporter.assertNoExceptions(); | |
| inPath.delete(); | |
| inPath.createSymbolicLink(PathFragment.create("in")); | |
| if (keepGoing) { | |
| buildTarget("//foo:prune"); | |
| bugReporter.assertNoExceptions(); | |
| } else { | |
| RecordingOutErr outErr = new RecordingOutErr(); | |
| this.outErr = outErr; | |
| BuildFailedException e = | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:prune")); | |
| assertDetailedExitCodeIsSourceIOFailure(e); | |
| Throwable cause = bugReporter.getFirstCause(); | |
| assertDetailedExitCodeIsSourceIOFailure(cause); | |
| assertThat(cause).hasMessageThat().isEqualTo("error reading file '//foo:in': Symlink cycle"); | |
| assertThat(outErr.errAsLatin1()).contains("error reading file '//foo:in': Symlink cycle"); | |
| } | |
| } | |
| private static final FailureDetails.FailureDetail SOURCE_IO_FAILURE = | |
| FailureDetails.FailureDetail.newBuilder() | |
| .setExecution( | |
| FailureDetails.Execution.newBuilder() | |
| .setCode(FailureDetails.Execution.Code.SOURCE_INPUT_IO_EXCEPTION)) | |
| .build(); | |
| private static void assertDetailedExitCodeIsSourceIOFailure(Throwable exception) { | |
| assertThat(exception).isInstanceOf(DetailedException.class); | |
| assertThat(((DetailedException) exception).getDetailedExitCode().getFailureDetail()) | |
| .comparingExpectedFieldsOnly() | |
| .isEqualTo(SOURCE_IO_FAILURE); | |
| } | |
| @Test | |
| public void incrementalUnusedDanglingSymlink() throws Exception { | |
| write( | |
| "foo/pruning.bzl", | |
| "def _impl(ctx):", | |
| " inputs = ctx.attr.inputs.files", | |
| " output = ctx.actions.declare_file(ctx.label.name + '.out')", | |
| " unused_inputs_list = ctx.actions.declare_file(ctx.label.name + '.unused')", | |
| " arguments = [output.path, unused_inputs_list.path]", | |
| " for input in inputs.to_list():", | |
| " arguments += [input.path]", | |
| " ctx.actions.run(", | |
| " inputs = inputs,", | |
| " outputs = [output, unused_inputs_list],", | |
| " arguments = arguments,", | |
| " executable = ctx.executable.executable,", | |
| " unused_inputs_list = unused_inputs_list,", | |
| " )", | |
| " return DefaultInfo(files = depset([output]))", | |
| "", | |
| "build_rule = rule(", | |
| " attrs = {", | |
| " 'inputs': attr.label(allow_files = True),", | |
| " 'executable': attr.label(executable = True, allow_files = True, cfg = 'exec'),", | |
| " },", | |
| " implementation = _impl,", | |
| ")"); | |
| Path unusedSh = | |
| write("foo/all_unused.sh", "touch $1", "shift", "unused=$1", "shift", "echo $@ > $unused"); | |
| unusedSh.setExecutable(true); | |
| Path inPath = write("foo/in"); | |
| write( | |
| "foo/BUILD", | |
| "load('//foo:pruning.bzl', 'build_rule')", | |
| "build_rule(name = 'prune', inputs = ':in', executable = ':all_unused.sh')"); | |
| buildTarget("//foo:prune"); | |
| inPath.delete(); | |
| inPath.createSymbolicLink(PathFragment.create("nope")); | |
| buildTarget("//foo:prune"); | |
| } | |
| } |