| // 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.truth.Truth.assertThat; | |
| import static com.google.devtools.build.skyframe.EvaluationResultSubjectFactory.assertThatEvaluationResult; | |
| import static org.junit.Assert.fail; | |
| import com.google.common.base.Optional; | |
| import com.google.common.base.Predicates; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.Maps; | |
| import com.google.common.collect.Sets; | |
| import com.google.devtools.build.lib.actions.FileStateValue; | |
| import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; | |
| import com.google.devtools.build.lib.clock.BlazeClock; | |
| import com.google.devtools.build.lib.cmdline.Label; | |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; | |
| import com.google.devtools.build.lib.packages.BuildFileNotFoundException; | |
| import com.google.devtools.build.lib.packages.ConstantRuleVisibility; | |
| import com.google.devtools.build.lib.packages.NoSuchPackageException; | |
| import com.google.devtools.build.lib.packages.NoSuchTargetException; | |
| import com.google.devtools.build.lib.packages.StarlarkSemanticsOptions; | |
| import com.google.devtools.build.lib.pkgcache.PackageCacheOptions; | |
| import com.google.devtools.build.lib.pkgcache.PathPackageLocator; | |
| import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction; | |
| import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils; | |
| import com.google.devtools.build.lib.testutil.ManualClock; | |
| import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor; | |
| 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.inmemoryfs.InMemoryFileSystem; | |
| import com.google.devtools.build.skyframe.ErrorInfo; | |
| import com.google.devtools.build.skyframe.EvaluationResult; | |
| 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 java.io.IOException; | |
| import java.io.InputStream; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collection; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.UUID; | |
| import javax.annotation.Nullable; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** | |
| * Unit tests of specific functionality of PackageFunction. Note that it's already tested indirectly | |
| * in several other places. | |
| */ | |
| @RunWith(JUnit4.class) | |
| public class PackageFunctionTest extends BuildViewTestCase { | |
| private CustomInMemoryFs fs = new CustomInMemoryFs(new ManualClock()); | |
| private void preparePackageLoading(Path... roots) { | |
| PackageCacheOptions packageCacheOptions = Options.getDefaults(PackageCacheOptions.class); | |
| packageCacheOptions.defaultVisibility = ConstantRuleVisibility.PUBLIC; | |
| packageCacheOptions.showLoadingProgress = true; | |
| packageCacheOptions.globbingThreads = 7; | |
| getSkyframeExecutor() | |
| .preparePackageLoading( | |
| new PathPackageLocator( | |
| outputBase, | |
| Arrays.stream(roots).map(Root::fromPath).collect(ImmutableList.toImmutableList()), | |
| BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY), | |
| packageCacheOptions, | |
| Options.getDefaults(StarlarkSemanticsOptions.class), | |
| UUID.randomUUID(), | |
| ImmutableMap.<String, String>of(), | |
| new TimestampGranularityMonitor(BlazeClock.instance())); | |
| skyframeExecutor.setActionEnv(ImmutableMap.<String, String>of()); | |
| } | |
| @Override | |
| protected FileSystem createFileSystem() { | |
| return fs; | |
| } | |
| private PackageValue validPackage(SkyKey skyKey) throws InterruptedException { | |
| SkyframeExecutor skyframeExecutor = getSkyframeExecutor(); | |
| skyframeExecutor.injectExtraPrecomputedValues( | |
| ImmutableList.of( | |
| PrecomputedValue.injected( | |
| RepositoryDelegatorFunction.RESOLVED_FILE_INSTEAD_OF_WORKSPACE, | |
| Optional.<RootedPath>absent()))); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| skyframeExecutor, skyKey, /*keepGoing=*/ false, reporter); | |
| if (result.hasError()) { | |
| fail(result.getError(skyKey).getException().getMessage()); | |
| } | |
| PackageValue value = result.get(skyKey); | |
| assertThat(value.getPackage().containsErrors()).isFalse(); | |
| return value; | |
| } | |
| @Test | |
| public void testValidPackage() throws Exception { | |
| scratch.file("pkg/BUILD"); | |
| validPackage(PackageValue.key(PackageIdentifier.parse("@//pkg"))); | |
| } | |
| @Test | |
| public void testPropagatesFilesystemInconsistencies() throws Exception { | |
| reporter.removeHandler(failFastHandler); | |
| RecordingDifferencer differencer = getSkyframeExecutor().getDifferencerForTesting(); | |
| Root pkgRoot = getSkyframeExecutor().getPathEntries().get(0); | |
| Path fooBuildFile = scratch.file("foo/BUILD"); | |
| Path fooDir = fooBuildFile.getParentDirectory(); | |
| // 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() throws IOException { | |
| return 0; | |
| } | |
| @Override | |
| public long getLastModifiedTime() throws IOException { | |
| return 0; | |
| } | |
| @Override | |
| public long getLastChangeTime() throws IOException { | |
| return 0; | |
| } | |
| @Override | |
| public long getNodeId() throws IOException { | |
| return 0; | |
| } | |
| }; | |
| fs.stubStat(fooBuildFile, inconsistentFileStatus); | |
| RootedPath pkgRootedPath = RootedPath.toRootedPath(pkgRoot, fooDir); | |
| SkyValue fooDirValue = FileStateValue.create(pkgRootedPath, tsgm); | |
| differencer.inject(ImmutableMap.of(FileStateValue.key(pkgRootedPath), fooDirValue)); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| String expectedMessage = | |
| "according to stat, existing path /workspace/foo/BUILD is neither" | |
| + " a file nor directory nor symlink."; | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| String errorMessage = errorInfo.getException().getMessage(); | |
| assertThat(errorMessage).contains("Inconsistent filesystem operations"); | |
| assertThat(errorMessage).contains(expectedMessage); | |
| } | |
| @Test | |
| public void testPropagatesFilesystemInconsistencies_Globbing() throws Exception { | |
| getSkyframeExecutor().turnOffSyscallCacheForTesting(); | |
| reporter.removeHandler(failFastHandler); | |
| RecordingDifferencer differencer = getSkyframeExecutor().getDifferencerForTesting(); | |
| Root pkgRoot = getSkyframeExecutor().getPathEntries().get(0); | |
| scratch.file( | |
| "foo/BUILD", | |
| "subinclude('//a:a')", | |
| "sh_library(name = 'foo', srcs = glob(['bar/**/baz.sh']))"); | |
| scratch.file("a/BUILD"); | |
| scratch.file("a/a"); | |
| Path bazFile = scratch.file("foo/bar/baz/baz.sh"); | |
| Path bazDir = bazFile.getParentDirectory(); | |
| Path barDir = bazDir.getParentDirectory(); | |
| // 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), | |
| DirectoryListingStateValue.create( | |
| ImmutableList.of(new Dirent("baz", Dirent.Type.DIRECTORY))))); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| String expectedMessage = "/workspace/foo/bar/baz is no longer an existing directory"; | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| String errorMessage = errorInfo.getException().getMessage(); | |
| assertThat(errorMessage).contains("Inconsistent filesystem operations"); | |
| assertThat(errorMessage).contains(expectedMessage); | |
| } | |
| /** Regression test for unexpected exception type from PackageValue. */ | |
| @Test | |
| public void testDiscrepancyBetweenLegacyAndSkyframePackageLoadingErrors() throws Exception { | |
| // Normally, legacy globbing and skyframe globbing share a cache for `readdir` filesystem calls. | |
| // In order to exercise a situation where they observe different results for filesystem calls, | |
| // we disable the cache. This might happen in a real scenario, e.g. if the cache hits a limit | |
| // and evicts entries. | |
| getSkyframeExecutor().turnOffSyscallCacheForTesting(); | |
| reporter.removeHandler(failFastHandler); | |
| Path fooBuildFile = | |
| scratch.file("foo/BUILD", "sh_library(name = 'foo', srcs = glob(['bar/*.sh']))"); | |
| Path fooDir = fooBuildFile.getParentDirectory(); | |
| Path barDir = fooDir.getRelative("bar"); | |
| scratch.file("foo/bar/baz.sh"); | |
| fs.scheduleMakeUnreadableAfterReaddir(barDir); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| String expectedMessage = "Encountered error 'Directory is not readable'"; | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| String errorMessage = errorInfo.getException().getMessage(); | |
| assertThat(errorMessage).contains("Inconsistent filesystem operations"); | |
| assertThat(errorMessage).contains(expectedMessage); | |
| } | |
| @SuppressWarnings("unchecked") // Cast of srcs attribute to Iterable<Label>. | |
| @Test | |
| public void testGlobOrderStable() throws Exception { | |
| scratch.file("foo/BUILD", "sh_library(name = 'foo', srcs = glob(['**/*.txt']))"); | |
| scratch.file("foo/b.txt"); | |
| scratch.file("foo/c/c.txt"); | |
| preparePackageLoading(rootDirectory); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| PackageValue value = validPackage(skyKey); | |
| assertThat( | |
| (Iterable<Label>) | |
| value | |
| .getPackage() | |
| .getTarget("foo") | |
| .getAssociatedRule() | |
| .getAttributeContainer() | |
| .getAttr("srcs")) | |
| .containsExactly( | |
| Label.parseAbsoluteUnchecked("//foo:b.txt"), | |
| Label.parseAbsoluteUnchecked("//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)); | |
| value = validPackage(skyKey); | |
| assertThat( | |
| (Iterable<Label>) | |
| value | |
| .getPackage() | |
| .getTarget("foo") | |
| .getAssociatedRule() | |
| .getAttributeContainer() | |
| .getAttr("srcs")) | |
| .containsExactly( | |
| Label.parseAbsoluteUnchecked("//foo:b.txt"), | |
| Label.parseAbsoluteUnchecked("//foo:c/c.txt"), | |
| Label.parseAbsoluteUnchecked("//foo:d.txt")) | |
| .inOrder(); | |
| } | |
| @Test | |
| public void testGlobOrderStableWithLegacyAndSkyframeComponents() throws Exception { | |
| scratch.file("foo/BUILD", "sh_library(name = 'foo', srcs = glob(['*.txt']))"); | |
| scratch.file("foo/b.txt"); | |
| scratch.file("foo/a.config"); | |
| preparePackageLoading(rootDirectory); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| assertSrcs(validPackage(skyKey), "foo", "//foo:b.txt"); | |
| scratch.overwriteFile( | |
| "foo/BUILD", "sh_library(name = 'foo', srcs = glob(['*.txt', '*.config']))"); | |
| getSkyframeExecutor() | |
| .invalidateFilesUnderPathForTesting( | |
| reporter, | |
| ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(), | |
| Root.fromPath(rootDirectory)); | |
| assertSrcs(validPackage(skyKey), "foo", "//foo:a.config", "//foo:b.txt"); | |
| scratch.overwriteFile( | |
| "foo/BUILD", "sh_library(name = 'foo', srcs = glob(['*.txt', '*.config'])) # comment"); | |
| getSkyframeExecutor() | |
| .invalidateFilesUnderPathForTesting( | |
| reporter, | |
| ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(), | |
| Root.fromPath(rootDirectory)); | |
| assertSrcs(validPackage(skyKey), "foo", "//foo:a.config", "//foo:b.txt"); | |
| getSkyframeExecutor().resetEvaluator(); | |
| PackageCacheOptions packageCacheOptions = Options.getDefaults(PackageCacheOptions.class); | |
| packageCacheOptions.defaultVisibility = ConstantRuleVisibility.PUBLIC; | |
| packageCacheOptions.showLoadingProgress = true; | |
| packageCacheOptions.globbingThreads = 7; | |
| getSkyframeExecutor() | |
| .preparePackageLoading( | |
| new PathPackageLocator( | |
| outputBase, | |
| ImmutableList.of(Root.fromPath(rootDirectory)), | |
| BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY), | |
| packageCacheOptions, | |
| Options.getDefaults(StarlarkSemanticsOptions.class), | |
| UUID.randomUUID(), | |
| ImmutableMap.<String, String>of(), | |
| tsgm); | |
| getSkyframeExecutor().setActionEnv(ImmutableMap.<String, String>of()); | |
| assertSrcs(validPackage(skyKey), "foo", "//foo:a.config", "//foo:b.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() throws Exception { | |
| scratch.file( | |
| "foo/BUILD", | |
| "sh_library(name = 'foo', srcs = glob(['*.sh']))", | |
| "sh_library(name = 'bar', srcs = glob(['link.sh']))", | |
| "sh_library(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(rootDirectory); | |
| SkyKey fooKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| PackageValue fooValue = validPackage(fooKey); | |
| assertSrcs(fooValue, "foo", "//foo:link.sh", "//foo:ordinary.sh"); | |
| assertSrcs(fooValue, "bar", "//foo:link.sh"); | |
| assertSrcs(fooValue, "baz", "//foo:subdir_link/target.txt"); | |
| scratch.overwriteFile( | |
| "foo/BUILD", | |
| "sh_library(name = 'foo', srcs = glob(['*.sh'])) #comment", | |
| "sh_library(name = 'bar', srcs = glob(['link.sh']))", | |
| "sh_library(name = 'baz', srcs = glob(['subdir_link/*.txt']))"); | |
| getSkyframeExecutor() | |
| .invalidateFilesUnderPathForTesting( | |
| reporter, | |
| ModifiedFileSet.builder().modify(PathFragment.create("foo/BUILD")).build(), | |
| Root.fromPath(rootDirectory)); | |
| PackageValue fooValue2 = validPackage(fooKey); | |
| assertThat(fooValue2).isNotEqualTo(fooValue); | |
| assertSrcs(fooValue2, "foo", "//foo:link.sh", "//foo:ordinary.sh"); | |
| assertSrcs(fooValue2, "bar", "//foo:link.sh"); | |
| assertSrcs(fooValue2, "baz", "//foo:subdir_link/target.txt"); | |
| } | |
| private static void assertSrcs(PackageValue value, String targetName, String... expected) | |
| throws NoSuchTargetException { | |
| List<Label> expectedLabels = new ArrayList<>(); | |
| for (String item : expected) { | |
| expectedLabels.add(Label.parseAbsoluteUnchecked(item)); | |
| } | |
| assertThat(getSrcs(value, targetName)).containsExactlyElementsIn(expectedLabels).inOrder(); | |
| } | |
| @SuppressWarnings("unchecked") | |
| private static Iterable<Label> getSrcs(PackageValue packageValue, String targetName) | |
| throws NoSuchTargetException { | |
| return (Iterable<Label>) | |
| packageValue | |
| .getPackage() | |
| .getTarget(targetName) | |
| .getAssociatedRule() | |
| .getAttributeContainer() | |
| .getAttr("srcs"); | |
| } | |
| @Test | |
| public void testOneNewElementInMultipleGlob() throws Exception { | |
| scratch.file( | |
| "foo/BUILD", | |
| "sh_library(name = 'foo', srcs = glob(['*.sh']))", | |
| "sh_library(name = 'bar', srcs = glob(['*.sh', '*.txt']))"); | |
| preparePackageLoading(rootDirectory); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| PackageValue value = validPackage(skyKey); | |
| scratch.file("foo/irrelevent"); | |
| getSkyframeExecutor() | |
| .invalidateFilesUnderPathForTesting( | |
| reporter, | |
| ModifiedFileSet.builder().modify(PathFragment.create("foo/irrelevant")).build(), | |
| Root.fromPath(rootDirectory)); | |
| assertThat(validPackage(skyKey)).isSameAs(value); | |
| } | |
| @Test | |
| public void testNoNewElementInMultipleGlob() throws Exception { | |
| scratch.file( | |
| "foo/BUILD", | |
| "sh_library(name = 'foo', srcs = glob(['*.sh', '*.txt']))", | |
| "sh_library(name = 'bar', srcs = glob(['*.sh', '*.txt']))"); | |
| preparePackageLoading(rootDirectory); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| PackageValue value = validPackage(skyKey); | |
| scratch.file("foo/irrelevent"); | |
| getSkyframeExecutor() | |
| .invalidateFilesUnderPathForTesting( | |
| reporter, | |
| ModifiedFileSet.builder().modify(PathFragment.create("foo/irrelevant")).build(), | |
| Root.fromPath(rootDirectory)); | |
| assertThat(validPackage(skyKey)).isSameAs(value); | |
| } | |
| @Test | |
| public void testTransitiveSkylarkDepsStoredInPackage() throws Exception { | |
| scratch.file("foo/BUILD", "load('//bar:ext.bzl', 'a')"); | |
| scratch.file("bar/BUILD"); | |
| scratch.file("bar/ext.bzl", "load('//baz:ext.bzl', 'b')", "a = b"); | |
| scratch.file("baz/BUILD"); | |
| scratch.file("baz/ext.bzl", "b = 1"); | |
| scratch.file("qux/BUILD"); | |
| scratch.file("qux/ext.bzl", "c = 1"); | |
| preparePackageLoading(rootDirectory); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| PackageValue value = validPackage(skyKey); | |
| assertThat(value.getPackage().getSkylarkFileDependencies()) | |
| .containsExactly( | |
| Label.parseAbsolute("//bar:ext.bzl", ImmutableMap.of()), | |
| Label.parseAbsolute("//baz:ext.bzl", ImmutableMap.of())); | |
| 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)); | |
| value = validPackage(skyKey); | |
| assertThat(value.getPackage().getSkylarkFileDependencies()) | |
| .containsExactly( | |
| Label.parseAbsolute("//bar:ext.bzl", ImmutableMap.of()), | |
| Label.parseAbsolute("//qux:ext.bzl", ImmutableMap.of())); | |
| } | |
| @Test | |
| public void testNonExistingSkylarkExtension() throws Exception { | |
| reporter.removeHandler(failFastHandler); | |
| scratch.file( | |
| "test/skylark/BUILD", | |
| "load('//test/skylark:bad_extension.bzl', 'some_symbol')", | |
| "genrule(name = gr,", | |
| " outs = ['out.txt'],", | |
| " cmd = 'echo hello >@')"); | |
| invalidatePackages(); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//test/skylark")); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| String expectedMsg = | |
| "error loading package 'test/skylark': " | |
| + "Unable to load file '//test/skylark:bad_extension.bzl': file doesn't exist"; | |
| assertThat(errorInfo.getException()).hasMessageThat().isEqualTo(expectedMsg); | |
| } | |
| @Test | |
| public void testNonExistingSkylarkExtensionFromExtension() throws Exception { | |
| reporter.removeHandler(failFastHandler); | |
| scratch.file( | |
| "test/skylark/extension.bzl", | |
| "load('//test/skylark:bad_extension.bzl', 'some_symbol')", | |
| "a = 'a'"); | |
| scratch.file( | |
| "test/skylark/BUILD", | |
| "load('//test/skylark:extension.bzl', 'a')", | |
| "genrule(name = gr,", | |
| " outs = ['out.txt'],", | |
| " cmd = 'echo hello >@')"); | |
| invalidatePackages(); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//test/skylark")); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| assertThat(errorInfo.getException()) | |
| .hasMessageThat() | |
| .isEqualTo( | |
| "error loading package 'test/skylark': " | |
| + "in /workspace/test/skylark/extension.bzl: " | |
| + "Unable to load file '//test/skylark:bad_extension.bzl': file doesn't exist"); | |
| } | |
| @Test | |
| public void testSymlinkCycleWithSkylarkExtension() throws Exception { | |
| reporter.removeHandler(failFastHandler); | |
| Path extensionFilePath = scratch.resolve("/workspace/test/skylark/extension.bzl"); | |
| FileSystemUtils.ensureSymbolicLink(extensionFilePath, PathFragment.create("extension.bzl")); | |
| scratch.file( | |
| "test/skylark/BUILD", | |
| "load('//test/skylark:extension.bzl', 'a')", | |
| "genrule(name = gr,", | |
| " outs = ['out.txt'],", | |
| " cmd = 'echo hello >@')"); | |
| invalidatePackages(); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//test/skylark")); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| assertThat(errorInfo.getRootCauseOfException()).isEqualTo(skyKey); | |
| assertThat(errorInfo.getException()) | |
| .hasMessageThat() | |
| .isEqualTo( | |
| "error loading package 'test/skylark': Encountered error while reading extension " | |
| + "file 'test/skylark/extension.bzl': Symlink cycle"); | |
| } | |
| @Test | |
| public void testIOErrorLookingForSubpackageForLabelIsHandled() throws Exception { | |
| reporter.removeHandler(failFastHandler); | |
| scratch.file("foo/BUILD", "sh_library(name = 'foo', srcs = ['bar/baz.sh'])"); | |
| Path barBuildFile = scratch.file("foo/bar/BUILD"); | |
| fs.stubStatError(barBuildFile, new IOException("nope")); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isTrue(); | |
| assertContainsEvent("nope"); | |
| } | |
| @Test | |
| public void testLoadRelativePath() throws Exception { | |
| scratch.file("pkg/BUILD", "load(':ext.bzl', 'a')"); | |
| scratch.file("pkg/ext.bzl", "a = 1"); | |
| validPackage(PackageValue.key(PackageIdentifier.parse("@//pkg"))); | |
| } | |
| @Test | |
| public void testLoadAbsolutePath() throws Exception { | |
| scratch.file("pkg1/BUILD"); | |
| scratch.file("pkg2/BUILD", "load('//pkg1:ext.bzl', 'a')"); | |
| scratch.file("pkg1/ext.bzl", "a = 1"); | |
| validPackage(PackageValue.key(PackageIdentifier.parse("@//pkg2"))); | |
| } | |
| @Test | |
| public void testBadWorkspaceFile() throws Exception { | |
| Path workspacePath = scratch.overwriteFile("WORKSPACE", "junk"); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.createInMainRepo("external")); | |
| getSkyframeExecutor() | |
| .invalidate( | |
| Predicates.equalTo( | |
| FileStateValue.key( | |
| RootedPath.toRootedPath( | |
| Root.fromPath(workspacePath.getParentDirectory()), | |
| PathFragment.create(workspacePath.getBaseName()))))); | |
| reporter.removeHandler(failFastHandler); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isFalse(); | |
| assertThat(result.get(skyKey).getPackage().containsErrors()).isTrue(); | |
| } | |
| // Regression test for the two ugly consequences of a bug where GlobFunction incorrectly matched | |
| // dangling symlinks. | |
| @Test | |
| public void testIncrementalSkyframeHybridGlobbingOnDanglingSymlink() 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(rootDirectory); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| PackageValue value = validPackage(skyKey); | |
| assertThat(value.getPackage().containsErrors()).isFalse(); | |
| assertThat(value.getPackage().getTarget("existing.txt").getName()).isEqualTo("existing.txt"); | |
| try { | |
| value.getPackage().getTarget("dangling.txt"); | |
| fail(); | |
| } catch (NoSuchTargetException expected) { | |
| } | |
| 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)); | |
| value = validPackage(skyKey); | |
| assertThat(value.getPackage().containsErrors()).isFalse(); | |
| assertThat(value.getPackage().getTarget("existing.txt").getName()).isEqualTo("existing.txt"); | |
| try { | |
| value.getPackage().getTarget("dangling.txt"); | |
| fail(); | |
| } catch (NoSuchTargetException expected) { | |
| // 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)); | |
| PackageValue newValue = validPackage(skyKey); | |
| assertThat(newValue.getPackage().containsErrors()).isFalse(); | |
| assertThat(newValue.getPackage().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(newValue.getPackage().getTarget("dangling.txt").getName()).isEqualTo("dangling.txt"); | |
| assertThat(newValue.getPackage()).isNotSameAs(value.getPackage()); | |
| } | |
| // 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 legacy globbing and skyframe globbing). | |
| @Test | |
| public void testRecursiveGlobNeverMatchesPackageDirectory() throws Exception { | |
| scratch.file( | |
| "foo/BUILD", | |
| "[sh_library(name = x + '-matched') for x in glob(['**'], exclude_directories = 0)]"); | |
| scratch.file("foo/bar"); | |
| preparePackageLoading(rootDirectory); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| PackageValue value = validPackage(skyKey); | |
| assertThat(value.getPackage().containsErrors()).isFalse(); | |
| assertThat(value.getPackage().getTarget("bar-matched").getName()).isEqualTo("bar-matched"); | |
| try { | |
| value.getPackage().getTarget("-matched"); | |
| fail(); | |
| } catch (NoSuchTargetException expected) { | |
| } | |
| scratch.overwriteFile( | |
| "foo/BUILD", | |
| "[sh_library(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)); | |
| value = validPackage(skyKey); | |
| assertThat(value.getPackage().containsErrors()).isFalse(); | |
| assertThat(value.getPackage().getTarget("bar-matched").getName()).isEqualTo("bar-matched"); | |
| try { | |
| value.getPackage().getTarget("-matched"); | |
| fail(); | |
| } catch (NoSuchTargetException expected) { | |
| } | |
| } | |
| @Test | |
| public void testPackageLoadingErrorOnIOExceptionReadingBuildFile() throws Exception { | |
| Path fooBuildFilePath = scratch.file("foo/BUILD"); | |
| IOException exn = new IOException("nope"); | |
| fs.throwExceptionOnGetInputStream(fooBuildFilePath, exn); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| String errorMessage = errorInfo.getException().getMessage(); | |
| assertThat(errorMessage).contains("nope"); | |
| assertThat(errorInfo.getException()).isInstanceOf(NoSuchPackageException.class); | |
| assertThat(errorInfo.getException()).hasCauseThat().isInstanceOf(IOException.class); | |
| } | |
| @Test | |
| public void testPackageLoadingErrorOnIOExceptionReadingBzlFile() 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); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo")); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| String errorMessage = errorInfo.getException().getMessage(); | |
| assertThat(errorMessage).contains("nope"); | |
| assertThat(errorInfo.getException()).isInstanceOf(NoSuchPackageException.class); | |
| assertThat(errorInfo.getException()).hasCauseThat().isInstanceOf(IOException.class); | |
| } | |
| @Test | |
| public void testLabelsCrossesSubpackageBoundaries() throws Exception { | |
| reporter.removeHandler(failFastHandler); | |
| scratch.file("pkg/BUILD", "exports_files(['sub/blah'])"); | |
| scratch.file("pkg/sub/BUILD"); | |
| invalidatePackages(); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//pkg")); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThatEvaluationResult(result).hasNoError(); | |
| assertThat(result.get(skyKey).getPackage().containsErrors()).isTrue(); | |
| assertContainsEvent("Label '//pkg:sub/blah' crosses boundary of subpackage 'pkg/sub'"); | |
| } | |
| @Test | |
| public void testSymlinkCycleEncounteredWhileHandlingLabelCrossingSubpackageBoundaries() | |
| throws Exception { | |
| reporter.removeHandler(failFastHandler); | |
| scratch.file("pkg/BUILD", "exports_files(['sub/blah'])"); | |
| Path subBuildFilePath = scratch.dir("pkg/sub").getChild("BUILD"); | |
| FileSystemUtils.ensureSymbolicLink(subBuildFilePath, subBuildFilePath); | |
| invalidatePackages(); | |
| SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//pkg")); | |
| EvaluationResult<PackageValue> result = | |
| SkyframeExecutorTestUtils.evaluate( | |
| getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); | |
| assertThatEvaluationResult(result).hasError(); | |
| assertThatEvaluationResult(result) | |
| .hasErrorEntryForKeyThat(skyKey) | |
| .hasExceptionThat() | |
| .isInstanceOf(BuildFileNotFoundException.class); | |
| assertThatEvaluationResult(result) | |
| .hasErrorEntryForKeyThat(skyKey) | |
| .hasExceptionThat() | |
| .hasMessageThat() | |
| .contains( | |
| "no such package 'pkg/sub': Symlink cycle detected while trying to find BUILD file"); | |
| assertContainsEvent("circular symlinks detected"); | |
| } | |
| 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<Path, FileStatusOrException> stubbedStats = Maps.newHashMap(); | |
| private final Set<Path> makeUnreadableAfterReaddir = Sets.newHashSet(); | |
| private final Map<Path, IOException> pathsToErrorOnGetInputStream = Maps.newHashMap(); | |
| public CustomInMemoryFs(ManualClock manualClock) { | |
| super(manualClock); | |
| } | |
| public void stubStat(Path path, @Nullable FileStatus stubbedResult) { | |
| stubbedStats.put(path, new FileStatusOrException.FileStatusImpl(stubbedResult)); | |
| } | |
| public void stubStatError(Path path, IOException stubbedResult) { | |
| stubbedStats.put(path, new FileStatusOrException.ExceptionImpl(stubbedResult)); | |
| } | |
| @Override | |
| public FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException { | |
| if (stubbedStats.containsKey(path)) { | |
| return stubbedStats.get(path).get(); | |
| } | |
| return super.statIfFound(path, followSymlinks); | |
| } | |
| public void scheduleMakeUnreadableAfterReaddir(Path path) { | |
| makeUnreadableAfterReaddir.add(path); | |
| } | |
| @Override | |
| public Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException { | |
| Collection<Dirent> result = super.readdir(path, followSymlinks); | |
| if (makeUnreadableAfterReaddir.contains(path)) { | |
| path.setReadable(false); | |
| } | |
| return result; | |
| } | |
| public void throwExceptionOnGetInputStream(Path path, IOException exn) { | |
| pathsToErrorOnGetInputStream.put(path, exn); | |
| } | |
| @Override | |
| protected InputStream getInputStream(Path path) throws IOException { | |
| IOException exnToThrow = pathsToErrorOnGetInputStream.get(path); | |
| if (exnToThrow != null) { | |
| throw exnToThrow; | |
| } | |
| return super.getInputStream(path); | |
| } | |
| } | |
| } |