blob: f0acae1ec55ca4e8fc98968a43e077850a7239ae [file] [log] [blame]
// 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",
executable = ":unused.sh",
filler = ":filler",
inputs = ":in",
)
genrule(
name = "gen",
outs = ["in"],
cmd = "$(location :gen_run.sh) && touch $@",
tools = [":gen_run.sh"],
)
""");
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",
executable = ":all_unused.sh",
inputs = ":in",
)
""");
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",
executable = ":all_unused.sh",
inputs = ":in",
)
""");
buildTarget("//foo:prune");
inPath.delete();
inPath.createSymbolicLink(PathFragment.create("nope"));
buildTarget("//foo:prune");
}
}