blob: df99feada2f5f9561fcaf4587564826d75864238 [file] [log] [blame]
// Copyright 2015 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.skyframe;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Streams.stream;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.devtools.build.skyframe.EvaluationResultSubjectFactory.assertThatEvaluationResult;
import static java.util.Arrays.stream;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.actions.FileStateValue;
import com.google.devtools.build.lib.actions.FileValue;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
import com.google.devtools.build.lib.packages.Globber.Operation;
import com.google.devtools.build.lib.packages.InputFile;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.NoSuchTargetException;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.PackageLoadingListener.Metrics;
import com.google.devtools.build.lib.packages.PackageOverheadEstimator;
import com.google.devtools.build.lib.packages.PackagePiece;
import com.google.devtools.build.lib.packages.PackagePieceIdentifier;
import com.google.devtools.build.lib.packages.PackageValidator;
import com.google.devtools.build.lib.packages.PackageValidator.InvalidPackageException;
import com.google.devtools.build.lib.packages.Packageoid;
import com.google.devtools.build.lib.packages.RuleVisibility;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.pkgcache.PackageOptions;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
import com.google.devtools.build.lib.runtime.QuiescingExecutorsImpl;
import com.google.devtools.build.lib.server.FailureDetails.PackageLoading;
import com.google.devtools.build.lib.skyframe.GlobsValue.GlobRequest;
import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils;
import com.google.devtools.build.lib.testutil.ManualClock;
import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.Pair;
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.ModifiedFileSet;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.lib.vfs.Symlinks;
import com.google.devtools.build.lib.vfs.SyscallCache;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import com.google.devtools.build.skyframe.Differencer.DiffWithDelta.Delta;
import com.google.devtools.build.skyframe.EvaluationResult;
import com.google.devtools.build.skyframe.InMemoryGraph;
import com.google.devtools.build.skyframe.InMemoryNodeEntry;
import com.google.devtools.build.skyframe.RecordingDifferencer;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.common.options.Options;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
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.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/**
* Unit tests of specific functionality of PackageFunction. Note that it's already tested indirectly
* in several other places.
*/
@RunWith(TestParameterInjector.class)
public class PackageFunctionTest extends BuildViewTestCase {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@Mock private PackageValidator mockPackageValidator;
@Mock private PackageOverheadEstimator mockPackageOverheadEstimator;
@TestParameter private boolean globUnderSingleDep;
// Set by preparePackageLoading().
private ComputationMode computationMode = ComputationMode.MONOLITHIC_PACKAGE;
private final CustomInMemoryFs fs = new CustomInMemoryFs(new ManualClock());
private static enum ComputationMode {
// Use PackageIdentifier as the key, and retrieve the result as a Package.
MONOLITHIC_PACKAGE,
// Use PackagePieceIdentifier.ForBuildFile as the key, and retrieve the result as a
// PackagePiece.ForBuildFile.
PACKAGE_PIECE_FOR_BUILD_FILE,
// Use PackageIdentifier as the key, expand symbolic macros lazily, and assemble the resulting
// Package from PackagePieces.
PACKAGE_FROM_PACKAGE_PIECES
}
private void preparePackageLoading(ComputationMode computationMode, Path... roots)
throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode, parseBuildLanguageOptions(), roots);
}
private void preparePackageLoadingWithCustomStarklarkSemanticsOptions(
ComputationMode computationMode, BuildLanguageOptions buildLanguageOptions, Path... roots)
throws InterruptedException, AbruptExitException {
this.computationMode = computationMode;
PackageOptions packageOptions = Options.getDefaults(PackageOptions.class);
ImmutableList<String> packagePath =
stream(roots).map(Path::getPathString).collect(toImmutableList());
if (!packagePath.isEmpty()) {
packageOptions.packagePath = packagePath;
}
packageOptions.defaultVisibility = RuleVisibility.PUBLIC;
packageOptions.showLoadingProgress = true;
packageOptions.globbingThreads = 7;
if (computationMode.equals(ComputationMode.PACKAGE_FROM_PACKAGE_PIECES)) {
packageOptions.lazyMacroExpansionPackages = PackageOptions.LazyMacroExpansionPackages.ALL;
}
setPackageAndBuildLanguageOptions(packageOptions, buildLanguageOptions);
}
@Override
protected FileSystem createFileSystem() {
return fs;
}
@Override
protected PackageValidator getPackageValidator() {
return mockPackageValidator;
}
@Override
protected PackageOverheadEstimator getPackageOverheadEstimator() {
return mockPackageOverheadEstimator;
}
@CanIgnoreReturnValue
private Packageoid validPackageoidWithoutErrors(String pkg) throws InterruptedException {
return validPackageoidInternal(pkg, /* checkError= */ true);
}
@CanIgnoreReturnValue
private Packageoid validPackageoid(String pkg) throws InterruptedException {
return validPackageoidInternal(pkg, /* checkError= */ false);
}
private Packageoid validPackageoidInternal(String pkg, boolean checkError)
throws InterruptedException {
SkyKey skyKey = getSkyKey(pkg);
SkyframeExecutor skyframeExecutor = getSkyframeExecutor();
EvaluationResult<PackageoidValue> result =
SkyframeExecutorTestUtils.evaluate(
skyframeExecutor, skyKey, /* keepGoing= */ false, reporter);
if (result.hasError()) {
fail(result.getError(skyKey).getException().getMessage());
}
Packageoid value = result.get(skyKey).getPackageoid();
if (skyKey instanceof PackageIdentifier) {
assertThat(value).isInstanceOf(Package.class);
InputFile buildFile = ((Package) value).getBuildFile();
assertThat(buildFile).isNotNull();
if (computationMode.equals(ComputationMode.PACKAGE_FROM_PACKAGE_PIECES)) {
// Targets are owned by package pieces, not by the package-from-pieces.
assertThat(buildFile.getPackageoid()).isInstanceOf(PackagePiece.ForBuildFile.class);
for (Target target : value.getTargets().values()) {
assertWithMessage("Packageoid of target %s", target.getLabel())
.that(target.getPackageoid())
.isNotSameInstanceAs(value);
assertWithMessage("Packageoid of target %s", target.getLabel())
.that(target.getPackageoid())
.isInstanceOf(PackagePiece.class);
}
}
} else {
assertThat(value).isInstanceOf(PackagePiece.ForBuildFile.class);
InputFile buildFile = ((PackagePiece.ForBuildFile) value).getBuildFile();
assertThat(buildFile).isNotNull();
}
if (checkError) {
assertThat(value.containsErrors()).isFalse();
}
return value;
}
private SkyKey getSkyKey(String pkg) {
PackageIdentifier pkgId = PackageIdentifier.createInMainRepo(pkg);
return computationMode.equals(ComputationMode.PACKAGE_PIECE_FOR_BUILD_FILE)
? new PackagePieceIdentifier.ForBuildFile(pkgId)
: pkgId;
}
@CanIgnoreReturnValue
private Exception evaluatePackageoidToException(String pkg) throws Exception {
return evaluatePackageoidToException(pkg, /* keepGoing= */ false);
}
/**
* Helper that evaluates the given package or package piece and returns the expected exception.
*
* <p>Disables the failFastHandler as a side-effect.
*/
@CanIgnoreReturnValue
private Exception evaluatePackageoidToException(String pkg, boolean keepGoing) throws Exception {
reporter.removeHandler(failFastHandler);
SkyKey skyKey = getSkyKey(pkg);
EvaluationResult<PackageoidValue> result =
SkyframeExecutorTestUtils.evaluate(getSkyframeExecutor(), skyKey, keepGoing, reporter);
assertThat(result.hasError()).isTrue();
return result.getError(skyKey).getException();
}
@Before
@Override
public final void initializeSkyframeExecutor() throws Exception {
when(mockPackageValidator.getPackageLimits())
.thenReturn(Package.Builder.PackageLimits.DEFAULTS);
initializeSkyframeExecutor(
/* doPackageLoadingChecks= */ true,
/* diffAwarenessFactories= */ ImmutableList.of(),
/* globUnderSingleDep= */ globUnderSingleDep);
}
@Test
public void testValidPackage(@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("pkg/BUILD", "cc_library(name = 'foo')");
preparePackageLoading(computationMode);
Packageoid pkg = validPackageoidWithoutErrors("pkg");
assertThat(pkg.getTargets()).containsKey("foo");
}
@Test
public void symbolicMacroExpansion_onlyInFullPackages(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file(
"pkg/macro.bzl",
"""
def legacy(name, visibility = None, **kwargs):
native.cc_library(name = name, visibility = visibility, **kwargs)
symbolic = macro(
implementation = legacy,
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":macro.bzl", "legacy", "symbolic")
legacy(name = "target_in_legacy_macro")
symbolic(name = "target_in_symbolic_macro")
""");
preparePackageLoading(computationMode);
Packageoid pkg = validPackageoidWithoutErrors("pkg");
assertThat(pkg.getTargets()).containsKey("target_in_legacy_macro");
if (computationMode.equals(ComputationMode.PACKAGE_PIECE_FOR_BUILD_FILE)) {
assertThat(pkg.getTargets()).doesNotContainKey("target_in_symbolic_macro");
} else {
assertThat(pkg.getTargets()).containsKey("target_in_symbolic_macro");
}
}
@Test
// TODO(https://github.com/bazelbuild/bazel/issues/23852): enable PACKAGE_PIECE_FOR_BUILD_FILE
// once package piece validation is supported.
public void testInvalidPackage(
@TestParameter({"MONOLITHIC_PACKAGE", "PACKAGE_FROM_PACKAGE_PIECES"})
ComputationMode computationMode)
throws Exception {
scratch.file("pkg/BUILD", "filegroup(name='foo', srcs=['foo.sh'])");
scratch.file("pkg/foo.sh");
preparePackageLoading(computationMode);
doAnswer(
inv -> {
Package pkg = inv.getArgument(0, Package.class);
if (pkg.getName().equals("pkg")) {
inv.getArgument(2, ExtendedEventHandler.class).handle(Event.warn("warning event"));
throw new InvalidPackageException(pkg.getPackageIdentifier(), "no good");
}
return null;
})
.when(mockPackageValidator)
.validate(any(Package.class), any(Metrics.class), any(ExtendedEventHandler.class));
invalidatePackages();
Exception ex = evaluatePackageoidToException("pkg");
assertThat(ex).isInstanceOf(InvalidPackageException.class);
assertThat(ex).hasMessageThat().contains("no such package 'pkg': no good");
assertContainsEvent("warning event");
}
@Test
public void package_withBadMacroImplementation(
@TestParameter({"MONOLITHIC_PACKAGE", "PACKAGE_FROM_PACKAGE_PIECES"})
ComputationMode computationMode)
throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name, visibility = visibility)
fail("fail fail fail")
my_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "foo")
""");
preparePackageLoading(computationMode);
reporter.removeHandler(failFastHandler);
Package pkg = (Package) validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(
"""
Traceback (most recent call last):
\tFile "/workspace/pkg/BUILD", line 2, column 9, in <toplevel>
\t\tmy_macro(name = "foo")
\tFile "/workspace/pkg/my_macro.bzl", line 4, column 1, in my_macro
\t\tmy_macro = macro(implementation = _impl)
\tFile "/workspace/pkg/my_macro.bzl", line 3, column 9, in _impl
\t\tfail("fail fail fail")
Error in fail: fail fail fail\
""");
if (computationMode.equals(ComputationMode.MONOLITHIC_PACKAGE)) {
assertThat(eventCollector.filtered(EventKind.ERROR)).hasSize(1);
} else {
assertContainsEvent(
"ERROR /workspace/pkg/BUILD: cannot compute package //pkg: error in package piece for"
+ " macro //pkg:foo defined by //pkg:my_macro.bzl%my_macro");
assertThat(eventCollector.filtered(EventKind.ERROR)).hasSize(2);
}
}
@Test
public void package_withMacroNameConflict(
@TestParameter boolean inFinalizer,
@TestParameter({"MONOLITHIC_PACKAGE", "PACKAGE_FROM_PACKAGE_PIECES"})
ComputationMode computationMode)
throws Exception {
scratch.file(
"pkg/my_macro.bzl",
String.format(
"""
def _impl(name, visibility):
native.cc_library(name = name + "_bar")
my_macro = macro(implementation = _impl, finalizer = %s)
""",
inFinalizer ? "True" : "False"));
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
genrule(name = "foo_bar", outs = ["unused"], cmd = "touch $@")
my_macro(name = "foo") # will try to create another target named "foo_bar"
""");
preparePackageLoading(computationMode);
reporter.removeHandler(failFastHandler);
Package pkg = (Package) validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(
"cc_library rule 'foo_bar' conflicts with existing genrule rule, defined at"
+ " /workspace/pkg/BUILD:2:8");
}
@Test
// TODO(https://github.com/bazelbuild/bazel/issues/23852): enable PACKAGE_PIECE_FOR_BUILD_FILE
// once package piece validation is supported.
public void testPackageOverheadPassedToValidationLogic(
@TestParameter({"MONOLITHIC_PACKAGE", "PACKAGE_FROM_PACKAGE_PIECES"})
ComputationMode computationMode)
throws Exception {
scratch.file("pkg/BUILD", "# Contents doesn't matter, it's all fake");
preparePackageLoading(computationMode);
when(mockPackageOverheadEstimator.estimatePackageOverhead(any(Package.class)))
.thenReturn(OptionalLong.of(42));
ArgumentCaptor<Package> packageCaptor = ArgumentCaptor.forClass(Package.class);
invalidatePackages(true);
reset(mockPackageValidator);
when(mockPackageValidator.getPackageLimits())
.thenReturn(Package.Builder.PackageLimits.DEFAULTS);
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), getSkyKey("pkg"), /* keepGoing= */ false, reporter);
verify(mockPackageValidator)
.validate(packageCaptor.capture(), any(Metrics.class), any(ExtendedEventHandler.class));
List<Package> packages = packageCaptor.getAllValues();
assertThat(packages.get(0).getPackageOverhead()).isEqualTo(OptionalLong.of(42));
}
@Test
// TODO(https://github.com/bazelbuild/bazel/issues/23852): enable PACKAGE_PIECE_FOR_BUILD_FILE
// once package piece validation is supported.
public void testSkyframeExecutorClearedPackagesResultsInReload(
@TestParameter({"MONOLITHIC_PACKAGE", "PACKAGE_FROM_PACKAGE_PIECES"})
ComputationMode computationMode)
throws Exception {
scratch.file("pkg/BUILD", "filegroup(name='foo', srcs=['foo.sh'])");
scratch.file("pkg/foo.sh");
preparePackageLoading(computationMode);
invalidatePackages();
// Use number of times the package was validated as a proxy for number of times it was loaded.
AtomicInteger validationCount = new AtomicInteger();
doAnswer(
inv -> {
if (inv.getArgument(0, Package.class).getName().equals("pkg")) {
validationCount.incrementAndGet();
}
return null;
})
.when(mockPackageValidator)
.validate(any(Package.class), any(Metrics.class), any(ExtendedEventHandler.class));
SkyKey skyKey = getSkyKey("pkg");
EvaluationResult<PackageoidValue> result1 =
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), skyKey, /* keepGoing= */ false, reporter);
assertThatEvaluationResult(result1).hasNoError();
skyframeExecutor.clearLoadedPackages();
EvaluationResult<PackageoidValue> result2 =
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), skyKey, /* keepGoing= */ false, reporter);
assertThatEvaluationResult(result2).hasNoError();
assertThat(validationCount.get()).isEqualTo(2);
}
@Test
public void testPropagatesFilesystemInconsistencies(
@TestParameter ComputationMode computationMode) throws Exception {
RecordingDifferencer differencer = getSkyframeExecutor().getDifferencerForTesting();
Root pkgRoot = getSkyframeExecutor().getPackagePathEntries().getFirst();
Path fooBuildFile = scratch.file("foo/BUILD");
Path fooDir = fooBuildFile.getParentDirectory();
preparePackageLoading(computationMode);
// Our custom filesystem says that fooDir is neither a file nor directory nor symlink
FileStatus inconsistentFileStatus =
new FileStatus() {
@Override
public boolean isFile() {
return false;
}
@Override
public boolean isDirectory() {
return false;
}
@Override
public boolean isSymbolicLink() {
return false;
}
@Override
public boolean isSpecialFile() {
return false;
}
@Override
public long getSize() {
return 0;
}
@Override
public long getLastModifiedTime() {
return 0;
}
@Override
public long getLastChangeTime() {
return 0;
}
@Override
public long getNodeId() {
return 0;
}
};
fs.stubStat(fooBuildFile, inconsistentFileStatus);
RootedPath pkgRootedPath = RootedPath.toRootedPath(pkgRoot, fooDir);
SkyValue fooDirValue = FileStateValue.create(pkgRootedPath, SyscallCache.NO_CACHE, tsgm);
differencer.inject(
ImmutableMap.of(FileStateValue.key(pkgRootedPath), Delta.justNew(fooDirValue)));
Exception ex = evaluatePackageoidToException("foo");
String msg = ex.getMessage();
assertThat(msg).contains("Inconsistent filesystem operations");
assertThat(msg)
.contains(
"according to stat, existing path /workspace/foo/BUILD is neither"
+ " a file nor directory nor symlink.");
assertDetailedExitCode(
ex,
PackageLoading.Code.PERSISTENT_INCONSISTENT_FILESYSTEM_ERROR,
ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
}
@Test
public void testPropagatesFilesystemInconsistencies_globbing(
@TestParameter ComputationMode computationMode) throws Exception {
RecordingDifferencer differencer = getSkyframeExecutor().getDifferencerForTesting();
Root pkgRoot = getSkyframeExecutor().getPackagePathEntries().getFirst();
scratch.file(
"foo/BUILD",
"""
filegroup(
name = "foo",
srcs = glob(["bar/**/baz.sh"]),
)
x = 1 // 0
""" // causes 'foo' to be marked in error
);
Path bazFile = scratch.file("foo/bar/baz/baz.sh");
Path bazDir = bazFile.getParentDirectory();
Path barDir = bazDir.getParentDirectory();
preparePackageLoading(computationMode);
// Our custom filesystem says "foo/bar/baz" does not exist but it also says that "foo/bar"
// has a child directory "baz".
fs.stubStat(bazDir, null);
RootedPath barDirRootedPath = RootedPath.toRootedPath(pkgRoot, barDir);
differencer.inject(
ImmutableMap.of(
DirectoryListingStateValue.key(barDirRootedPath),
Delta.justNew(
DirectoryListingStateValue.create(
ImmutableList.of(new Dirent("baz", Dirent.Type.DIRECTORY))))));
Exception ex = evaluatePackageoidToException("foo");
String msg = ex.getMessage();
assertThat(msg).contains("Inconsistent filesystem operations");
assertThat(msg).contains("/workspace/foo/bar/baz is no longer an existing directory");
assertDetailedExitCode(
ex,
PackageLoading.Code.PERSISTENT_INCONSISTENT_FILESYSTEM_ERROR,
ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
}
/** Regression test for unexpected exception type from PackageValue. */
@Test
public void testDiscrepancyBetweenGlobbingErrors(@TestParameter ComputationMode computationMode)
throws Exception {
preparePackageLoading(computationMode);
Path fooBuildFile =
scratch.file("foo/BUILD", "filegroup(name = 'foo', srcs = glob(['bar/*.sh']))");
Path fooDir = fooBuildFile.getParentDirectory();
Path barDir = fooDir.getRelative("bar");
scratch.file("foo/bar/baz.sh");
fs.scheduleMakeUnreadableAfterReaddir(barDir);
preparePackageLoading(computationMode);
Exception ex =
evaluatePackageoidToException(
"foo",
// Use --keep_going, not --nokeep_going, semantics so as to exercise the situation we
// want to exercise.
//
// In --nokeep_going semantics, the GlobValue node's error would halt normal evaluation
// and trigger error bubbling. Then, during error bubbling we would freshly compute the
// PackageValue node again, meaning we would do non-Skyframe globbing except this time
// non-Skyframe globbing would encounter the io error, meaning there actually wouldn't
// be a discrepancy.
/* keepGoing= */ true);
String msg = ex.getMessage();
assertThat(msg).contains("Inconsistent filesystem operations");
assertThat(msg).contains("Encountered error '/workspace/foo/bar (Permission denied)'");
assertDetailedExitCode(
ex,
PackageLoading.Code.TRANSIENT_INCONSISTENT_FILESYSTEM_ERROR,
ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
}
@SuppressWarnings("unchecked") // Cast of srcs attribute to Iterable<Label>.
@Test
public void testGlobOrderStable(@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("foo/BUILD", "filegroup(name = 'foo', srcs = glob(['**/*.txt']))");
scratch.file("foo/b.txt");
scratch.file("foo/c/c.txt");
preparePackageLoading(computationMode, rootDirectory);
Packageoid pkg = validPackageoidWithoutErrors("foo");
assertThat((Iterable<Label>) pkg.getTarget("foo").getAssociatedRule().getAttr("srcs"))
.containsExactly(
Label.parseCanonicalUnchecked("//foo:b.txt"),
Label.parseCanonicalUnchecked("//foo:c/c.txt"))
.inOrder();
scratch.file("foo/d.txt");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/d.txt")).build(),
Root.fromPath(rootDirectory));
pkg = validPackageoidWithoutErrors("foo");
assertThat((Iterable<Label>) pkg.getTarget("foo").getAssociatedRule().getAttr("srcs"))
.containsExactly(
Label.parseCanonicalUnchecked("//foo:b.txt"),
Label.parseCanonicalUnchecked("//foo:c/c.txt"),
Label.parseCanonicalUnchecked("//foo:d.txt"))
.inOrder();
}
@Test
public void testGlobOrderStableWithNonSkyframeAndSkyframeComponents(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("foo/BUILD", "filegroup(name = 'foo', srcs = glob(['*.txt']))");
scratch.file("foo/b.txt");
scratch.file("foo/a.config");
preparePackageLoading(computationMode, rootDirectory);
assertSrcs(validPackageoidWithoutErrors("foo"), "foo", "//foo:b.txt");
scratch.overwriteFile(
"foo/BUILD", "filegroup(name = 'foo', srcs = glob(['*.txt', '*.config']))");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(),
Root.fromPath(rootDirectory));
assertSrcs(validPackageoidWithoutErrors("foo"), "foo", "//foo:a.config", "//foo:b.txt");
scratch.overwriteFile(
"foo/BUILD", "filegroup(name = 'foo', srcs = glob(['*.txt', '*.config'])) # comment");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(),
Root.fromPath(rootDirectory));
assertSrcs(validPackageoidWithoutErrors("foo"), "foo", "//foo:a.config", "//foo:b.txt");
getSkyframeExecutor().resetEvaluator();
PackageOptions packageOptions = Options.getDefaults(PackageOptions.class);
packageOptions.defaultVisibility = RuleVisibility.PUBLIC;
packageOptions.showLoadingProgress = true;
packageOptions.globbingThreads = 7;
if (computationMode.equals(ComputationMode.PACKAGE_FROM_PACKAGE_PIECES)) {
packageOptions.lazyMacroExpansionPackages = PackageOptions.LazyMacroExpansionPackages.ALL;
}
getSkyframeExecutor()
.preparePackageLoading(
new PathPackageLocator(
outputBase,
ImmutableList.of(Root.fromPath(rootDirectory)),
BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY),
packageOptions,
parseBuildLanguageOptions(),
UUID.randomUUID(),
ImmutableMap.of(),
QuiescingExecutorsImpl.forTesting(),
tsgm);
getSkyframeExecutor().injectExtraPrecomputedValues(analysisMock.getPrecomputedValues());
getSkyframeExecutor().setActionEnv(ImmutableMap.of());
assertSrcs(validPackageoidWithoutErrors("foo"), "foo", "//foo:a.config", "//foo:b.txt");
}
@Test
public void globEscapesAt(@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("foo/BUILD", "filegroup(name = 'foo', srcs = glob(['*.txt']))");
scratch.file("foo/@f.txt");
preparePackageLoading(computationMode, rootDirectory);
assertSrcs(validPackageoidWithoutErrors("foo"), "foo", "//foo:@f.txt");
scratch.overwriteFile("foo/BUILD", "filegroup(name = 'foo', srcs = glob(['*.txt'])) # comment");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(),
Root.fromPath(rootDirectory));
assertSrcs(validPackageoidWithoutErrors("foo"), "foo", "//foo:@f.txt");
}
/**
* Tests that a symlink to a file outside of the package root is handled consistently. If the
* default behavior of Bazel was changed from {@code
* ExternalFileAction#DEPEND_ON_EXTERNAL_PKG_FOR_EXTERNAL_REPO_PATHS} to {@code
* ExternalFileAction#ASSUME_NON_EXISTENT_AND_IMMUTABLE_FOR_EXTERNAL_PATHS} then foo/link.sh
* should no longer appear in the srcs of //foo:foo. However, either way the srcs should be the
* same independent of the evaluation being incremental or clean.
*/
@Test
public void testGlobWithExternalSymlink(@TestParameter ComputationMode computationMode)
throws Exception {
scratch.file(
"foo/BUILD",
"""
filegroup(
name = "foo",
srcs = glob(["*.sh"]),
)
filegroup(
name = "bar",
srcs = glob(["link.sh"]),
)
filegroup(
name = "baz",
srcs = glob(["subdir_link/*.txt"]),
)
""");
scratch.file("foo/ordinary.sh");
Path externalTarget = scratch.file("../ops/target.txt");
FileSystemUtils.ensureSymbolicLink(scratch.resolve("foo/link.sh"), externalTarget);
FileSystemUtils.ensureSymbolicLink(
scratch.resolve("foo/subdir_link"), externalTarget.getParentDirectory());
preparePackageLoading(computationMode, rootDirectory);
Packageoid fooPkg = validPackageoidWithoutErrors("foo");
assertSrcs(fooPkg, "foo", "//foo:link.sh", "//foo:ordinary.sh");
assertSrcs(fooPkg, "bar", "//foo:link.sh");
assertSrcs(fooPkg, "baz", "//foo:subdir_link/target.txt");
scratch.overwriteFile(
"foo/BUILD",
"""
filegroup(
name = "foo",
srcs = glob(["*.sh"]),
) #comment
filegroup(
name = "bar",
srcs = glob(["link.sh"]),
)
filegroup(
name = "baz",
srcs = glob(["subdir_link/*.txt"]),
)
""");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(),
Root.fromPath(rootDirectory));
Packageoid fooPkg2 = validPackageoidWithoutErrors("foo");
assertThat(fooPkg2).isNotEqualTo(fooPkg);
assertSrcs(fooPkg2, "foo", "//foo:link.sh", "//foo:ordinary.sh");
assertSrcs(fooPkg2, "bar", "//foo:link.sh");
assertSrcs(fooPkg2, "baz", "//foo:subdir_link/target.txt");
}
private static void assertSrcs(Packageoid pkg, String targetName, String... expected)
throws NoSuchTargetException {
List<Label> expectedLabels = new ArrayList<>();
for (String item : expected) {
expectedLabels.add(Label.parseCanonicalUnchecked(item));
}
assertThat(getSrcs(pkg, targetName)).containsExactlyElementsIn(expectedLabels).inOrder();
}
@SuppressWarnings("unchecked")
private static Iterable<Label> getSrcs(Packageoid pkg, String targetName)
throws NoSuchTargetException {
return (Iterable<Label>) pkg.getTarget(targetName).getAssociatedRule().getAttr("srcs");
}
@Test
public void testOneNewElementInMultipleGlob(@TestParameter ComputationMode computationMode)
throws Exception {
scratch.file(
"foo/BUILD",
"""
filegroup(
name = "foo",
srcs = glob(
["*.sh"],
allow_empty = True,
),
)
filegroup(
name = "bar",
srcs = glob(
[
"*.sh",
"*.txt",
],
allow_empty = True,
),
)
""");
preparePackageLoading(computationMode, rootDirectory);
Packageoid pkg = validPackageoidWithoutErrors("foo");
scratch.file("foo/irrelevant");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/irrelevant")).build(),
Root.fromPath(rootDirectory));
assertThat(validPackageoidWithoutErrors("foo")).isSameInstanceAs(pkg);
}
@Test
public void testNoNewElementInMultipleGlob(@TestParameter ComputationMode computationMode)
throws Exception {
scratch.file(
"foo/BUILD",
"""
filegroup(
name = "foo",
srcs = glob(
[
"*.sh",
"*.txt",
],
allow_empty = True,
),
)
filegroup(
name = "bar",
srcs = glob(
[
"*.sh",
"*.txt",
],
allow_empty = True,
),
)
""");
preparePackageLoading(computationMode, rootDirectory);
Packageoid pkg = validPackageoidWithoutErrors("foo");
scratch.file("foo/irrelevant");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/irrelevant")).build(),
Root.fromPath(rootDirectory));
assertThat(validPackageoidWithoutErrors("foo")).isSameInstanceAs(pkg);
}
@Test
public void testTransitiveStarlarkDepsStoredInPackage(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("foo/BUILD", "load('//bar:ext.bzl', 'a')");
scratch.file("bar/BUILD");
scratch.file(
"bar/ext.bzl",
"""
load("//baz:ext.scl", "b")
a = b
""");
scratch.file("baz/BUILD");
scratch.file("baz/ext.scl", "b = 1");
scratch.file("qux/BUILD");
scratch.file("qux/ext.bzl", "c = 1");
preparePackageLoading(computationMode, rootDirectory);
// must be done after preparePackageLoading()
setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
Packageoid pkg = validPackageoidWithoutErrors("foo");
assertThat(pkg.getDeclarations().getOrComputeTransitivelyLoadedStarlarkFiles())
.containsExactly(
Label.parseCanonical("//bar:ext.bzl"), Label.parseCanonical("//baz:ext.scl"));
scratch.overwriteFile(
"bar/ext.bzl",
"""
load("//qux:ext.bzl", "c")
a = c
""");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("bar/ext.bzl")).build(),
Root.fromPath(rootDirectory));
pkg = validPackageoidWithoutErrors("foo");
assertThat(pkg.getDeclarations().getOrComputeTransitivelyLoadedStarlarkFiles())
.containsExactly(
Label.parseCanonical("//bar:ext.bzl"), Label.parseCanonical("//qux:ext.bzl"));
}
@Test
public void testNonExistingStarlarkExtension(@TestParameter ComputationMode computationMode)
throws Exception {
scratch.file("test/starlark/BUILD", "load('//test/starlark:bad_extension.bzl', 'some_symbol')");
preparePackageLoading(computationMode);
invalidatePackages();
Exception ex = evaluatePackageoidToException("test/starlark");
assertThat(ex)
.hasMessageThat()
.isEqualTo(
"error loading package 'test/starlark': "
+ "cannot load '//test/starlark:bad_extension.bzl': no such file");
assertDetailedExitCode(
ex, PackageLoading.Code.IMPORT_STARLARK_FILE_ERROR, ExitCode.BUILD_FAILURE);
}
@Test
public void testNonExistingStarlarkExtensionFromExtension(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file(
"test/starlark/extension.bzl",
"""
load("//test/starlark:bad_extension.bzl", "some_symbol")
a = "a"
""");
scratch.file("test/starlark/BUILD", "load('//test/starlark:extension.bzl', 'a')");
preparePackageLoading(computationMode);
invalidatePackages();
Exception ex = evaluatePackageoidToException("test/starlark");
assertThat(ex)
.hasMessageThat()
.isEqualTo(
"error loading package 'test/starlark': "
+ "at /workspace/test/starlark/extension.bzl:1:6: "
+ "cannot load '//test/starlark:bad_extension.bzl': no such file");
assertDetailedExitCode(
ex, PackageLoading.Code.IMPORT_STARLARK_FILE_ERROR, ExitCode.BUILD_FAILURE);
}
@Test
public void testBuiltinsInjectionFailure(@TestParameter ComputationMode computationMode)
throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode,
parseBuildLanguageOptions("--experimental_builtins_bzl_path=tools/builtins_staging"));
scratch.file(
"tools/builtins_staging/exports.bzl",
"""
1 // 0 # <-- dynamic error
exported_toplevels = {}
exported_rules = {}
exported_to_java = {}
""");
scratch.file("pkg/BUILD");
Exception ex = evaluatePackageoidToException("pkg");
assertThat(ex)
.hasMessageThat()
.isEqualTo(
"error loading package 'pkg': Internal error while loading Starlark builtins: Failed"
+ " to load builtins sources: initialization of module 'exports.bzl' (internal)"
+ " failed");
assertDetailedExitCode(
ex, PackageLoading.Code.BUILTINS_INJECTION_FAILURE, ExitCode.BUILD_FAILURE);
}
@Test
public void testSymlinkCycleWithStarlarkExtension(@TestParameter ComputationMode computationMode)
throws Exception {
Path extensionFilePath = scratch.resolve("/workspace/test/starlark/extension.bzl");
FileSystemUtils.ensureSymbolicLink(extensionFilePath, PathFragment.create("extension.bzl"));
scratch.file("test/starlark/BUILD", "load('//test/starlark:extension.bzl', 'a')");
preparePackageLoading(computationMode);
invalidatePackages();
Exception ex = evaluatePackageoidToException("test/starlark");
assertThat(ex)
.hasMessageThat()
.isEqualTo(
"error loading package 'test/starlark': Encountered error while reading extension "
+ "file 'test/starlark/extension.bzl': Symlink cycle");
assertDetailedExitCode(
ex, PackageLoading.Code.IMPORT_STARLARK_FILE_ERROR, ExitCode.BUILD_FAILURE);
}
@Test
public void testIOErrorLookingForSubpackageForLabelIsHandled(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file(
"foo/BUILD", //
"filegroup(name = 'foo', srcs = ['bar/baz.sh'])");
Path barBuildFile = scratch.file("foo/bar/BUILD");
fs.stubStatError(barBuildFile, new IOException("nope"));
preparePackageLoading(computationMode);
evaluatePackageoidToException("foo");
assertContainsEvent("nope");
}
@Test
public void testLoadOK(@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("p/a.bzl", "a = 1; b = 1; d = 1");
scratch.file("p/subdir/a.bzl", "c = 1; e = 1");
scratch.file(
"p/BUILD",
"""
load("//p:a.bzl", "d")
load("//p:subdir/a.bzl", "e")
load(":a.bzl", "a")
load("a.bzl", "b")
load("subdir/a.bzl", "c")
""");
preparePackageLoading(computationMode);
validPackageoidWithoutErrors("p");
}
// See WorkspaceFileFunctionTest for tests that exercise load('@repo...').
@Test
public void testLoadBadLabel(@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("p/BUILD", "load('this\tis not a label', 'a')");
preparePackageLoading(computationMode);
reporter.removeHandler(failFastHandler);
SkyKey key = getSkyKey("p");
SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, /*keepGoing=*/ false, reporter);
assertContainsEvent(
"in load statement: invalid target name 'this<?>is not a label': target names may not"
+ " contain non-printable characters");
}
@Test
public void testLoadFromExternalPackage(@TestParameter ComputationMode computationMode)
throws Exception {
scratch.file("p/BUILD", "load('//external:file.bzl', 'a')");
preparePackageLoading(computationMode);
reporter.removeHandler(failFastHandler);
SkyKey key = getSkyKey("p");
SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, /*keepGoing=*/ false, reporter);
assertContainsEvent("Starlark files may not be loaded from the //external package");
}
@Test
public void testLoadWithoutBzlSuffix(@TestParameter ComputationMode computationMode)
throws Exception {
scratch.file("p/BUILD", "load('//p:file.starlark', 'a')");
preparePackageLoading(computationMode);
reporter.removeHandler(failFastHandler);
SkyKey key = getSkyKey("p");
SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, /*keepGoing=*/ false, reporter);
assertContainsEvent("The label must reference a file with extension \".bzl\"");
}
@Test
public void testBzlVisibilityViolation(@TestParameter ComputationMode computationMode)
throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode, parseBuildLanguageOptions("--experimental_bzl_visibility=true"));
scratch.file(
"a/BUILD", //
"load(\"//b:foo.bzl\", \"x\")");
scratch.file("b/BUILD");
scratch.file(
"b/foo.bzl",
"""
visibility("private")
x = 1
""");
reporter.removeHandler(failFastHandler);
Exception ex = evaluatePackageoidToException("a");
assertThat(ex)
.hasMessageThat()
.contains(
"error loading package 'a': file //a:BUILD contains .bzl load visibility violations");
assertDetailedExitCode(
ex, PackageLoading.Code.IMPORT_STARLARK_FILE_ERROR, ExitCode.BUILD_FAILURE);
assertContainsEvent("Starlark file //b:foo.bzl is not visible for loading from package //a.");
}
@Test
public void testBzlVisibilityViolationDemotedToWarningWhenBreakGlassFlagIsSet(
@TestParameter ComputationMode computationMode) throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode,
parseBuildLanguageOptions(
"--experimental_bzl_visibility=true", "--check_bzl_visibility=false"));
scratch.file(
"a/BUILD", //
"load(\"//b:foo.bzl\", \"x\")");
scratch.file("b/BUILD");
scratch.file(
"b/foo.bzl",
"""
visibility("private")
x = 1
""");
validPackageoidWithoutErrors("a");
assertContainsEvent("Starlark file //b:foo.bzl is not visible for loading from package //a.");
assertContainsEvent("Continuing because --nocheck_bzl_visibility is active");
}
@Test
public void testVisibilityCallableNotAvailableInBUILD(
@TestParameter ComputationMode computationMode) throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode, parseBuildLanguageOptions("--experimental_bzl_visibility=true"));
scratch.file(
"a/BUILD", //
"visibility(\"public\")");
reporter.removeHandler(failFastHandler);
// The evaluation result ends up being null, probably due to the test framework swallowing
// exceptions (similar to b/26382502). So let's just look for the error event instead of
// asserting on the exception.
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), getSkyKey("a"), /* keepGoing= */ false, reporter);
assertContainsEvent("name 'visibility' is not defined");
}
@Test
public void testVisibilityCallableErroneouslyInvokedInBUILD(
@TestParameter ComputationMode computationMode) throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode, parseBuildLanguageOptions("--experimental_bzl_visibility=true"));
scratch.file(
"a/BUILD",
"""
load(":helper.bzl", "helper")
helper()
""");
scratch.file(
"a/helper.bzl",
"""
def helper():
visibility("public")
""");
reporter.removeHandler(failFastHandler);
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), getSkyKey("a"), /* keepGoing= */ false, reporter);
assertContainsEvent(
"visibility() can only be used during .bzl initialization (top-level evaluation)");
}
// Regression test for the two ugly consequences of a bug where GlobFunction incorrectly matched
// dangling symlinks.
@Test
public void testIncrementalSkyframeHybridGlobbingOnDanglingSymlink(
@TestParameter ComputationMode computationMode) throws Exception {
Path packageDirPath =
scratch.file("foo/BUILD", "exports_files(glob(['*.txt']))").getParentDirectory();
scratch.file("foo/existing.txt");
FileSystemUtils.ensureSymbolicLink(packageDirPath.getChild("dangling.txt"), "nope");
preparePackageLoading(computationMode, rootDirectory);
Packageoid pkg = validPackageoidWithoutErrors("foo");
assertThat(pkg.containsErrors()).isFalse();
assertThat(pkg.getTarget("existing.txt").getName()).isEqualTo("existing.txt");
assertThrows(NoSuchTargetException.class, () -> pkg.getTarget("dangling.txt"));
scratch.overwriteFile(
"foo/BUILD",
"""
exports_files(glob(["*.txt"]))
#some-irrelevant-comment
""");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(),
Root.fromPath(rootDirectory));
Packageoid pkg2 = validPackageoidWithoutErrors("foo");
assertThat(pkg2.containsErrors()).isFalse();
assertThat(pkg2.getTarget("existing.txt").getName()).isEqualTo("existing.txt");
assertThrows(NoSuchTargetException.class, () -> pkg2.getTarget("dangling.txt"));
// One consequence of the bug was that dangling symlinks were matched by globs evaluated by
// Skyframe globbing, meaning there would incorrectly be corresponding targets in packages
// that had skyframe cache hits during skyframe hybrid globbing.
scratch.file("foo/nope");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/nope")).build(),
Root.fromPath(rootDirectory));
Packageoid newPkg = validPackageoidWithoutErrors("foo");
assertThat(newPkg.containsErrors()).isFalse();
assertThat(newPkg.getTarget("existing.txt").getName()).isEqualTo("existing.txt");
// Another consequence of the bug is that change pruning would incorrectly cut off changes that
// caused a dangling symlink potentially matched by a glob to come into existence.
assertThat(newPkg.getTarget("dangling.txt").getName()).isEqualTo("dangling.txt");
assertThat(newPkg).isNotSameInstanceAs(pkg);
}
// Regression test for Skyframe globbing incorrectly matching the package's directory path on
// 'glob(['**'], exclude_directories = 0)'. We test for this directly by triggering
// hybrid globbing (gives coverage for both non-skyframe globbing and skyframe globbing).
@Test
public void testRecursiveGlobNeverMatchesPackageDirectory(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file(
"foo/BUILD",
"[filegroup(name = x + '-matched') for x in glob(['**'], exclude_directories = 0)]");
scratch.file("foo/bar");
preparePackageLoading(computationMode, rootDirectory);
Packageoid pkg = validPackageoidWithoutErrors("foo");
assertThat(pkg.containsErrors()).isFalse();
assertThat(pkg.getTarget("bar-matched").getName()).isEqualTo("bar-matched");
assertThrows(NoSuchTargetException.class, () -> pkg.getTarget("-matched"));
scratch.overwriteFile(
"foo/BUILD",
"""
[filegroup(name = x + "-matched") for x in glob(
["**"],
exclude_directories = 0,
)]
#some-irrelevant-comment
""");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(),
Root.fromPath(rootDirectory));
Packageoid pkg2 = validPackageoidWithoutErrors("foo");
assertThat(pkg2.containsErrors()).isFalse();
assertThat(pkg2.getTarget("bar-matched").getName()).isEqualTo("bar-matched");
assertThrows(NoSuchTargetException.class, () -> pkg2.getTarget("-matched"));
}
@Test
public void testPackageLoadingErrorOnIOExceptionReadingBuildFile(
@TestParameter ComputationMode computationMode) throws Exception {
Path fooBuildFilePath = scratch.file("foo/BUILD");
IOException exn = new IOException("nope");
fs.throwExceptionOnGetInputStream(fooBuildFilePath, exn);
preparePackageLoading(computationMode);
Exception ex = evaluatePackageoidToException("foo");
assertThat(ex).hasMessageThat().contains("nope");
assertThat(ex).isInstanceOf(NoSuchPackageException.class);
assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);
assertDetailedExitCode(ex, PackageLoading.Code.BUILD_FILE_MISSING, ExitCode.BUILD_FAILURE);
}
@Test
public void testPackageLoadingErrorOnMissingBuildFile_singlePackagePath(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("foo/bar");
preparePackageLoading(computationMode);
// There is no foo/BUILD file, but we enforce loading package 'foo'.
Exception ex = evaluatePackageoidToException("foo");
assertThat(ex)
.hasMessageThat()
.contains(
"BUILD file not found in any of the following directories. "
+ "Add a BUILD file to a directory to mark it as a package.\n"
// Print the package_path relative directory path if only a single `package_path` is
// provided.
+ " - foo");
assertThat(ex).isInstanceOf(BuildFileNotFoundException.class);
assertDetailedExitCode(ex, PackageLoading.Code.BUILD_FILE_MISSING, ExitCode.BUILD_FAILURE);
}
@Test
public void testPackageLoadingErrorOnMissingBuildFile_multiplePackagePath(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("foo/bar");
Path otherRootDir = scratch.dir("/ws2");
scratch.file("/ws2/foo/bar");
preparePackageLoading(computationMode, rootDirectory, otherRootDir);
// There is no foo/BUILD file under both `package_path`s foo directory, but we enforce loading
// package 'foo'.
Exception ex = evaluatePackageoidToException("foo");
assertThat(ex)
.hasMessageThat()
.contains(
"BUILD file not found in any of the following directories. "
+ "Add a BUILD file to a directory to mark it as a package.");
// Print the absolute directory paths if multiple `package_path`s are provided.
assertThat(ex).hasMessageThat().contains("- /workspace/foo");
assertThat(ex).hasMessageThat().contains("- /ws2/foo");
assertThat(ex).isInstanceOf(BuildFileNotFoundException.class);
assertDetailedExitCode(ex, PackageLoading.Code.BUILD_FILE_MISSING, ExitCode.BUILD_FAILURE);
}
@Test
public void testPackageLoadingErrorOnIOExceptionReadingBzlFile(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("foo/BUILD", "load('//foo:bzl.bzl', 'x')");
Path fooBzlFilePath = scratch.file("foo/bzl.bzl");
IOException exn = new IOException("nope");
fs.throwExceptionOnGetInputStream(fooBzlFilePath, exn);
preparePackageLoading(computationMode);
Exception ex = evaluatePackageoidToException("foo");
assertThat(ex).hasMessageThat().contains("nope");
assertThat(ex).isInstanceOf(NoSuchPackageException.class);
assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);
assertDetailedExitCode(
ex, PackageLoading.Code.IMPORT_STARLARK_FILE_ERROR, ExitCode.BUILD_FAILURE);
}
@Test
public void testLabelsCrossesSubpackageBoundaries_singleSubpackageCrossing(
@TestParameter ComputationMode computationMode) throws Exception {
reporter.removeHandler(failFastHandler);
scratch.file("pkg/foo/BUILD", "exports_files(['sub/bar/blah'])");
scratch.file("pkg/foo/sub/BUILD");
preparePackageLoading(computationMode);
invalidatePackages();
SkyKey skyKey = getSkyKey("pkg/foo");
EvaluationResult<PackageoidValue> result =
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), skyKey, /* keepGoing= */ false, reporter);
assertThatEvaluationResult(result).hasNoError();
assertThat(result.get(skyKey).getPackageoid().containsErrors()).isTrue();
assertContainsEvent(
"Label '//pkg/foo:sub/bar/blah' is invalid because 'pkg/foo/sub' is a subpackage; perhaps"
+ " you meant to put the colon here: '//pkg/foo/sub:bar/blah'?");
}
@Test
public void testLabelsCrossesSubpackageBoundaries_complexSubpackageCrossing(
@TestParameter ComputationMode computationMode) throws Exception {
reporter.removeHandler(failFastHandler);
scratch.file(
"pkg/foo/BUILD",
"""
exports_files(["sub11/sub12/blah1"])
exports_files(["sub21/sub22/blah2"])
""");
scratch.file("pkg/foo/sub11/BUILD");
scratch.file("pkg/foo/sub11/sub12/BUILD");
scratch.file("pkg/foo/sub21/BUILD");
scratch.file("pkg/foo/sub21/sub22/BUILD");
preparePackageLoading(computationMode);
invalidatePackages();
SkyKey skyKey = getSkyKey("pkg/foo");
EvaluationResult<PackageoidValue> result =
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), skyKey, /* keepGoing= */ false, reporter);
assertThatEvaluationResult(result).hasNoError();
assertThat(result.get(skyKey).getPackageoid().containsErrors()).isTrue();
// Only the deepest package that crosses subpackage boundary should be displayed in the error
// message.
assertContainsEvent(
"Label '//pkg/foo:sub11/sub12/blah1' is invalid because 'pkg/foo/sub11/sub12' is a"
+ " subpackage; perhaps you meant to put the colon here:"
+ " '//pkg/foo/sub11/sub12:blah1'?");
assertContainsEvent(
"Label '//pkg/foo:sub21/sub22/blah2' is invalid because 'pkg/foo/sub21/sub22' is a"
+ " subpackage; perhaps you meant to put the colon here:"
+ " '//pkg/foo/sub21/sub22:blah2'?");
if (computationMode.equals(ComputationMode.PACKAGE_FROM_PACKAGE_PIECES)) {
assertContainsEvent("error in top-level package piece defined by //pkg/foo:BUILD");
assertThat(eventCollector.filtered(EventKind.ERROR)).hasSize(3);
} else {
assertThat(eventCollector.filtered(EventKind.ERROR)).hasSize(2);
}
}
@Test
public void testSymlinkCycleEncounteredWhileHandlingLabelCrossingSubpackageBoundaries(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("pkg/BUILD", "exports_files(['sub/blah'])");
Path subBuildFilePath = scratch.dir("pkg/sub").getChild("BUILD");
FileSystemUtils.ensureSymbolicLink(subBuildFilePath, subBuildFilePath);
preparePackageLoading(computationMode);
invalidatePackages();
Exception ex = evaluatePackageoidToException("pkg");
assertThat(ex).isInstanceOf(BuildFileNotFoundException.class);
assertThat(ex)
.hasMessageThat()
.contains(
"no such package 'pkg/sub': Symlink cycle detected while trying to find BUILD file");
assertContainsEvent("circular symlinks detected");
}
// Regression test for b/206459361.
@Test
public void nonSkyframeGlobbingIOException_andLabelCrossingSubpackageBoundaries_withKeepGoing(
@TestParameter ComputationMode computationMode) throws Exception {
reporter.removeHandler(failFastHandler);
// When a package's BUILD file and the relevant filesystem state is such that non-Skyframe
// globbing will encounter an IOException due to a directory symlink cycle *and* the BUILD file
// defines a target with a label that crosses subpackage boundaries,
Path pkgBUILDPath =
scratch.file(
"pkg/BUILD",
"""
exports_files(["sub/blah"]) # label crossing subpackage boundaries
glob(["globcycle/**/foo.txt"]) # triggers non-Skyframe globbing error
""");
scratch.file("pkg/sub/BUILD");
Path pkgGlobcyclePath = pkgBUILDPath.getParentDirectory().getChild("globcycle");
FileSystemUtils.ensureSymbolicLink(pkgGlobcyclePath, pkgGlobcyclePath);
assertThrows(IOException.class, () -> pkgGlobcyclePath.statIfFound(Symlinks.FOLLOW));
preparePackageLoading(computationMode);
invalidatePackages();
// ... and we evaluate the package with keepGoing == true, we expect the evaluation to fail with
// the non-Skyframe globbing error, but for the label crossing event to *not* get added (because
// the globbing IOException would put Package.Builder in a state on which we cannot run
// handleLabelsCrossingSubpackagesAndPropagateInconsistentFilesystemExceptions).
SkyKey pkgKey = getSkyKey("pkg");
EvaluationResult<PackageoidValue> result =
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), pkgKey, /* keepGoing= */ true, reporter);
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(pkgKey)
.hasExceptionThat()
.isInstanceOf(NoSuchPackageException.class);
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(pkgKey)
.hasExceptionThat()
.hasMessageThat()
.contains("Symlink cycle: /workspace/pkg/globcycle");
assertDoesNotContainEvent(
"Label '//pkg:sub/blah' is invalid because 'pkg/sub' is a subpackage");
}
@Test
public void testGlobAllowEmpty_paramValueMustBeBoolean(
@TestParameter ComputationMode computationMode) throws Exception {
reporter.removeHandler(failFastHandler);
scratch.file("pkg/BUILD", "x = " + "glob(['*.foo'], allow_empty = 5)");
preparePackageLoading(computationMode);
invalidatePackages();
validPackageoid("pkg");
assertContainsEvent("expected boolean for argument `allow_empty`, got `5`");
}
@Test
public void testGlobAllowEmpty_functionParam(@TestParameter ComputationMode computationMode)
throws Exception {
scratch.file("pkg/BUILD", "x = " + "glob(['*.foo'], allow_empty=True)");
preparePackageLoading(computationMode);
invalidatePackages();
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isFalse();
assertNoEvents();
}
@Test
public void testGlobAllowEmpty_starlarkOption(@TestParameter ComputationMode computationMode)
throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode,
parseBuildLanguageOptions("--incompatible_disallow_empty_glob=false"),
rootDirectory);
scratch.file("pkg/BUILD", "x = " + "glob(['*.foo'])");
invalidatePackages();
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isFalse();
assertNoEvents();
}
@Test
public void testGlobDisallowEmpty_functionParam_wasNonEmptyAndBecomesEmpty(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("pkg/BUILD", "x = " + "glob(['*.foo'], allow_empty=False)");
scratch.file("pkg/blah.foo");
preparePackageLoading(computationMode);
invalidatePackages();
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isFalse();
assertNoEvents();
scratch.deleteFile("pkg/blah.foo");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("pkg/blah.foo")).build(),
Root.fromPath(rootDirectory));
reporter.removeHandler(failFastHandler);
pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(
"glob pattern '*.foo' didn't match anything, but allow_empty is set to False (the "
+ "default value of allow_empty can be set with --incompatible_disallow_empty_glob).");
}
@Test
public void testGlobDisallowEmpty_starlarkOption_wasNonEmptyAndBecomesEmpty(
@TestParameter ComputationMode computationMode) throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode,
parseBuildLanguageOptions("--incompatible_disallow_empty_glob=true"),
rootDirectory);
scratch.file("pkg/BUILD", "x = " + "glob(['*.foo'])");
scratch.file("pkg/blah.foo");
invalidatePackages();
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isFalse();
assertNoEvents();
scratch.deleteFile("pkg/blah.foo");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("pkg/blah.foo")).build(),
Root.fromPath(rootDirectory));
reporter.removeHandler(failFastHandler);
pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(
"glob pattern '*.foo' didn't match anything, but allow_empty is set to False (the "
+ "default value of allow_empty can be set with --incompatible_disallow_empty_glob).");
}
@Test
public void testGlobDisallowEmpty_functionParam_wasEmptyAndStaysEmpty(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("pkg/BUILD", "x = " + "glob(['*.foo'], allow_empty=False)");
preparePackageLoading(computationMode);
invalidatePackages();
reporter.removeHandler(failFastHandler);
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
String expectedEventString =
"glob pattern '*.foo' didn't match anything, but allow_empty is set to False (the "
+ "default value of allow_empty can be set with --incompatible_disallow_empty_glob).";
assertContainsEvent(expectedEventString);
scratch.overwriteFile("pkg/BUILD", "x = " + "glob(['*.foo'], allow_empty=False) #comment");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("pkg/BUILD")).build(),
Root.fromPath(rootDirectory));
pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(expectedEventString);
}
@Test
public void testGlobDisallowEmpty_starlarkOption_wasEmptyAndStaysEmpty(
@TestParameter ComputationMode computationMode) throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode,
parseBuildLanguageOptions("--incompatible_disallow_empty_glob=true"),
rootDirectory);
scratch.file("pkg/BUILD", "x = " + "glob(['*.foo'])");
invalidatePackages();
reporter.removeHandler(failFastHandler);
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
String expectedEventString =
"glob pattern '*.foo' didn't match anything, but allow_empty is set to False (the "
+ "default value of allow_empty can be set with --incompatible_disallow_empty_glob).";
assertContainsEvent(expectedEventString);
scratch.overwriteFile("pkg/BUILD", "x = " + "glob(['*.foo']) #comment");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("pkg/BUILD")).build(),
Root.fromPath(rootDirectory));
pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(expectedEventString);
}
@Test
public void testGlobDisallowEmpty_functionParam_wasEmptyDueToExcludeAndStaysEmpty(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("pkg/BUILD", "x = glob(include=['*.foo'], exclude=['blah.*'], allow_empty=False)");
scratch.file("pkg/blah.foo");
preparePackageLoading(computationMode);
invalidatePackages();
reporter.removeHandler(failFastHandler);
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
String expectedEventString =
"all files in the glob have been excluded, but allow_empty is set to False (the "
+ "default value of allow_empty can be set with --incompatible_disallow_empty_glob).";
assertContainsEvent(expectedEventString);
scratch.overwriteFile(
"pkg/BUILD",
"x = glob(include=['*.foo'], exclude=['blah.*'], allow_empty=False) # comment");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("pkg/BUILD")).build(),
Root.fromPath(rootDirectory));
pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(expectedEventString);
}
@Test
public void testGlobDisallowEmpty_starlarkOption_wasEmptyDueToExcludeAndStaysEmpty(
@TestParameter ComputationMode computationMode) throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode,
parseBuildLanguageOptions("--incompatible_disallow_empty_glob=true"),
rootDirectory);
scratch.file("pkg/BUILD", "x = glob(include=['*.foo'], exclude=['blah.*'])");
scratch.file("pkg/blah.foo");
invalidatePackages();
reporter.removeHandler(failFastHandler);
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
String expectedEventString =
"all files in the glob have been excluded, but allow_empty is set to False (the "
+ "default value of allow_empty can be set with --incompatible_disallow_empty_glob).";
assertContainsEvent(expectedEventString);
scratch.overwriteFile("pkg/BUILD", "x = glob(include=['*.foo'], exclude=['blah.*']) # comment");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("pkg/BUILD")).build(),
Root.fromPath(rootDirectory));
pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(expectedEventString);
}
@Test
public void testGlobDisallowEmpty_functionParam_wasEmptyAndBecomesNonEmpty(
@TestParameter ComputationMode computationMode) throws Exception {
scratch.file("pkg/BUILD", "x = " + "glob(['*.foo'], allow_empty=False)");
preparePackageLoading(computationMode);
invalidatePackages();
reporter.removeHandler(failFastHandler);
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(
"glob pattern '*.foo' didn't match anything, but allow_empty is set to False (the "
+ "default value of allow_empty can be set with --incompatible_disallow_empty_glob).");
scratch.file("pkg/blah.foo");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("pkg/blah.foo")).build(),
Root.fromPath(rootDirectory));
reporter.addHandler(failFastHandler);
eventCollector.clear();
pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isFalse();
assertNoEvents();
}
@Test
public void testGlobDisallowEmpty_starlarkOption_wasEmptyAndBecomesNonEmpty(
@TestParameter ComputationMode computationMode) throws Exception {
preparePackageLoadingWithCustomStarklarkSemanticsOptions(
computationMode,
parseBuildLanguageOptions("--incompatible_disallow_empty_glob=true"),
rootDirectory);
scratch.file("pkg/BUILD", "x = " + "glob(['*.foo'])");
invalidatePackages();
reporter.removeHandler(failFastHandler);
Packageoid pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(
"glob pattern '*.foo' didn't match anything, but allow_empty is set to False (the "
+ "default value of allow_empty can be set with --incompatible_disallow_empty_glob).");
scratch.file("pkg/blah.foo");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("pkg/blah.foo")).build(),
Root.fromPath(rootDirectory));
reporter.addHandler(failFastHandler);
eventCollector.clear();
pkg = validPackageoid("pkg");
assertThat(pkg.containsErrors()).isFalse();
assertNoEvents();
}
@Test
public void testPackageRecordsLoadedModules(@TestParameter ComputationMode computationMode)
throws Exception {
scratch.file("p/BUILD", "load('a.bzl', 'a'); load(':b.bzl', 'b')");
scratch.file("p/a.bzl", "load('c.bzl', 'c'); a = c");
scratch.file("p/b.bzl", "load(':c.bzl', 'c'); b = c");
scratch.file("p/c.bzl", "load(':d.bzl', 'd'); c = d");
scratch.file("p/d.bzl", "d = 0");
// load p
preparePackageLoading(computationMode, rootDirectory);
Packageoid p = validPackageoidWithoutErrors("p");
assertThat(toStrings(p.getDeclarations().getOrComputeTransitivelyLoadedStarlarkFiles()))
.containsExactly("//p:a.bzl", "//p:b.bzl", "//p:c.bzl", "//p:d.bzl");
assertThat(p.getDeclarations().countTransitivelyLoadedStarlarkFiles()).isEqualTo(4);
// Custom visitation: c.bzl is visited twice, but the second time we don't recurse, so d.bzl is
// only visited once.
Multiset<Label> loads = HashMultiset.create();
p.getDeclarations().visitLoadGraph(load -> loads.add(load, 1) == 0);
assertThat(toStrings(loads))
.containsExactly("//p:a.bzl", "//p:b.bzl", "//p:c.bzl", "//p:c.bzl", "//p:d.bzl");
}
private static Stream<String> toStrings(Iterable<Label> labels) {
return stream(labels).map(Label::toString);
}
@Test
public void veryBrokenPackagePostsDoneToProgressReceiver(
@TestParameter ComputationMode computationMode) throws Exception {
reporter.removeHandler(failFastHandler);
// Note: syntax error (recovered), non-existent .bzl file.
scratch.file("pkg/BUILD", "load('//does_not:exist.bzl', 'broken'");
preparePackageLoading(computationMode);
SkyKey key = getSkyKey("pkg");
EvaluationResult<PackageoidValue> result =
SkyframeExecutorTestUtils.evaluate(getSkyframeExecutor(), key, false, reporter);
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(key);
assertContainsEvent("syntax error at 'newline': expected ,");
assertThat(getSkyframeExecutor().getPackageProgressReceiver().progressState())
.isEqualTo(new Pair<>("1 packages loaded", ""));
}
@Test
public void testNonSkyframeGlobbingEncountersSymlinkCycleAndThrowsIOException(
@TestParameter ComputationMode computationMode) throws Exception {
reporter.removeHandler(failFastHandler);
// When a package's BUILD file and the relevant filesystem state is such that non-Skyframe
// globbing will encounter an IOException due to a directory symlink cycle,
Path fooBUILDPath = scratch.file("foo/BUILD", "glob(['cycle/**/foo.txt'])");
Path fooCyclePath = fooBUILDPath.getParentDirectory().getChild("cycle");
FileSystemUtils.ensureSymbolicLink(fooCyclePath, fooCyclePath);
IOException ioExnFromFS =
assertThrows(IOException.class, () -> fooCyclePath.statIfFound(Symlinks.FOLLOW));
// And it is indeed the case that the FileSystem throws an IOException when the cycle's Path is
// stat'd (following symlinks, as non-Skyframe globbing does).
assertThat(ioExnFromFS).hasMessageThat().contains("Too many levels of symbolic links");
preparePackageLoading(computationMode);
// Then, when we evaluate the PackageValue node for the Package in keepGoing mode,
SkyKey pkgKey = getSkyKey("foo");
EvaluationResult<PackageoidValue> result =
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), pkgKey, /* keepGoing= */ true, reporter);
// The result is a *non-transient* Skyframe error.
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(pkgKey).isNotTransient();
// And that error is a NoSuchPackageException
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(pkgKey)
.hasExceptionThat()
.isInstanceOf(NoSuchPackageException.class);
// With a useful error message,
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(pkgKey)
.hasExceptionThat()
.hasMessageThat()
.contains("Symlink cycle: /workspace/foo/cycle");
// And appropriate Skyframe root cause (N.B. since we want PackageFunction to rethrow in
// situations like this, we want the PackageValue node to be its own root cause).
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(pkgKey);
// Then, when we modify the BUILD file so as to force package loading,
scratch.overwriteFile(
"foo/BUILD", "glob(['cycle/**/foo.txt']) # dummy comment to force package loading");
if (!globUnderSingleDep) {
// When globbing strategy is SKYFRAME_HYBRID (globUnderSingleDep = false), and we don't make
// any filesystem changes that would invalidate the GlobValues, PackageFunction will observe
// cache hits from Skyframe globbing.
//
// And we also have our filesystem blow up if the directory symlink cycle is encountered
// (thus, the absence of a crash indicates the lack of non-Skyframe globbing).
//
// However, when globbing strategy is GLOBS (globUnderSingleDep = true), and we lose Skyframe
// Hybrid globbing, we expect package reloading still to always do non-Skyframe globbing which
// calls stats for symlink `foo/cycle`.
fs.stubStatError(
fooCyclePath,
new IOException() {
@Override
public String getMessage() {
throw new IllegalStateException("shouldn't get here!");
}
});
}
// And we evaluate the PackageValue node for the Package in keepGoing mode,
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(),
Root.fromPath(rootDirectory));
// The results are exactly the same as before,
result =
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), pkgKey, /*keepGoing=*/ true, reporter);
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(pkgKey).isNotTransient();
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(pkgKey)
.hasExceptionThat()
.isInstanceOf(NoSuchPackageException.class);
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(pkgKey)
.hasExceptionThat()
.hasMessageThat()
.contains("Symlink cycle: /workspace/foo/cycle");
// Thus showing that clean and incremental package loading have the same semantics in the
// presence of a symlink cycle encountered during glob evaluation.
}
@Test
// The test checks direct deps of the PackageValue, so testing in PACKAGE_FROM_PACKAGE_PIECES mode
// doesn't make sense.
public void testGlobbingSkyframeDependencyStructure(
@TestParameter({"MONOLITHIC_PACKAGE", "PACKAGE_PIECE_FOR_BUILD_FILE"})
ComputationMode computationMode)
throws Exception {
reporter.removeHandler(failFastHandler);
Root pkgRoot = getSkyframeExecutor().getPackagePathEntries().getFirst();
Path fooBuildPath =
scratch.file("foo/BUILD", "glob(['dir/*.sh'])", "subpackages(include = ['subpkg/**'])");
Path fooDirPath = fooBuildPath.getParentDirectory().getChild("dir");
scratch.file("foo/dir/bar.sh");
scratch.file("foo/dir/baz.sh");
Path fooSubpkgPath = fooBuildPath.getParentDirectory().getChild("subpkg");
scratch.file("foo/subpkg/BUILD");
preparePackageLoading(computationMode);
SkyKey pkgKey = getSkyKey("foo");
SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), pkgKey, /* keepGoing= */ true, reporter);
InMemoryGraph graph = getSkyframeExecutor().memoizingEvaluator.getInMemoryGraph();
InMemoryNodeEntry packageNode = graph.getIfPresent(pkgKey);
if (globUnderSingleDep) {
// The package subgraph with single Globs node looks like this:
// PKG["foo"]
// |- GLOBS[["dir/*.sh", FILES], ["subpkg/**", SUBPACKAGES]]
// |- FILE["foo/dir"]
// |- DIRECTORY_LISTING["foo/dir"]
// |- PACKAGE_LOOKUP["foo/dir"]
// |- FILE["foo/subdir"]
// |- PACKAGE_LOOKUP["foo/subdir"]
GlobsValue.Key globsKey =
GlobsValue.key(
PackageIdentifier.createInMainRepo("foo"),
pkgRoot,
ImmutableSet.of(
GlobRequest.create("dir/*.sh", Operation.FILES),
GlobRequest.create("subpkg/**", Operation.SUBPACKAGES)));
assertThat(packageNode.getDirectDeps()).contains(globsKey);
InMemoryNodeEntry globsNode = graph.getIfPresent(globsKey);
SkyValue globsValue = globsNode.getValue();
assertThat(globsValue).isInstanceOf(GlobsValue.class);
assertThat(((GlobsValue) globsValue).getMatches())
.containsExactly(
PathFragment.create("subpkg"),
PathFragment.create("dir/bar.sh"),
PathFragment.create("dir/baz.sh"));
ImmutableSet<SkyKey> globsDirectDeps = ImmutableSet.copyOf(globsNode.getDirectDeps());
assertThat(globsDirectDeps)
.containsAtLeast(
DirectoryListingValue.key(RootedPath.toRootedPath(pkgRoot, fooDirPath)),
FileValue.key(RootedPath.toRootedPath(pkgRoot, fooDirPath)),
FileValue.key(RootedPath.toRootedPath(pkgRoot, fooSubpkgPath)),
PackageLookupValue.key(PackageIdentifier.createInMainRepo("foo/dir")),
PackageLookupValue.key(PackageIdentifier.createInMainRepo("foo/subpkg")));
} else {
// The package subgraph with multiple Glob nodes looks like this:
// PKG["foo"]
// |- GLOB["dir/*.sh", FILES]
// |- FILE["foo/dir"]
// |- DIRECTORY_LISTING["foo/dir"]
// |- PACKAGE_LOOKUP["foo/dir"]
// |- GLOB["subpkg/**", SUBPACKAGES]
// |- FILE["foo/subdir"]
// |- PACKAGE_LOOKUP["foo/subdir"]
GlobDescriptor dirGlobDescriptor =
GlobValue.key(
PackageIdentifier.createInMainRepo("foo"),
pkgRoot,
/* pattern= */ "dir/*.sh",
Operation.FILES,
PathFragment.EMPTY_FRAGMENT);
GlobDescriptor subdirGlobDescriptor =
GlobValue.key(
PackageIdentifier.createInMainRepo("foo"),
pkgRoot,
/* pattern= */ "subpkg/**",
Operation.SUBPACKAGES,
PathFragment.EMPTY_FRAGMENT);
assertThat(packageNode.getDirectDeps())
.containsAtLeast(dirGlobDescriptor, subdirGlobDescriptor);
ImmutableSet<SkyKey> dirGlobNodeDeps =
ImmutableSet.copyOf(graph.getIfPresent(dirGlobDescriptor).getDirectDeps());
assertThat(dirGlobNodeDeps)
.containsAtLeast(
DirectoryListingValue.key(RootedPath.toRootedPath(pkgRoot, fooDirPath)),
FileValue.key(RootedPath.toRootedPath(pkgRoot, fooDirPath)),
PackageLookupValue.key(PackageIdentifier.createInMainRepo("foo/dir")));
ImmutableSet<SkyKey> subdirGlobNodeDeps =
ImmutableSet.copyOf(graph.getIfPresent(subdirGlobDescriptor).getDirectDeps());
assertThat(subdirGlobNodeDeps)
.containsAtLeast(
FileValue.key(RootedPath.toRootedPath(pkgRoot, fooSubpkgPath)),
PackageLookupValue.key(PackageIdentifier.createInMainRepo("foo/subpkg")));
}
}
private static void assertDetailedExitCode(
Exception exception, PackageLoading.Code expectedPackageLoadingCode, ExitCode exitCode) {
assertThat(exception).isInstanceOf(DetailedException.class);
DetailedExitCode detailedExitCode = ((DetailedException) exception).getDetailedExitCode();
assertThat(detailedExitCode.getExitCode()).isEqualTo(exitCode);
assertThat(detailedExitCode.getFailureDetail().getPackageLoading().getCode())
.isEqualTo(expectedPackageLoadingCode);
assertThat(DetailedExitCode.getExitCode(detailedExitCode.getFailureDetail()))
.isEqualTo(exitCode);
}
/**
* Tests of the prelude file functionality.
*
* <p>This is in a separate BuildViewTestCase because we override the prelude label for the test.
* (The prelude label is configured differently between Bazel and Blaze.)
*/
@RunWith(JUnit4.class)
public static class PreludeTest extends BuildViewTestCase {
private final CustomInMemoryFs fs = new CustomInMemoryFs(new ManualClock());
@Override
protected FileSystem createFileSystem() {
return fs;
}
@Override
protected ConfiguredRuleClassProvider createRuleClassProvider() {
ConfiguredRuleClassProvider.Builder builder = new ConfiguredRuleClassProvider.Builder();
// addStandardRules() may call setPrelude(), so do it first.
TestRuleClassProvider.addStandardRules(builder);
builder.setPrelude("//tools/test_build_rules:test_prelude");
return builder.build();
}
@Test
public void testPreludeDefinedSymbolIsUsable() throws Exception {
scratch.file("tools/test_build_rules/BUILD");
scratch.file(
"tools/test_build_rules/test_prelude", //
"foo = 'FOO'");
scratch.file(
"pkg/BUILD", //
"print(foo)");
invalidatePackages();
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("FOO");
}
@Test
public void testPreludeAutomaticallyReexportsLoadedSymbols() throws Exception {
scratch.file("tools/test_build_rules/BUILD");
scratch.file(
"tools/test_build_rules/test_prelude", //
"load('//util:common.bzl', 'foo')");
scratch.file("util/BUILD");
scratch.file(
"util/common.bzl", //
"foo = 'FOO'");
scratch.file(
"pkg/BUILD", //
"print(foo)");
invalidatePackages();
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("FOO");
}
// TODO(brandjon): Invert this test once the prelude is a module instead of a syntactic
// mutation on BUILD files.
@Test
public void testPreludeCanExportUnderscoreSymbols() throws Exception {
scratch.file("tools/test_build_rules/BUILD");
scratch.file(
"tools/test_build_rules/test_prelude", //
"_foo = 'FOO'");
scratch.file(
"pkg/BUILD", //
"print(_foo)");
invalidatePackages();
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("FOO");
}
@Test
public void testPreludeCanShadowUniversal() throws Exception {
scratch.file("tools/test_build_rules/BUILD");
scratch.file(
"tools/test_build_rules/test_prelude", //
"len = 'FOO'");
scratch.file(
"pkg/BUILD", //
"print(len)");
invalidatePackages();
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("FOO");
}
@Test
public void testPreludeCanShadowPredeclareds() throws Exception {
scratch.file("tools/test_build_rules/BUILD");
scratch.file(
"tools/test_build_rules/test_prelude", //
"cc_library = 'FOO'");
scratch.file(
"pkg/BUILD", //
"print(cc_library)");
invalidatePackages();
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("FOO");
}
@Test
public void testPreludeCanShadowInjectedPredeclareds() throws Exception {
setBuildLanguageOptions("--experimental_builtins_bzl_path=tools/builtins_staging");
scratch.file(
"tools/builtins_staging/exports.bzl",
"""
exported_toplevels = {}
exported_rules = {"cc_library": "BAR"}
exported_to_java = {}
""");
scratch.file("tools/test_build_rules/BUILD");
scratch.file(
"tools/test_build_rules/test_prelude", //
"cc_library = 'FOO'");
scratch.file(
"pkg/BUILD", //
"print(cc_library)");
try {
invalidatePackages();
} catch (
@SuppressWarnings("InterruptedExceptionSwallowed")
Exception e) {
// Ignore any errors.
}
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("FOO");
}
@Test
public void testPreludeSymbolCannotBeMutated() throws Exception {
scratch.file("tools/test_build_rules/BUILD");
scratch.file(
"tools/test_build_rules/test_prelude", //
"foo = ['FOO']");
scratch.file(
"pkg/BUILD", //
"foo.append('BAR')");
reporter.removeHandler(failFastHandler);
invalidatePackages();
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("trying to mutate a frozen list value");
}
@Test
public void testPreludeCanAccessBzlDialectFeatures() throws Exception {
scratch.file("tools/test_build_rules/BUILD");
// Test both bzl symbols and syntax (e.g. function defs).
scratch.file(
"tools/test_build_rules/test_prelude", //
"def foo():",
" return native.glob");
scratch.file(
"pkg/BUILD", //
"print(foo())");
invalidatePackages();
getConfiguredTarget("//pkg:BUILD");
// Prelude can access native.glob (though only a BUILD thread can call it).
assertContainsEvent("<built-in method glob of native value>");
}
@Test
public void testPreludeNeedNotBePresent() throws Exception {
scratch.file(
"pkg/BUILD", //
"print('FOO')");
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("FOO");
}
@Test
public void testPreludeNeedNotBePresent_evenWhenPackageIs() throws Exception {
scratch.file("tools/test_build_rules/BUILD");
scratch.file(
"pkg/BUILD", //
"print('FOO')");
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("FOO");
}
@Test
public void testPreludeFileNotRecognizedWithoutPackage() throws Exception {
scratch.file(
"tools/test_build_rules/test_prelude", //
"foo = 'FOO'");
scratch.file(
"pkg/BUILD", //
"print(foo)");
// The prelude file is not found without a corresponding package to contain it. BUILD files
// get processed as if no prelude file is present.
reporter.removeHandler(failFastHandler);
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("name 'foo' is not defined");
}
@Test
public void testPreludeFailsWhenErrorInPreludeFile() throws Exception {
scratch.file("tools/test_build_rules/BUILD");
scratch.file(
"tools/test_build_rules/test_prelude", //
"1//0", // <-- dynamic error
"foo = 'FOO'");
scratch.file(
"pkg/BUILD", //
"print(foo)");
reporter.removeHandler(failFastHandler);
try {
invalidatePackages();
} catch (
@SuppressWarnings("InterruptedExceptionSwallowed")
Exception e) {
// Ignore any errors.
}
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent(
"File \"/workspace/tools/test_build_rules/test_prelude\", line 1, column 2, in"
+ " <toplevel>");
assertContainsEvent("Error: integer division by zero");
}
@Test
public void testPreludeWorksEvenWhenPreludePackageInError() throws Exception {
scratch.file(
"tools/test_build_rules/BUILD", //
"1//0"); // <-- dynamic error
scratch.file(
"tools/test_build_rules/test_prelude", //
"foo = 'FOO'");
scratch.file(
"pkg/BUILD", //
"print(foo)");
invalidatePackages();
// Succeeds because prelude loading is only dependent on the prelude package's existence, not
// its evaluation.
getConfiguredTarget("//pkg:BUILD");
assertContainsEvent("FOO");
}
// Another hypothetical test case we could try: Confirm that it's possible to explicitly load
// the prelude file as a regular .bzl. We don't bother testing this use case because, aside from
// being arguably pathological, it is currently impossible in practice: The prelude label
// doesn't end with ".bzl" and isn't configurable by the user. We also want to eliminate the
// prelude, so there's no intention of adding such a feature.
// Another possible test case: Verify how prelude applies to WORKSPACE files.
}
private static class CustomInMemoryFs extends InMemoryFileSystem {
private abstract static class FileStatusOrException {
abstract FileStatus get() throws IOException;
private static class ExceptionImpl extends FileStatusOrException {
private final IOException exn;
private ExceptionImpl(IOException exn) {
this.exn = exn;
}
@Override
FileStatus get() throws IOException {
throw exn;
}
}
private static class FileStatusImpl extends FileStatusOrException {
@Nullable private final FileStatus fileStatus;
private FileStatusImpl(@Nullable FileStatus fileStatus) {
this.fileStatus = fileStatus;
}
@Override
@Nullable
FileStatus get() {
return fileStatus;
}
}
}
private final Map<PathFragment, FileStatusOrException> stubbedStats = Maps.newHashMap();
private final Set<PathFragment> makeUnreadableAfterReaddir = Sets.newHashSet();
private final Map<PathFragment, IOException> pathsToErrorOnGetInputStream = Maps.newHashMap();
CustomInMemoryFs(ManualClock manualClock) {
super(manualClock, DigestHashFunction.SHA256);
}
void stubStat(Path path, @Nullable FileStatus stubbedResult) {
stubbedStats.put(path.asFragment(), new FileStatusOrException.FileStatusImpl(stubbedResult));
}
void stubStatError(Path path, IOException stubbedResult) {
stubbedStats.put(path.asFragment(), new FileStatusOrException.ExceptionImpl(stubbedResult));
}
@Override
public FileStatus statIfFound(PathFragment path, boolean followSymlinks) throws IOException {
if (stubbedStats.containsKey(path)) {
return stubbedStats.get(path).get();
}
return super.statIfFound(path, followSymlinks);
}
void scheduleMakeUnreadableAfterReaddir(Path path) {
makeUnreadableAfterReaddir.add(path.asFragment());
}
@Override
public Collection<Dirent> readdir(PathFragment path, boolean followSymlinks)
throws IOException {
Collection<Dirent> result = super.readdir(path, followSymlinks);
if (makeUnreadableAfterReaddir.contains(path)) {
setReadable(path, false);
}
return result;
}
void throwExceptionOnGetInputStream(Path path, IOException exn) {
pathsToErrorOnGetInputStream.put(path.asFragment(), exn);
}
@Override
public synchronized InputStream getInputStream(PathFragment path) throws IOException {
IOException exnToThrow = pathsToErrorOnGetInputStream.get(path);
if (exnToThrow != null) {
throw exnToThrow;
}
return super.getInputStream(path);
}
}
}