blob: f13093b66f6cb2d670115b4a37667a94ae154579 [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.extensions.proto.ProtoTruth.assertThat;
import static com.google.devtools.build.lib.testutil.BlazeTestUtils.createFilesetRule;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.BuildFailedException;
import com.google.devtools.build.lib.actions.FileStateValue;
import com.google.devtools.build.lib.buildtool.util.GoogleBuildIntegrationTestCase;
import com.google.devtools.build.lib.cmdline.TargetParsingException;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.skyframe.DetailedException;
import com.google.devtools.build.lib.testutil.MoreAsserts;
import com.google.devtools.build.lib.testutil.Suite;
import com.google.devtools.build.lib.testutil.TestSpec;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.lib.unix.UnixFileSystem;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.io.RecordingOutErr;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.Dirent;
import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.NotifyingHelper;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Integration tests with a custom filesystem layer, for faking things like IOExceptions, on top of
* the real unix filesystem (so we can execute actions).
*/
@TestSpec(size = Suite.MEDIUM_TESTS)
@RunWith(TestParameterInjector.class)
public class CustomRealFilesystemBuildIntegrationTest extends GoogleBuildIntegrationTestCase {
private CustomRealFilesystem customFileSystem = null;
@Override
protected FileSystem createFileSystem() {
if (customFileSystem == null) {
customFileSystem = new CustomRealFilesystem(getDigestHashFunction());
}
return customFileSystem;
}
/** Tests that IOExceptions encountered while handling inputs are properly handled. */
@Test
public void testIOException() throws Exception {
Path fooBuildFile = write("foo/BUILD", "sh_binary(name = 'foo', srcs = ['foo.sh'])");
Path fooShFile = fooBuildFile.getParentDirectory().getRelative("foo.sh");
customFileSystem.alwaysError(fooShFile);
assertThrows(BuildFailedException.class, () -> buildTarget("//foo"));
events.assertContainsError("//foo:foo: error reading file '//foo:foo.sh': nope");
}
/** Tests that IOExceptions encountered while handling inputs are properly handled. */
@Test
public void testIOExceptionMidLevel() throws Exception {
Path fooBuildFile =
write(
"foo/BUILD",
"sh_binary(name = 'foo', srcs = ['foo.sh'])",
"genrule(name = 'top', srcs = [':foo'], outs = ['out'], cmd = 'touch $@')");
Path fooShFile = fooBuildFile.getParentDirectory().getRelative("foo.sh");
customFileSystem.alwaysError(fooShFile);
assertThrows(BuildFailedException.class, () -> buildTarget("//foo:top"));
events.assertContainsError(
"Executing genrule //foo:top failed: error reading file '//foo:foo.sh': nope");
}
@Test
public void globDanglingSymlink() throws Exception {
Path packageDirPath = write("foo/BUILD", "exports_files(glob(['*.txt']))").getParentDirectory();
write("foo/existing.txt");
Path badSymlink = packageDirPath.getChild("bad.txt");
FileSystemUtils.ensureSymbolicLink(badSymlink, "nope");
customFileSystem.alwaysError(badSymlink);
TargetParsingException e =
assertThrows(TargetParsingException.class, () -> buildTarget("//foo:all"));
assertThat(e).hasMessageThat().contains("no such package 'foo': error globbing [*.txt]: nope");
}
/**
* Tests that IOExceptions encountered while handling non-mandatory inputs are properly handled.
*/
@Test
public void testIOException_nonMandatoryInputs() throws Exception {
addOptions("--features=cc_include_scanning");
write("foo/BUILD", "cc_library(name = 'foo', srcs = ['foo.cc'], hdrs_check = 'loose')");
write("foo/foo.cc", "#include \"foo/foo.h\"");
Path fooHFile = write("foo/foo.h", "//thisisacomment");
customFileSystem.alwaysErrorAfter(fooHFile, 1);
BuildFailedException e = assertThrows(BuildFailedException.class, () -> buildTarget("//foo"));
assertThat(e.getDetailedExitCode().getExitCode()).isEqualTo(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
events.assertContainsError(
"foo/BUILD:1:11: Compiling foo/foo.cc failed: include scanning: Include scanning"
+ " IOException: nope");
}
@Test
public void ioExceptionInSkyframeOptionalInput(@TestParameter boolean keepGoing)
throws Exception {
addOptions("--keep_going=" + keepGoing);
addOptions("--features=cc_include_scanning");
write("foo/BUILD", "cc_library(name = 'foo', srcs = ['foo.cc'], hdrs_check = 'loose')");
Path ccFile = write("foo/foo.cc", "#include \"foo/foo.h\"");
// Making the destination a symlink keeps the include scanner from populating the syscalls cache
// before Skyframe gets a chance to stat the bad file.
ccFile.getParentDirectory().getChild("foo.h").createSymbolicLink(PathFragment.create("bad.h"));
Path badFile = write("foo/bad.h", "//ok contents");
customFileSystem.alwaysError(badFile);
BuildFailedException e = assertThrows(BuildFailedException.class, () -> buildTarget("//foo"));
assertThat(e.getDetailedExitCode().getExitCode()).isEqualTo(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
events.assertContainsError(
"foo/BUILD:1:11: Compiling foo/foo.cc failed: error reading file 'foo/foo.h': nope");
}
@Test
public void incrementalNonMandatoryInputIOException(
@TestParameter boolean keepGoing, @TestParameter({"0", "1"}) int nestedSetOnSkyframe)
throws Exception {
RecordingBugReporter bugReporter = recordBugReportsAndReinitialize();
addOptions("--features=cc_include_scanning");
addOptions("--keep_going=" + keepGoing);
addOptions("--experimental_nested_set_as_skykey_threshold=" + nestedSetOnSkyframe);
write("foo/BUILD", "cc_library(name = 'foo', srcs = ['foo.cc'], hdrs_check = 'loose')");
write("foo/foo.cc", "#include \"foo/foo.h\"");
Path fooHFile = write("foo/foo.h", "//thisisacomment");
buildTarget("//foo");
bugReporter.assertNoExceptions();
write("foo/foo.cc", "//no include anymore");
customFileSystem.alwaysError(fooHFile);
if (keepGoing) {
buildTarget("//foo");
bugReporter.assertNoExceptions();
} else {
RecordingOutErr outErr = new RecordingOutErr();
this.outErr = outErr;
BuildFailedException e = assertThrows(BuildFailedException.class, () -> buildTarget("//foo"));
assertDetailedExitCodeIsSourceIOFailure(e);
Throwable cause = bugReporter.getFirstCause();
assertThat(cause).hasMessageThat().isEqualTo("error reading file 'foo/foo.h': nope");
assertDetailedExitCodeIsSourceIOFailure(cause);
assertThat(outErr.errAsLatin1()).contains("error reading file 'foo/foo.h': nope");
}
}
@Test
public void unusedInputIOExceptionIncremental(@TestParameter boolean keepGoing) throws Exception {
RecordingBugReporter bugReporter = recordBugReportsAndReinitialize();
addOptions("--keep_going=" + keepGoing);
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(",
" inputs = 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),",
" 'executable': attr.label(executable = True, allow_files = True, cfg = 'host'),",
" },",
" implementation = _impl,",
")");
Path unusedSh =
write("foo/all_unused.sh", "touch $1", "shift", "unused=$1", "shift", "echo $@ > $unused");
unusedSh.setExecutable(true);
write(
"foo/BUILD",
"load('//foo:pruning.bzl', 'build_rule')",
"build_rule(name = 'prune', inputs = ':unused.txt', executable = ':all_unused.sh')");
Path unusedPath = write("foo/unused.txt");
buildTarget("//foo:prune");
bugReporter.assertNoExceptions();
customFileSystem.alwaysError(unusedPath);
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();
assertThat(cause).hasMessageThat().isEqualTo("error reading file '//foo:unused.txt': nope");
assertDetailedExitCodeIsSourceIOFailure(cause);
assertThat(outErr.errAsLatin1()).contains("error reading file '//foo:unused.txt': nope");
}
}
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);
}
/** Tests that IOExceptions encountered when not all discovered deps are done are handled. */
@Test
public void testIOException_missingNonMandatoryInput() throws Exception {
addOptions("--features=cc_include_scanning");
Path fooBuildFile =
write("foo/BUILD", "cc_library(name = 'foo', srcs = ['foo.cc'], hdrs_check = 'loose')");
write("foo/foo.cc", "#include \"foo/error.h\"", "#include \"foo/other.h\"");
Path errorHFile = fooBuildFile.getParentDirectory().getRelative("error.h");
Path otherHFile = fooBuildFile.getParentDirectory().getRelative("other.h");
writeAbsolute(errorHFile, "//thisisacomment");
writeAbsolute(otherHFile, "//thisisacomment");
customFileSystem.alwaysErrorAfter(errorHFile, 1);
getSkyframeExecutor()
.getEvaluatorForTesting()
.injectGraphTransformerForTesting(
NotifyingHelper.makeNotifyingTransformer(
(key, type, order, context) -> {
if (key.functionName().equals(FileStateValue.FILE_STATE)
&& ((RootedPath) key.argument())
.getRootRelativePath()
.getPathString()
.endsWith("foo/other.h")) {
try {
Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
throw new IllegalStateException("Should have been interrupted by failure");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}));
BuildFailedException e = assertThrows(BuildFailedException.class, () -> buildTarget("//foo"));
assertThat(e.getDetailedExitCode().getExitCode()).isEqualTo(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
events.assertContainsError(
"foo/BUILD:1:11: Compiling foo/foo.cc failed: include scanning: Include scanning"
+ " IOException: nope");
}
/**
* Tests that IOExceptions encountered while handling non-mandatory generated inputs are properly
* handled.
*/
@Test
public void testIOException_nonMandatoryGeneratedInputs() throws Exception {
addOptions("--features=cc_include_scanning");
write(
"bar/BUILD",
"cc_library(",
" name = 'bar',",
" hdrs = ['bar.h'],",
" srcs = ['bar.cc'],",
" visibility = ['//foo:__pkg__']",
")",
"genrule(name = 'bar-gen', srcs = ['in.txt'], outs = ['bar.h'], "
+ "cmd = \"cp $(SRCS) $(OUTS)\")");
write("foo/BUILD",
"cc_binary(name = 'foo', srcs = ['foo.cc'], deps = [':mid'])",
"cc_library(name = 'mid', deps = ['//bar'])");
write("foo/foo.cc",
"#include \"bar/bar.h\"",
"int main() { return f(); }");
write("bar/bar.cc", "int f() { return 0; }");
write("bar/in.txt", "int f(); // 0");
// On an incremental skyframe build, the output file from a genrule is accessed 6 times via this
// test's instrumented methods.
// FilesystemValueChecker
// ActionCacheChecker#needToExecute
// Path#getDigest (from ActionCacheChecker)
// Internal remote execution client to check if symlink
// SpawnIncludeScanner#shouldparseRemotely
// IncludeParser#extractInclusions
int numAccessesOnIncrementalBuildWithChange = 6;
buildTarget("//bar");
Path barHOutputPath = Iterables.getOnlyElement(getArtifacts("//bar:bar.h")).getPath();
customFileSystem.alwaysErrorAfter(barHOutputPath, numAccessesOnIncrementalBuildWithChange);
write("bar/in.txt", "int f(); // 1");
buildTarget("//foo");
// Check that the expected number of stats were made on the generated file (note that this
// sufficiently confirms that the genrule was rerun).
// TODO(bazel-team): This test currently isn't super useful because Skyframe doesn't declare
// deps on undeclared generated headers (it only does so for source headers). If we change this
// behavior, we can test it by having the final stat done on the output file (on a clean build)
// fail, but all the previous ones done (e.g. during the genrule execution) succeed. But for now
// we just check the exact number of stats done.
assertThat(customFileSystem.getNumCallsUntilError(barHOutputPath)).isEqualTo(0);
customFileSystem.alwaysErrorAfter(barHOutputPath, numAccessesOnIncrementalBuildWithChange);
write("bar/in.txt", "int f(); // 2");
buildTarget("//foo");
assertThat(customFileSystem.getNumCallsUntilError(barHOutputPath)).isEqualTo(0);
}
@Test
public void ioExceptionReadingBuildFileForDiscoveredInput() throws Exception {
addOptions("--features=cc_include_scanning");
write("hello/BUILD", "cc_library(name = 'hello', srcs = ['hello.cc'], hdrs_check = 'loose')");
write("hello/hello.cc", "#include \"hello/subdir/undeclared.h\"");
Path header = write("hello/subdir/undeclared.h");
Path buildFile = header.getParentDirectory().getChild("BUILD");
// Error unfortunately not noticed when we find header directly.
customFileSystem.alwaysError(buildFile);
addOptions("--discard_analysis_cache"); // Fall back to action cache on next build.
buildTarget("//hello:hello");
BuildFailedException e =
assertThrows(BuildFailedException.class, () -> buildTarget("//hello:hello"));
MoreAsserts.assertContainsEvent(
events.collector(),
Pattern.compile(
"^ERROR.*Compiling hello/hello.cc failed: Unable to resolve hello/subdir/undeclared.h"
+ " as an artifact: no such package 'hello/subdir': IO errors while looking for"
+ " BUILD file reading .*hello/subdir/BUILD: nope"));
assertThat(e.getDetailedExitCode().getFailureDetail())
.comparingExpectedFieldsOnly()
.isEqualTo(
FailureDetails.FailureDetail.newBuilder()
.setIncludeScanning(
FailureDetails.IncludeScanning.newBuilder()
.setCode(FailureDetails.IncludeScanning.Code.SYSTEM_PACKAGE_LOAD_FAILURE)
.setPackageLoadingCode(
FailureDetails.PackageLoading.Code.OTHER_IO_EXCEPTION))
.build());
}
@Test
public void inconsistentExceptionReadingBuildFileForDiscoveredInput() throws Exception {
addOptions("--features=cc_include_scanning");
write("hello/BUILD", "cc_library(name = 'hello', srcs = ['hello.cc'], hdrs_check = 'loose')");
write("hello/hello.cc", "#include \"hello/subdir/undeclared.h\"");
write("hello/subdir/undeclared.h");
addOptions("--discard_analysis_cache"); // Fall back to action cache on next build.
buildTarget("//hello:hello");
Path buildFile = write("hello/subdir/BUILD");
customFileSystem.errorInsideStat(buildFile, 0);
BuildFailedException e =
assertThrows(BuildFailedException.class, () -> buildTarget("//hello:hello"));
MoreAsserts.assertContainsEvent(
events.collector(),
Pattern.compile(
".*Compiling hello/hello.cc failed: Unable to resolve hello/subdir/undeclared.h as an"
+ " artifact: Inconsistent filesystem operations. 'stat' said"
+ " .*/hello/subdir/BUILD is a file but then we later encountered error 'nope for"
+ " .*/hello/subdir/BUILD' which indicates that .*/hello/subdir/BUILD is no longer"
+ " a file.*"));
events.assertContainsError("hello/subdir/BUILD ");
assertThat(e.getDetailedExitCode().getFailureDetail())
.comparingExpectedFieldsOnly()
.isEqualTo(
FailureDetails.FailureDetail.newBuilder()
.setIncludeScanning(
FailureDetails.IncludeScanning.newBuilder()
.setCode(FailureDetails.IncludeScanning.Code.SYSTEM_PACKAGE_LOAD_FAILURE)
.setPackageLoadingCode(
FailureDetails.PackageLoading.Code
.PERSISTENT_INCONSISTENT_FILESYSTEM_ERROR))
.build());
}
@Test
public void treeArtifactIOExceptionTopLevel() throws Exception {
write(
"foo/tree.bzl",
"def _tree_impl(ctx):",
" tree = ctx.actions.declare_directory('mytree.cc')",
" ctx.actions.run_shell(outputs = [tree], command = 'mkdir -p %s && touch %s/one.cc' %"
+ " (tree.path, tree.path))",
" return [DefaultInfo(files = depset([tree]))]",
"",
"mytree = rule(implementation = _tree_impl)");
write(
"foo/BUILD",
"load('//foo:tree.bzl', 'mytree')",
"mytree(name = 'tree')",
"cc_library(name = 'lib', srcs = [':tree'])");
customFileSystem.errorOnDirectory("mytree");
BuildFailedException e =
assertThrows(BuildFailedException.class, () -> buildTarget("//foo:lib"));
assertThat(e.getDetailedExitCode().getExitCode()).isEqualTo(ExitCode.BUILD_FAILURE);
events.assertContainsError(
"foo/BUILD:3:11: Failed to create output directory for TreeArtifact"
+ " blaze-out/k8-fastbuild/bin/foo/_pic_objs/lib/mytree: nope");
}
@Test
public void treeArtifactIOExceptionMidLevel() throws Exception {
write(
"foo/tree.bzl",
"def _tree_impl(ctx):",
" tree = ctx.actions.declare_directory('mytree.cc')",
" ctx.actions.run_shell(outputs = [tree], command = 'mkdir -p %s && touch %s/one.cc' %"
+ " (tree.path, tree.path))",
" return [DefaultInfo(files = depset([tree]))]",
"",
"mytree = rule(implementation = _tree_impl)");
write(
"foo/BUILD",
"load('//foo:tree.bzl', 'mytree')",
"mytree(name = 'tree')",
"cc_library(name = 'lib', srcs = [':tree'])",
"genrule(name = 'top', srcs = [':lib'], outs = ['out'], cmd = 'touch $@')");
customFileSystem.errorOnDirectory("mytree");
// Make sure we take default codepath in ActionExecutionFunction.
addOptions("--experimental_nested_set_as_skykey_threshold=1");
BuildFailedException e =
assertThrows(BuildFailedException.class, () -> buildTarget("//foo:top"));
assertThat(e.getDetailedExitCode().getExitCode()).isEqualTo(ExitCode.BUILD_FAILURE);
events.assertContainsError(
"foo/BUILD:3:11: Failed to create output directory for TreeArtifact"
+ " blaze-out/k8-fastbuild/bin/foo/_pic_objs/lib/mytree: nope");
}
@Test
public void filesetIOException() throws Exception {
write("foo/BUILD");
Path filePath = write("foo/subdir/file");
// Violating best practices, this doesn't explicitly list the file underneath //foo that it
// wants, since then foo would have to expose that file as a target, leading to foo/subdir
// being listed (and cached in Skyframe) during the analysis phase, not the execution phase.
write(
"fileset/BUILD",
createFilesetRule("fileset", "fs_out", "FilesetEntry (srcdir = '//foo', destdir = 'x')"));
customFileSystem.alwaysError(filePath.getParentDirectory());
BuildFailedException e =
assertThrows(BuildFailedException.class, () -> buildTarget("//fileset"));
assertThat(e.getDetailedExitCode().getExitCode()).isEqualTo(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
events.assertContainsError(
"Traversing Fileset trees to write manifest fileset/fileset.fileset_manifest failed: Error"
+ " while traversing directory foo/subdir: nope");
}
@Test
public void filesetIOExceptionInBuildFile() throws Exception {
// Violating best practices, this doesn't explicitly list the file underneath //foo that it
// wants, since then foo would have to expose that file as a target, leading to foo/subdir
// being listed (and cached in Skyframe) during the analysis phase, not the execution phase.
Path packageBuildFile =
write(
"fileset/BUILD",
createFilesetRule("fileset", "fs_out", "FilesetEntry (srcdir = 'foo', destdir = 'x')"));
Path packageDirectory = packageBuildFile.getParentDirectory();
Path subdir = packageDirectory.getRelative("foo/bar");
subdir.createDirectoryAndParents();
customFileSystem.alwaysError(subdir.getChild("BUILD"));
BuildFailedException e =
assertThrows(BuildFailedException.class, () -> buildTarget("//fileset"));
assertThat(e.getDetailedExitCode().getExitCode()).isEqualTo(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
events.assertContainsError(
"Traversing Fileset trees to write manifest fileset/fileset.fileset_manifest failed: Error"
+ " while traversing directory fileset/foo/bar: no such package 'fileset/foo/bar': IO"
+ " errors while looking for BUILD file");
}
private void runIoExceptionInTopLevelSource() throws Exception {
write(
"foo/rule.bzl",
"def _impl(ctx):",
" return [DefaultInfo(files = depset([], transitive = [dep[DefaultInfo].files for dep in"
+ " ctx.attr.srcs]))]",
"",
"top_source = rule(",
" implementation = _impl,",
" attrs = {'srcs': attr.label_list(allow_files = True)}",
")");
Path buildFile =
write(
"foo/BUILD",
"load(':rule.bzl', 'top_source')",
"top_source(name = 'foo', srcs = ['error.in', 'missing.in'])");
customFileSystem.alwaysError(buildFile.getParentDirectory().getChild("error.in"));
assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo"));
}
@Test
public void ioExceptionInTopLevelSource_keepGoing() throws Exception {
addOptions("--keep_going");
runIoExceptionInTopLevelSource();
events.assertContainsError(
"foo/BUILD:2:11: //foo:foo: error reading file '//foo:error.in': nope");
events.assertContainsError("foo/BUILD:2:11: //foo:foo: missing input file '//foo:missing.in'");
events.assertContainsError("2 input file(s) are in error or do not exist");
}
@Test
public void ioExceptionInTopLevelSource_noKeepGoing() throws Exception {
runIoExceptionInTopLevelSource();
MoreAsserts.assertContainsEvent(
events.collector(),
Pattern.compile(
".*foo/BUILD:2:11: //foo:foo: (error reading file '//foo:error.in': nope|missing input"
+ " file '//foo:missing.in')"));
MoreAsserts.assertContainsEvent(
events.collector(),
Pattern.compile(
".*(1 input file\\(s\\) (are in error|do not exist)|2 input file\\(s\\) are in error"
+ " or do not exist)"));
}
private void runMissingFileAndIoException() throws Exception {
Path buildFile =
write(
"foo/BUILD",
"genrule(name = 'foo', srcs = ['error.in', 'missing.in'], outs = ['out'], cmd = 'touch"
+ " $@')");
customFileSystem.alwaysError(buildFile.getParentDirectory().getChild("error.in"));
assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo"));
}
@Test
public void missingFileAndIoException_keepGoing() throws Exception {
addOptions("--keep_going");
runMissingFileAndIoException();
events.assertContainsError(
"foo/BUILD:1:8: Executing genrule //foo:foo failed: error reading file '//foo:error.in':"
+ " nope");
events.assertContainsError(
"foo/BUILD:1:8: Executing genrule //foo:foo failed: missing input file '//foo:missing.in'");
events.assertContainsError(
"Executing genrule //foo:foo failed: 2 input file(s) are in error or do not exist");
}
@Test
public void missingFileAndIoException_noKeepGoing() throws Exception {
runMissingFileAndIoException();
MoreAsserts.assertContainsEvent(
events.collector(),
Pattern.compile(
".*foo/BUILD:1:8: Executing genrule //foo:foo failed: (error reading file"
+ " '//foo:error.in': nope|missing input file '//foo:missing.in')"));
MoreAsserts.assertContainsEvent(
events.collector(),
Pattern.compile(
".*(1 input file\\(s\\) (are in error|do not exist)|2 input file\\(s\\) are in error"
+ " or do not exist)"));
}
private static class CustomRealFilesystem extends UnixFileSystem {
private final Map<PathFragment, Integer> badPaths = new HashMap<>();
private final Map<PathFragment, Integer> statBadPaths = new HashMap<>();
private final Set<String> createDirectoryErrorNames = new HashSet<>();
private CustomRealFilesystem(DigestHashFunction digestHashFunction) {
super(digestHashFunction, /*hashAttributeName=*/ "");
}
void alwaysError(Path path) {
alwaysErrorAfter(path, 0);
}
void alwaysErrorAfter(Path path, int numCalls) {
badPaths.put(path.asFragment(), numCalls);
}
void errorOnDirectory(String baseName) {
createDirectoryErrorNames.add(baseName);
}
void errorInsideStat(Path path, int numCalls) {
statBadPaths.put(path.asFragment(), numCalls);
}
int getNumCallsUntilError(Path path) {
return badPaths.getOrDefault(path.asFragment(), 0);
}
private static boolean shouldThrowExn(PathFragment path, Map<PathFragment, Integer> paths) {
if (paths.containsKey(path)) {
Integer numCallsRemaining = paths.get(path);
if (numCallsRemaining <= 0) {
return true;
} else {
paths.put(path, numCallsRemaining - 1);
}
}
return false;
}
private synchronized void maybeThrowExn(PathFragment path) throws IOException {
if (shouldThrowExn(path, badPaths)) {
throw new IOException("nope");
}
}
@Override
protected FileStatus statNullable(PathFragment path, boolean followSymlinks) {
try {
maybeThrowExn(path);
} catch (IOException e) {
return null;
}
return super.statNullable(path, followSymlinks);
}
@Override
protected FileStatus statIfFound(PathFragment path, boolean followSymlinks) throws IOException {
maybeThrowExn(path);
FileStatus fileStatus = super.statIfFound(path, followSymlinks);
return shouldThrowExn(path, statBadPaths) ? new ThrowingFileStatus(path) : fileStatus;
}
@Override
protected UnixFileStatus statInternal(PathFragment path, boolean followSymlinks)
throws IOException {
maybeThrowExn(path);
return super.statInternal(path, followSymlinks);
}
@Override
protected Collection<Dirent> readdir(PathFragment path, boolean followSymlinks)
throws IOException {
maybeThrowExn(path);
return super.readdir(path, followSymlinks);
}
@Override
public void createDirectoryAndParents(PathFragment path) throws IOException {
if (createDirectoryErrorNames.contains(path.getBaseName())) {
throw new IOException("nope");
}
super.createDirectoryAndParents(path);
}
@Override
protected PathFragment readSymbolicLinkUnchecked(PathFragment path) throws IOException {
maybeThrowExn(path);
return super.readSymbolicLinkUnchecked(path);
}
@Override
protected InputStream createFileInputStream(PathFragment path) throws IOException {
maybeThrowExn(path);
return super.createFileInputStream(path);
}
private static class ThrowingFileStatus implements FileStatus {
private final PathFragment path;
ThrowingFileStatus(PathFragment path) {
this.path = path;
}
@Override
public boolean isFile() {
return true;
}
@Override
public boolean isDirectory() {
return false;
}
@Override
public boolean isSymbolicLink() {
return false;
}
@Override
public boolean isSpecialFile() {
return false;
}
@Override
public long getSize() throws IOException {
throw new IOException("nope for " + path);
}
@Override
public long getLastModifiedTime() throws IOException {
throw new IOException("nope for " + path);
}
@Override
public long getLastChangeTime() throws IOException {
throw new IOException("nope for " + path);
}
@Override
public long getNodeId() throws IOException {
throw new IOException("nope for " + path);
}
}
}
}