diff options
author | 2015-11-16 23:19:13 +0000 | |
---|---|---|
committer | 2015-11-17 10:52:56 +0000 | |
commit | 335f0672e981d66a32ffac70d5b6aa58254a80c7 (patch) | |
tree | a2699f37c385cf47901b45acf74ed9285208d244 /src/test/java/com/google | |
parent | 1648470c7fe1048e23fadcd02d6e81c693f52259 (diff) |
Open source skyframe tests
--
MOS_MIGRATED_REVID=107983315
Diffstat (limited to 'src/test/java/com/google')
9 files changed, 2480 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD index f3d0b6a303..888f231986 100644 --- a/src/test/java/com/google/devtools/build/lib/BUILD +++ b/src/test/java/com/google/devtools/build/lib/BUILD @@ -59,10 +59,12 @@ java_library( "//src/main/java/com/google/devtools/build/lib/actions", "//src/main/java/com/google/devtools/build/skyframe", "//src/test/java/com/google/devtools/build/lib:packages_testutil", + "//src/test/java/com/google/devtools/build/skyframe:testutil", "//third_party:guava", "//third_party:guava-testlib", "//third_party:jsr305", "//third_party:junit4", + "//third_party:mockito", "//third_party:truth", ], ) @@ -99,6 +101,7 @@ java_test( "//third_party:guava-testlib", "//third_party:jsr305", "//third_party:junit4", + "//third_party:mockito", "//third_party:truth", ], ) diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java new file mode 100644 index 0000000000..8fe6c9dfbf --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java @@ -0,0 +1,557 @@ +// 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 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.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.packages.ConstantRuleVisibility; +import com.google.devtools.build.lib.packages.Preprocessor; +import com.google.devtools.build.lib.packages.util.SubincludePreprocessor; +import com.google.devtools.build.lib.pkgcache.PathPackageLocator; +import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils; +import com.google.devtools.build.lib.testutil.ManualClock; +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.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 java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.annotation.Nullable; + +/** + * Unit tests of specific functionality of PackageFunction. Note that it's already tested + * indirectly in several other places. + */ +public class PackageFunctionTest extends BuildViewTestCase { + + private CustomInMemoryFs fs = new CustomInMemoryFs(new ManualClock()); + + @Override + protected Preprocessor.Factory.Supplier getPreprocessorFactorySupplier() { + return new SubincludePreprocessor.FactorySupplier(scratch.getFileSystem()); + } + + @Override + protected FileSystem createFileSystem() { + return fs; + } + + private PackageValue validPackage(SkyKey skyKey) throws InterruptedException { + EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate( + getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter); + if (result.hasError()) { + fail(result.getError(skyKey).getException().getMessage()); + } + PackageValue value = result.get(skyKey); + assertFalse(value.getPackage().containsErrors()); + return value; + } + + public void testInconsistentNewPackage() throws Exception { + scratch.file("pkg/BUILD", "subinclude('//foo:sub')"); + scratch.file("foo/sub"); + + getSkyframeExecutor().preparePackageLoading( + new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)), + ConstantRuleVisibility.PUBLIC, true, + 7, "", UUID.randomUUID()); + + SkyKey pkgLookupKey = PackageLookupValue.key(new PathFragment("foo")); + EvaluationResult<PackageLookupValue> result = SkyframeExecutorTestUtils.evaluate( + getSkyframeExecutor(), pkgLookupKey, /*keepGoing=*/false, reporter); + assertFalse(result.hasError()); + assertFalse(result.get(pkgLookupKey).packageExists()); + + scratch.file("foo/BUILD"); + + SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("pkg")); + result = SkyframeExecutorTestUtils.evaluate(getSkyframeExecutor(), + skyKey, /*keepGoing=*/false, reporter); + assertTrue(result.hasError()); + Throwable exception = result.getError(skyKey).getException(); + assertThat(exception.getMessage()).contains("Inconsistent filesystem operations"); + assertThat(exception.getMessage()).contains("Unexpected package"); + } + + public void testInconsistentMissingPackage() throws Exception { + reporter.removeHandler(failFastHandler); + Path root1 = fs.getPath("/root1"); + scratch.file("/root1/WORKSPACE"); + scratch.file("/root1/foo/sub"); + scratch.file("/root1/pkg/BUILD", "subinclude('//foo:sub')"); + + Path root2 = fs.getPath("/root2"); + scratch.file("/root2/foo/BUILD"); + scratch.file("/root2/foo/sub"); + + getSkyframeExecutor().preparePackageLoading( + new PathPackageLocator(outputBase, ImmutableList.of(root1, root2)), + ConstantRuleVisibility.PUBLIC, true, + 7, "", UUID.randomUUID()); + + SkyKey pkgLookupKey = PackageLookupValue.key(PackageIdentifier.parse("foo")); + EvaluationResult<PackageLookupValue> result = SkyframeExecutorTestUtils.evaluate( + getSkyframeExecutor(), pkgLookupKey, /*keepGoing=*/false, reporter); + assertFalse(result.hasError()); + assertEquals(root2, result.get(pkgLookupKey).getRoot()); + + scratch.file("/root1/foo/BUILD"); + + SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("pkg")); + result = SkyframeExecutorTestUtils.evaluate(getSkyframeExecutor(), + skyKey, /*keepGoing=*/false, reporter); + assertTrue(result.hasError()); + Throwable exception = result.getError(skyKey).getException(); + System.out.println("exception: " + exception.getMessage()); + assertThat(exception.getMessage()).contains("Inconsistent filesystem operations"); + assertThat(exception.getMessage()).contains("Inconsistent package location"); + } + + public void testPropagatesFilesystemInconsistencies() throws Exception { + reporter.removeHandler(failFastHandler); + RecordingDifferencer differencer = getSkyframeExecutor().getDifferencerForTesting(); + Path pkgRoot = getSkyframeExecutor().getPathEntries().get(0); + Path fooBuildFile = scratch.file("foo/BUILD"); + Path fooDir = fooBuildFile.getParentDirectory(); + + // Our custom filesystem says "foo/BUILD" exists but its parent "foo" is a file. + FileStatus inconsistentParentFileStatus = new FileStatus() { + @Override + public boolean isFile() { + return true; + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public boolean isSymbolicLink() { + return false; + } + + @Override + public boolean isSpecialFile() { + return false; + } + + @Override + public long getSize() throws IOException { + 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(fooDir, inconsistentParentFileStatus); + RootedPath pkgRootedPath = RootedPath.toRootedPath(pkgRoot, fooDir); + SkyValue fooDirValue = FileStateValue.create(pkgRootedPath, + getSkyframeExecutor().getTimestampGranularityMonitorForTesting()); + differencer.inject(ImmutableMap.of(FileStateValue.key(pkgRootedPath), fooDirValue)); + SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo")); + String expectedMessage = "/workspace/foo/BUILD exists but its parent path /workspace/foo isn't " + + "an existing directory"; + EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate( + getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter); + assertTrue(result.hasError()); + ErrorInfo errorInfo = result.getError(skyKey); + String errorMessage = errorInfo.getException().getMessage(); + assertThat(errorMessage).contains("Inconsistent filesystem operations"); + assertThat(errorMessage).contains(expectedMessage); + } + + public void testPropagatesFilesystemInconsistencies_Globbing() throws Exception { + reporter.removeHandler(failFastHandler); + RecordingDifferencer differencer = getSkyframeExecutor().getDifferencerForTesting(); + Path 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(); + + long bazFileNodeId = bazFile.stat().getNodeId(); + // 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); + FileStateValue barDirFileStateValue = FileStateValue.create(barDirRootedPath, + getSkyframeExecutor().getTimestampGranularityMonitorForTesting()); + FileValue barDirFileValue = FileValue.value(barDirRootedPath, barDirFileStateValue, + barDirRootedPath, barDirFileStateValue); + DirectoryListingValue barDirListing = DirectoryListingValue.value(barDirRootedPath, + barDirFileValue, DirectoryListingStateValue.create(ImmutableList.of( + new Dirent("baz", Dirent.Type.DIRECTORY)))); + differencer.inject(ImmutableMap.of(DirectoryListingValue.key(barDirRootedPath), barDirListing)); + SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo")); + String expectedMessage = "Some filesystem operations implied /workspace/foo/bar/baz/baz.sh was " + + "a regular file with size of 0 and mtime of 0 and nodeId of " + bazFileNodeId + " and " + + "mtime of 0 but others made us think it was a nonexistent path"; + EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate( + getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter); + assertTrue(result.hasError()); + 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. */ + public void testDiscrepancyBetweenLegacyAndSkyframePackageLoadingErrors() throws Exception { + 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); + assertTrue(result.hasError()); + ErrorInfo errorInfo = result.getError(skyKey); + String errorMessage = errorInfo.getException().getMessage(); + assertThat(errorMessage).contains("Inconsistent filesystem operations"); + assertThat(errorMessage).contains(expectedMessage); + } + + public void testMultipleSubincludesFromSamePackage() throws Exception { + scratch.file("foo/BUILD", + "subinclude('//bar:a')", + "subinclude('//bar:b')"); + scratch.file("bar/BUILD", + "exports_files(['a', 'b'])"); + scratch.file("bar/a"); + scratch.file("bar/b"); + + getSkyframeExecutor().preparePackageLoading( + new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)), + ConstantRuleVisibility.PUBLIC, true, + 7, "", UUID.randomUUID()); + + SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo")); + validPackage(skyKey); + } + + public void testTransitiveSubincludesStoredInPackage() throws Exception { + scratch.file("foo/BUILD", + "subinclude('//bar:a')"); + scratch.file("bar/BUILD", + "exports_files(['a'])"); + scratch.file("bar/a", + "subinclude('//baz:b')"); + scratch.file("baz/BUILD", + "exports_files(['b', 'c'])"); + scratch.file("baz/b"); + scratch.file("baz/c"); + + getSkyframeExecutor().preparePackageLoading( + new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)), + ConstantRuleVisibility.PUBLIC, true, + 7, "", UUID.randomUUID()); + + SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo")); + PackageValue value = validPackage(skyKey); + assertThat(value.getPackage().getSubincludeLabels()).containsExactly( + Label.parseAbsolute("//bar:a"), Label.parseAbsolute("//baz:b")); + + scratch.overwriteFile("bar/a", + "subinclude('//baz:c')"); + getSkyframeExecutor().invalidateFilesUnderPathForTesting(reporter, + ModifiedFileSet.builder().modify(new PathFragment("bar/a")).build(), rootDirectory); + + value = validPackage(skyKey); + assertThat(value.getPackage().getSubincludeLabels()).containsExactly( + Label.parseAbsolute("//bar:a"), Label.parseAbsolute("//baz:c")); + } + + public void testTransitiveSkylarkDepsStoredInPackage() throws Exception { + scratch.file("foo/BUILD", + "load('/bar/ext', 'a')"); + scratch.file("bar/BUILD"); + scratch.file("bar/ext.bzl", + "load('/baz/ext', '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"); + + getSkyframeExecutor().preparePackageLoading( + new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)), + ConstantRuleVisibility.PUBLIC, true, + 7, "", UUID.randomUUID()); + + SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo")); + PackageValue value = validPackage(skyKey); + assertThat(value.getPackage().getSkylarkFileDependencies()).containsExactly( + Label.parseAbsolute("//bar:ext.bzl"), Label.parseAbsolute("//baz:ext.bzl")); + + scratch.overwriteFile("bar/ext.bzl", + "load('/qux/ext', 'c')", + "a = c"); + getSkyframeExecutor().invalidateFilesUnderPathForTesting(reporter, + ModifiedFileSet.builder().modify(new PathFragment("bar/ext.bzl")).build(), rootDirectory); + + value = validPackage(skyKey); + assertThat(value.getPackage().getSkylarkFileDependencies()).containsExactly( + Label.parseAbsolute("//bar:ext.bzl"), Label.parseAbsolute("//qux:ext.bzl")); + } + + public void testNonExistingSkylarkExtension() throws Exception { + reporter.removeHandler(failFastHandler); + scratch.file("test/skylark/BUILD", + "load('/test/skylark/bad_extension', '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); + assertTrue(result.hasError()); + ErrorInfo errorInfo = result.getError(skyKey); + assertThat(errorInfo.getException()) + .hasMessage("error loading package 'test/skylark': Extension file not found. " + + "Unable to load file '//test/skylark:bad_extension.bzl': " + + "file doesn't exist or isn't a file"); + } + + public void testNonExistingSkylarkExtensionWithPythonPreprocessing() throws Exception { + reporter.removeHandler(failFastHandler); + scratch.file("foo/BUILD", + "exports_files(['a'])"); + scratch.file("foo/a", + "load('/test/skylark/bad_extension', 'some_symbol')"); + scratch.file("test/skylark/BUILD", + "subinclude('//foo:a')"); + invalidatePackages(); + + SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("test/skylark")); + EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate( + getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter); + assertTrue(result.hasError()); + assertContainsEvent("Extension file not found. " + + "Unable to load file '//test/skylark:bad_extension.bzl': " + + "file doesn't exist or isn't a file"); + } + + public void testNonExistingSkylarkExtensionFromExtension() throws Exception { + reporter.removeHandler(failFastHandler); + scratch.file("test/skylark/extension.bzl", + "load('/test/skylark/bad_extension', 'some_symbol')", + "a = 'a'"); + scratch.file("test/skylark/BUILD", + "load('/test/skylark/extension', '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); + assertTrue(result.hasError()); + ErrorInfo errorInfo = result.getError(skyKey); + assertThat(errorInfo.getException()) + .hasMessage("error loading package 'test/skylark': Extension file not found. " + + "Unable to load file '//test/skylark:bad_extension.bzl': " + + "file doesn't exist or isn't a file"); + } + + public void testSymlinkCycleWithSkylarkExtension() throws Exception { + reporter.removeHandler(failFastHandler); + Path extensionFilePath = scratch.resolve("/workspace/test/skylark/extension.bzl"); + FileSystemUtils.ensureSymbolicLink(extensionFilePath, new PathFragment("extension.bzl")); + scratch.file("test/skylark/BUILD", + "load('/test/skylark/extension', '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); + assertTrue(result.hasError()); + ErrorInfo errorInfo = result.getError(skyKey); + assertEquals(skyKey, errorInfo.getRootCauseOfException()); + assertThat(errorInfo.getException()) + .hasMessage( + "error loading package 'test/skylark': Encountered error while reading extension " + + "file 'test/skylark/extension.bzl': Symlink cycle"); + } + + 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); + assertTrue(result.hasError()); + assertContainsEvent("nope"); + } + + public void testLoadRelativePath() throws Exception { + scratch.file("pkg/BUILD", "load('ext', 'a')"); + scratch.file("pkg/ext.bzl", "a = 1"); + validPackage(PackageValue.key(PackageIdentifier.parse("pkg"))); + } + + public void testLoadAbsolutePath() throws Exception { + scratch.file("pkg1/BUILD"); + scratch.file("pkg2/BUILD", + "load('/pkg1/ext', 'a')"); + scratch.file("pkg1/ext.bzl", "a = 1"); + validPackage(PackageValue.key(PackageIdentifier.parse("pkg2"))); + } + + public void testBadWorkspaceFile() throws Exception { + Path workspacePath = scratch.overwriteFile("WORKSPACE", "junk"); + SkyKey skyKey = PackageValue.key(PackageIdentifier.createInDefaultRepo("external")); + getSkyframeExecutor() + .invalidate( + Predicates.equalTo( + FileStateValue.key( + RootedPath.toRootedPath( + workspacePath.getParentDirectory(), + new PathFragment(workspacePath.getBaseName()))))); + + reporter.removeHandler(failFastHandler); + EvaluationResult<PackageValue> result = + SkyframeExecutorTestUtils.evaluate( + getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter); + assertFalse(result.hasError()); + assertTrue(result.get(skyKey).getPackage().containsErrors()); + } + + 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 Map<Path, FileStatusOrException> stubbedStats = Maps.newHashMap(); + private Set<Path> makeUnreadableAfterReaddir = Sets.newHashSet(); + + 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 stat(Path path, boolean followSymlinks) throws IOException { + if (stubbedStats.containsKey(path)) { + return stubbedStats.get(path).get(); + } + return super.stat(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; + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PackageLookupFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PackageLookupFunctionTest.java new file mode 100644 index 0000000000..dfc1c4450b --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/PackageLookupFunctionTest.java @@ -0,0 +1,231 @@ +// 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 com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.testing.EqualsTester; +import com.google.devtools.build.lib.analysis.BlazeDirectories; +import com.google.devtools.build.lib.bazel.rules.BazelRulesModule; +import com.google.devtools.build.lib.cmdline.PackageIdentifier; +import com.google.devtools.build.lib.events.NullEventHandler; +import com.google.devtools.build.lib.packages.PackageFactory; +import com.google.devtools.build.lib.packages.RuleClassProvider; +import com.google.devtools.build.lib.pkgcache.PathPackageLocator; +import com.google.devtools.build.lib.skyframe.PackageLookupValue.ErrorReason; +import com.google.devtools.build.lib.testutil.FoundationTestCase; +import com.google.devtools.build.lib.testutil.TestRuleClassProvider; +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.RootedPath; +import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator; +import com.google.devtools.build.skyframe.MemoizingEvaluator; +import com.google.devtools.build.skyframe.RecordingDifferencer; +import com.google.devtools.build.skyframe.SequentialBuildDriver; +import com.google.devtools.build.skyframe.SkyFunction; +import com.google.devtools.build.skyframe.SkyFunctionName; +import com.google.devtools.build.skyframe.SkyKey; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Tests for {@link PackageLookupFunction}. + */ +public class PackageLookupFunctionTest extends FoundationTestCase { + private AtomicReference<ImmutableSet<PackageIdentifier>> deletedPackages; + private MemoizingEvaluator evaluator; + private SequentialBuildDriver driver; + private RecordingDifferencer differencer; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Path emptyPackagePath = rootDirectory.getRelative("somewhere/else"); + scratch.file("parentpackage/BUILD"); + + AtomicReference<PathPackageLocator> pkgLocator = new AtomicReference<>( + new PathPackageLocator(outputBase, ImmutableList.of(emptyPackagePath, rootDirectory))); + deletedPackages = new AtomicReference<>(ImmutableSet.<PackageIdentifier>of()); + ExternalFilesHelper externalFilesHelper = new ExternalFilesHelper(pkgLocator); + TimestampGranularityMonitor tsgm = new TimestampGranularityMonitor(BlazeClock.instance()); + BlazeDirectories directories = new BlazeDirectories(rootDirectory, outputBase, rootDirectory); + + Map<SkyFunctionName, SkyFunction> skyFunctions = new HashMap<>(); + skyFunctions.put(SkyFunctions.PACKAGE_LOOKUP, + new PackageLookupFunction(deletedPackages)); + skyFunctions.put( + SkyFunctions.PACKAGE, + new PackageFunction(null, null, null, null, null, null, null)); + skyFunctions.put(SkyFunctions.FILE_STATE, new FileStateFunction(tsgm, externalFilesHelper)); + skyFunctions.put(SkyFunctions.FILE, new FileFunction(pkgLocator, tsgm, externalFilesHelper)); + skyFunctions.put(SkyFunctions.BLACKLISTED_PACKAGE_PREFIXES, + new BlacklistedPackagePrefixesFunction()); + RuleClassProvider ruleClassProvider = TestRuleClassProvider.getRuleClassProvider(); + skyFunctions.put( + SkyFunctions.WORKSPACE_FILE, + new WorkspaceFileFunction( + ruleClassProvider, + new PackageFactory( + ruleClassProvider, new BazelRulesModule().getPackageEnvironmentExtension()), + directories)); + differencer = new RecordingDifferencer(); + evaluator = new InMemoryMemoizingEvaluator(skyFunctions, differencer); + driver = new SequentialBuildDriver(evaluator); + PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID()); + PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator.get()); + PrecomputedValue.BLACKLISTED_PACKAGE_PREFIXES_FILE.set( + differencer, PathFragment.EMPTY_FRAGMENT); + } + + private PackageLookupValue lookupPackage(String packageName) throws InterruptedException { + return lookupPackage(PackageIdentifier.createInDefaultRepo(packageName)); + } + + private PackageLookupValue lookupPackage(PackageIdentifier packageId) + throws InterruptedException { + SkyKey key = PackageLookupValue.key(packageId); + return driver.<PackageLookupValue>evaluate( + ImmutableList.of(key), false, SkyframeExecutor.DEFAULT_THREAD_COUNT, + NullEventHandler.INSTANCE).get(key); + } + + public void testNoBuildFile() throws Exception { + scratch.file("parentpackage/nobuildfile/foo.txt"); + PackageLookupValue packageLookupValue = lookupPackage("parentpackage/nobuildfile"); + assertFalse(packageLookupValue.packageExists()); + assertEquals(ErrorReason.NO_BUILD_FILE, packageLookupValue.getErrorReason()); + assertNotNull(packageLookupValue.getErrorMsg()); + } + + public void testNoBuildFileAndNoParentPackage() throws Exception { + scratch.file("noparentpackage/foo.txt"); + PackageLookupValue packageLookupValue = lookupPackage("noparentpackage"); + assertFalse(packageLookupValue.packageExists()); + assertEquals(ErrorReason.NO_BUILD_FILE, packageLookupValue.getErrorReason()); + assertNotNull(packageLookupValue.getErrorMsg()); + } + + public void testDeletedPackage() throws Exception { + scratch.file("parentpackage/deletedpackage/BUILD"); + deletedPackages.set(ImmutableSet.of( + PackageIdentifier.createInDefaultRepo("parentpackage/deletedpackage"))); + PackageLookupValue packageLookupValue = lookupPackage("parentpackage/deletedpackage"); + assertFalse(packageLookupValue.packageExists()); + assertEquals(ErrorReason.DELETED_PACKAGE, packageLookupValue.getErrorReason()); + assertNotNull(packageLookupValue.getErrorMsg()); + } + + + public void testBlacklistedPackage() throws Exception { + scratch.file("blacklisted/subdir/BUILD"); + scratch.file("blacklisted/BUILD"); + PrecomputedValue.BLACKLISTED_PACKAGE_PREFIXES_FILE.set(differencer, + new PathFragment("config/blacklisted.txt")); + Path blacklist = scratch.file("config/blacklisted.txt", "blacklisted"); + + ImmutableSet<String> pkgs = ImmutableSet.of("blacklisted/subdir", "blacklisted"); + for (String pkg : pkgs) { + PackageLookupValue packageLookupValue = lookupPackage(pkg); + assertFalse(packageLookupValue.packageExists()); + assertEquals(ErrorReason.DELETED_PACKAGE, packageLookupValue.getErrorReason()); + assertNotNull(packageLookupValue.getErrorMsg()); + } + + scratch.overwriteFile("config/blacklisted.txt", "not_blacklisted"); + RootedPath rootedBlacklist = RootedPath.toRootedPath( + blacklist.getParentDirectory().getParentDirectory(), + new PathFragment("config/blacklisted.txt")); + differencer.invalidate(ImmutableSet.of(FileStateValue.key(rootedBlacklist))); + for (String pkg : pkgs) { + PackageLookupValue packageLookupValue = lookupPackage(pkg); + assertTrue(packageLookupValue.packageExists()); + } + } + + public void testInvalidPackageName() throws Exception { + scratch.file("parentpackage/invalidpackagename%42/BUILD"); + PackageLookupValue packageLookupValue = lookupPackage("parentpackage/invalidpackagename%42"); + assertFalse(packageLookupValue.packageExists()); + assertEquals(ErrorReason.INVALID_PACKAGE_NAME, + packageLookupValue.getErrorReason()); + assertNotNull(packageLookupValue.getErrorMsg()); + } + + public void testDirectoryNamedBuild() throws Exception { + scratch.dir("parentpackage/isdirectory/BUILD"); + PackageLookupValue packageLookupValue = lookupPackage("parentpackage/isdirectory"); + assertFalse(packageLookupValue.packageExists()); + assertEquals(ErrorReason.NO_BUILD_FILE, + packageLookupValue.getErrorReason()); + assertNotNull(packageLookupValue.getErrorMsg()); + } + + public void testEverythingIsGood() throws Exception { + scratch.file("parentpackage/everythinggood/BUILD"); + PackageLookupValue packageLookupValue = lookupPackage("parentpackage/everythinggood"); + assertTrue(packageLookupValue.packageExists()); + assertEquals(rootDirectory, packageLookupValue.getRoot()); + } + + public void testEmptyPackageName() throws Exception { + scratch.file("BUILD"); + PackageLookupValue packageLookupValue = lookupPackage(""); + assertTrue(packageLookupValue.packageExists()); + assertEquals(rootDirectory, packageLookupValue.getRoot()); + } + + public void testWorkspaceLookup() throws Exception { + scratch.overwriteFile("WORKSPACE"); + PackageLookupValue packageLookupValue = lookupPackage("external"); + assertTrue(packageLookupValue.packageExists()); + assertEquals(rootDirectory, packageLookupValue.getRoot()); + } + + // TODO(kchodorow): Clean this up (see TODOs in PackageLookupValue). + public void testExternalPackageLookupSemantics() { + PackageLookupValue existing = PackageLookupValue.workspace(rootDirectory); + assertTrue(existing.isExternalPackage()); + assertTrue(existing.packageExists()); + PackageLookupValue nonExistent = PackageLookupValue.workspace(rootDirectory.getRelative("x/y")); + assertTrue(nonExistent.isExternalPackage()); + assertFalse(nonExistent.packageExists()); + } + + public void testPackageLookupValueHashCodeAndEqualsContract() throws Exception { + Path root1 = rootDirectory.getRelative("root1"); + Path root2 = rootDirectory.getRelative("root2"); + // Our (seeming) duplication of parameters here is intentional. Some of the subclasses of + // PackageLookupValue are supposed to have reference equality semantics, and some are supposed + // to have logical equality semantics. + new EqualsTester() + .addEqualityGroup(PackageLookupValue.success(root1), PackageLookupValue.success(root1)) + .addEqualityGroup(PackageLookupValue.success(root2), PackageLookupValue.success(root2)) + .addEqualityGroup( + PackageLookupValue.NO_BUILD_FILE_VALUE, PackageLookupValue.NO_BUILD_FILE_VALUE) + .addEqualityGroup( + PackageLookupValue.DELETED_PACKAGE_VALUE, PackageLookupValue.DELETED_PACKAGE_VALUE) + .addEqualityGroup(PackageLookupValue.invalidPackageName("nope1"), + PackageLookupValue.invalidPackageName("nope1")) + .addEqualityGroup(PackageLookupValue.invalidPackageName("nope2"), + PackageLookupValue.invalidPackageName("nope2")) + .testEquals(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ParallelBuilderMediumTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ParallelBuilderMediumTest.java new file mode 100644 index 0000000000..1d923d7dd1 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/ParallelBuilderMediumTest.java @@ -0,0 +1,44 @@ +// 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 com.google.devtools.build.lib.testutil.Suite; +import com.google.devtools.build.lib.testutil.TestSpec; + +/** + * Stress tests for the parallel builder. + */ +@TestSpec(size = Suite.MEDIUM_TESTS) +public class ParallelBuilderMediumTest extends ParallelBuilderTest { + + /** + * A larger set of tests using randomly-generated complex dependency graphs. + */ + public void testRandomStressTest1() throws Exception { + final int numTrials = 2; + final int numArtifacts = 100; + final int randomSeed = 43; + StressTest test = new StressTest(numArtifacts, numTrials, randomSeed); + test.runStressTest(); + } + + public void testRandomStressTest2() throws Exception { + final int numTrials = 10; + final int numArtifacts = 10; + final int randomSeed = 44; + StressTest test = new StressTest(numArtifacts, numTrials, randomSeed); + test.runStressTest(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ParallelBuilderTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ParallelBuilderTest.java new file mode 100644 index 0000000000..b1637b2b5c --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/ParallelBuilderTest.java @@ -0,0 +1,863 @@ +// 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 com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import com.google.common.util.concurrent.Runnables; +import com.google.devtools.build.lib.actions.Action; +import com.google.devtools.build.lib.actions.ActionExecutedEvent; +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.ActionExecutionException; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.BuildFailedException; +import com.google.devtools.build.lib.actions.Executor; +import com.google.devtools.build.lib.actions.LocalHostCapacity; +import com.google.devtools.build.lib.actions.ResourceManager; +import com.google.devtools.build.lib.actions.ResourceSet; +import com.google.devtools.build.lib.actions.TestExecException; +import com.google.devtools.build.lib.actions.cache.ActionCache; +import com.google.devtools.build.lib.actions.util.TestAction; +import com.google.devtools.build.lib.events.Event; +import com.google.devtools.build.lib.events.EventHandler; +import com.google.devtools.build.lib.events.EventKind; +import com.google.devtools.build.lib.events.PrintingEventHandler; +import com.google.devtools.build.lib.testutil.BlazeTestUtils; +import com.google.devtools.build.lib.testutil.Suite; +import com.google.devtools.build.lib.testutil.TestSpec; +import com.google.devtools.build.lib.testutil.TestUtils; +import com.google.devtools.build.lib.util.AbruptExitException; +import com.google.devtools.build.lib.vfs.FileStatus; +import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +/** + * Test suite for ParallelBuilder. + * + */ +@TestSpec(size = Suite.MEDIUM_TESTS) +public class ParallelBuilderTest extends TimestampBuilderTestCase { + + private static final Logger LOG = + Logger.getLogger(ParallelBuilderTest.class.getName()); + + protected ActionCache cache; + + protected static final int DEFAULT_NUM_JOBS = 100; + + @Override + protected void setUp() throws Exception { + super.setUp(); + this.cache = new InMemoryActionCache(); + ResourceManager.instance().setAvailableResources(LocalHostCapacity.getLocalHostCapacity()); + ResourceManager.instance().setRamUtilizationPercentage( + ResourceManager.DEFAULT_RAM_UTILIZATION_PERCENTAGE); + ResourceManager.instance().resetResourceUsage(); + } + + @SafeVarargs + protected static <T> Set<T> asSet(T... elements) { + return Sets.newHashSet(elements); + } + + protected void buildArtifacts(Artifact... artifacts) + throws BuildFailedException, AbruptExitException, + InterruptedException, TestExecException { + buildArtifacts(createBuilder(DEFAULT_NUM_JOBS, false), artifacts); + } + + private Builder createBuilder(int jobs, boolean keepGoing) { + return createBuilder(cache, jobs, keepGoing); + } + + private volatile boolean runningFooAction; + private volatile boolean runningBarAction; + + /** + * Test that independent actions are run in parallel threads + * that are scheduled concurrently. + */ + public void runsInParallelWithBuilder(Builder builder) throws Exception { + // We create two actions, each of which waits (spinning) until the + // other action has started. If the two actions are not run + // in parallel, the test will deadlock and time out. + + // This specifies how many iterations to run before timing out. + // This should be large enough to ensure that that there is at + // least one context switch, otherwise the test may spuriously fail. + final long maxIterations = 100000000; + + // This specifies how often to print out progress messages. + // Uncomment this for debugging. + //final long PRINT_FREQUENCY = maxIterations / 10; + + runningFooAction = false; + runningBarAction = false; + + // [action] -> foo + Artifact foo = createDerivedArtifact("foo"); + Runnable makeFoo = new Runnable() { + @Override + public void run() { + runningFooAction = true; + for (long i = 0; i < maxIterations; i++) { + Thread.yield(); + if (runningBarAction) { + return; + } + // Uncomment this for debugging. + //if (i % PRINT_FREQUENCY == 0) { + // String msg = "ParallelBuilderTest: foo: waiting for bar"; + // System.out.println(bar); + //} + } + fail("ParallelBuilderTest: foo: waiting for bar: timed out"); + } + }; + registerAction(new TestAction(makeFoo, Artifact.NO_ARTIFACTS, ImmutableList.of(foo))); + + // [action] -> bar + Artifact bar = createDerivedArtifact("bar"); + Runnable makeBar = new Runnable() { + @Override + public void run() { + runningBarAction = true; + for (long i = 0; i < maxIterations; i++) { + Thread.yield(); + if (runningFooAction) { + return; + } + // Uncomment this for debugging. + //if (i % PRINT_FREQUENCY == 0) { + // String msg = "ParallelBuilderTest: bar: waiting for foo"; + // System.out.println(msg); + //} + } + fail("ParallelBuilderTest: bar: waiting for foo: timed out"); + } + }; + registerAction(new TestAction(makeBar, Artifact.NO_ARTIFACTS, ImmutableList.of(bar))); + + buildArtifacts(builder, foo, bar); + } + + /** + * Intercepts actionExecuted events, ordinarily written to the master log, for + * use locally within this test suite. + */ + public static class ActionEventRecorder { + private final List<ActionExecutedEvent> actionExecutedEvents = new ArrayList<>(); + + @Subscribe + public void actionExecuted(ActionExecutedEvent event) { + actionExecutedEvents.add(event); + } + } + + public void testReportsActionExecutedEvent() throws Exception { + Artifact pear = createDerivedArtifact("pear"); + ActionEventRecorder recorder = new ActionEventRecorder(); + EventBus eventBus = new EventBus(); + eventBusRef.set(eventBus); + eventBus.register(recorder); + + Action action = registerAction(new TestAction(Runnables.doNothing(), emptySet, asSet(pear))); + + buildArtifacts(createBuilder(DEFAULT_NUM_JOBS, true), pear); + assertThat(recorder.actionExecutedEvents).hasSize(1); + assertEquals(action, recorder.actionExecutedEvents.get(0).getAction()); + } + + public void testRunsInParallel() throws Exception { + runsInParallelWithBuilder(createBuilder(DEFAULT_NUM_JOBS, false)); + } + + /** + * Test that we can recover properly after a failed build. + */ + public void testFailureRecovery() throws Exception { + + // [action] -> foo + Artifact foo = createDerivedArtifact("foo"); + Callable<Void> makeFoo = new Callable<Void>() { + @Override + public Void call() throws IOException { + throw new IOException("building 'foo' is supposed to fail"); + } + }; + registerAction(new TestAction(makeFoo, Artifact.NO_ARTIFACTS, ImmutableList.of(foo))); + + // [action] -> bar + Artifact bar = createDerivedArtifact("bar"); + registerAction(new TestAction(TestAction.NO_EFFECT, emptySet, ImmutableList.of(bar))); + + // Don't fail fast when we encounter the error + reporter.removeHandler(failFastHandler); + + // test that building 'foo' fails + try { + buildArtifacts(foo); + fail("building 'foo' was supposed to fail!"); + } catch (BuildFailedException e) { + if (!e.getMessage().contains("building 'foo' is supposed to fail")) { + throw e; + } + // Make sure the reporter reported the error message. + assertContainsEvent("building 'foo' is supposed to fail"); + } + // test that a subsequent build of 'bar' succeeds + buildArtifacts(bar); + } + + public void testUpdateCacheError() throws Exception { + FileSystem fs = new InMemoryFileSystem() { + @Override + public FileStatus stat(Path path, boolean followSymlinks) throws IOException { + final FileStatus stat = super.stat(path, followSymlinks); + if (path.toString().endsWith("/out/foo")) { + return new FileStatus() { + private final FileStatus original = stat; + + @Override + public boolean isSymbolicLink() { + return original.isSymbolicLink(); + } + + @Override + public boolean isFile() { + return original.isFile(); + } + + @Override + public boolean isDirectory() { + return original.isDirectory(); + } + + @Override + public boolean isSpecialFile() { + return original.isSpecialFile(); + } + + @Override + public long getSize() throws IOException { + return original.getSize(); + } + + @Override + public long getNodeId() throws IOException { + return original.getNodeId(); + } + + @Override + public long getLastModifiedTime() throws IOException { + throw new IOException(); + } + + @Override + public long getLastChangeTime() throws IOException { + return original.getLastChangeTime(); + } + }; + } + return stat; + } + }; + Artifact foo = createDerivedArtifact(fs, "foo"); + registerAction(new TestAction(TestAction.NO_EFFECT, emptySet, ImmutableList.of(foo))); + reporter.removeHandler(failFastHandler); + try { + buildArtifacts(foo); + fail("Expected to fail"); + } catch (BuildFailedException e) { + assertContainsEvent("not all outputs were created"); + } + } + + public void testNullBuild() throws Exception { + // BuildTool.setupLogging(Level.FINEST); + LOG.fine("Testing null build..."); + buildArtifacts(); + } + + /** + * Test a randomly-generated complex dependency graph. + */ + public void testSmallRandomStressTest() throws Exception { + final int numTrials = 1; + final int numArtifacts = 30; + final int randomSeed = 42; + StressTest test = new StressTest(numArtifacts, numTrials, randomSeed); + test.runStressTest(); + } + + private static enum BuildKind { Clean, Incremental, Nop } + + /** + * Sets up and manages stress tests of arbitrary size. + */ + protected class StressTest { + + final int numArtifacts; + final int numTrials; + + Random random; + Artifact artifacts[]; + + public StressTest(int numArtifacts, int numTrials, int randomSeed) { + this.numTrials = numTrials; + this.numArtifacts = numArtifacts; + this.random = new Random(randomSeed); + } + + public void runStressTest() throws Exception { + for (int trial = 0; trial < numTrials; trial++) { + List<Counter> counters = buildRandomActionGraph(trial); + + // do a clean build + LOG.fine("Testing clean build... (trial " + trial + ")"); + Artifact[] buildTargets = chooseRandomBuild(); + buildArtifacts(buildTargets); + doSanityChecks(buildTargets, counters, BuildKind.Clean); + resetCounters(counters); + + // Do an incremental build. + // + // BuildTool creates new instances of the Builder for each build request. It may rely on + // that fact (that its state will be discarded after each build request) - thus + // test should use same approach and ensure that a new instance is used each time. + LOG.fine("Testing incremental build..."); + buildTargets = chooseRandomBuild(); + buildArtifacts(buildTargets); + doSanityChecks(buildTargets, counters, BuildKind.Incremental); + resetCounters(counters); + + // do a do-nothing build + LOG.fine("Testing do-nothing rebuild..."); + buildArtifacts(buildTargets); + doSanityChecks(buildTargets, counters, BuildKind.Nop); + //resetCounters(counters); + } + } + + /** + * Construct a random action graph, and initialize the file system + * so that all of the input files exist and none of the output files + * exist. + */ + public List<Counter> buildRandomActionGraph(int actionGraphNumber) throws IOException { + List<Counter> counters = new ArrayList<>(numArtifacts); + + artifacts = new Artifact[numArtifacts]; + for (int i = 0; i < numArtifacts; i++) { + artifacts[i] = createDerivedArtifact("file" + actionGraphNumber + "-" + i); + } + + int numOutputs; + for (int i = 0; i < artifacts.length; i += numOutputs) { + int numInputs = random.nextInt(3); + numOutputs = 1 + random.nextInt(2); + if (i + numOutputs >= artifacts.length) { + numOutputs = artifacts.length - i; + } + + Collection<Artifact> inputs = new ArrayList<>(numInputs); + for (int j = 0; j < numInputs; j++) { + if (i != 0) { + int inputNum = random.nextInt(i); + inputs.add(artifacts[inputNum]); + } + } + Collection<Artifact> outputs = new ArrayList<>(numOutputs); + for (int j = 0; j < numOutputs; j++) { + outputs.add(artifacts[i + j]); + } + counters.add(createActionCounter(inputs, outputs)); + if (inputs.isEmpty()) { + // source files -- create them + for (Artifact output : outputs) { + BlazeTestUtils.makeEmptyFile(output.getPath()); + } + } else { + // generated files -- delete them + for (Artifact output : outputs) { + try { + output.getPath().delete(); + } catch (FileNotFoundException e) { + // ok + } + } + } + } + return counters; + } + + /** + * Choose a random set of targets to build. + */ + public Artifact[] chooseRandomBuild() { + Artifact[] buildTargets; + switch (random.nextInt(4)) { + case 0: + // build the final output target + LOG.fine("Building final output target."); + buildTargets = new Artifact[] { artifacts[numArtifacts - 1] }; + break; + + case 1: { + // build all the targets (in random order); + LOG.fine("Building all the targets."); + List<Artifact> targets = Lists.newArrayList(artifacts); + Collections.shuffle(targets, random); + buildTargets = targets.toArray(new Artifact[numArtifacts]); + break; + } + + case 2: + // build a random target + LOG.fine("Building a random target."); + buildTargets = new Artifact[] { + artifacts[random.nextInt(numArtifacts)] + }; + break; + + case 3: { + // build a random subset of targets + LOG.fine("Building a random subset of targets."); + List<Artifact> targets = Lists.newArrayList(artifacts); + Collections.shuffle(targets, random); + List<Artifact> targetSubset = new ArrayList<>(); + int numTargetsToTest = random.nextInt(numArtifacts); + LOG.fine("numTargetsToTest = " + numTargetsToTest); + Iterator<Artifact> iterator = targets.iterator(); + for (int i = 0; i < numTargetsToTest; i++) { + targetSubset.add(iterator.next()); + } + buildTargets = targetSubset.toArray(new Artifact[numTargetsToTest]); + break; + } + + default: + throw new IllegalStateException(); + } + return buildTargets; + } + + public void doSanityChecks(Artifact[] targets, List<Counter> counters, + BuildKind kind) { + // Check that we really did build all the targets. + for (Artifact file : targets) { + assertTrue(file.getPath().exists()); + } + // Check that each action was executed the right number of times + for (Counter counter : counters) { + switch (kind) { + case Clean: + //assert counter.count == 1; + //break; + case Incremental: + assert counter.count == 0 || counter.count == 1; + break; + case Nop: + assert counter.count == 0; + break; + } + } + } + + private void resetCounters(List<Counter> counters) { + for (Counter counter : counters) { + counter.count = 0; + } + } + + } + + // Regression test for bug fixed in CL 3548332: builder was not waiting for + // all its subprocesses to terminate. + public void testWaitsForSubprocesses() throws Exception { + final Semaphore semaphore = new Semaphore(1); + final boolean[] finished = { false }; + + semaphore.acquireUninterruptibly(); // t=0: semaphore acquired + + // This arrangement ensures that the "bar" action tries to run for about + // 100ms after the "foo" action has completed (failed). + + // [action] -> foo + Artifact foo = createDerivedArtifact("foo"); + Callable<Void> makeFoo = new Callable<Void>() { + @Override + public Void call() throws IOException { + semaphore.acquireUninterruptibly(); // t=2: semaphore re-acquired + throw new IOException("foo action failed"); + } + }; + registerAction(new TestAction(makeFoo, Artifact.NO_ARTIFACTS, ImmutableList.of(foo))); + + // [action] -> bar + Artifact bar = createDerivedArtifact("bar"); + Runnable makeBar = new Runnable() { + @Override + public void run() { + semaphore.release(); // t=1: semaphore released + try { + Thread.sleep(100); // 100ms + } catch (InterruptedException e) { + // This might happen (though not necessarily). The + // ParallelBuilder interrupts all its workers at the first sign + // of trouble. + } + finished[0] = true; + } + }; + registerAction(new TestAction(makeBar, emptySet, asSet(bar))); + + // Don't fail fast when we encounter the error + reporter.removeHandler(failFastHandler); + + try { + buildArtifacts(foo, bar); + fail(); + } catch (BuildFailedException e) { + assertThat(e.getMessage()).contains("TestAction failed due to exception: foo action failed"); + assertContainsEvent("TestAction failed due to exception: foo action failed"); + } + + assertTrue("bar action not finished, yet buildArtifacts has completed.", + finished[0]); + } + + public void testSchedulingOfMemoryResources() throws Exception { + // The action graph consists of 100 independent actions, but execution is + // memory limited: only 6 TestActions can run concurrently: + ResourceManager.instance().setRamUtilizationPercentage(50); + ResourceManager.instance().setAvailableResources( + ResourceSet.createWithRamCpuIo(/*memoryMb=*/12.8, /*cpu=*/Integer.MAX_VALUE, /*io=*/0.0)); + ResourceManager.instance().resetResourceUsage(); + + class Counter { + int currentlyRunning = 0; + int maxConcurrent = 0; + synchronized void increment() { + ++currentlyRunning; + if (currentlyRunning > maxConcurrent) { + maxConcurrent = currentlyRunning; + } + } + synchronized void decrement() { + currentlyRunning--; + } + } + final Counter counter = new Counter(); + + Artifact[] outputs = new Artifact[100]; + for (int ii = 0; ii < outputs.length; ++ii) { + Artifact artifact = createDerivedArtifact("file" + ii); + Callable<Void> callable = new Callable<Void>() { + @Override + public Void call() throws Exception{ + counter.increment(); + Thread.sleep(100); // 100ms + counter.decrement(); + return null; + } + }; + registerAction(new TestAction(callable, Artifact.NO_ARTIFACTS, ImmutableList.of(artifact))); + outputs[ii] = artifact; + } + + buildArtifacts(outputs); + + assertEquals(0, counter.currentlyRunning); + assertEquals(6, counter.maxConcurrent); + } + + public void testEstimateExceedsAvailableRam() throws Exception { + // Pretend that the machine has only 1MB of RAM available, + // then test running an action that we estimate requires 2MB of RAM. + + ResourceManager.instance().setAvailableResources( + ResourceSet.createWithRamCpuIo(/*memoryMb=*/1.0, /*cpuUsage=*/4, /*ioUsage=*/0)); + ResourceManager.instance().resetResourceUsage(); + + final boolean[] finished = { false }; + Artifact foo = createDerivedArtifact("foo"); + Runnable makeFoo = new Runnable() { + @Override + public void run() { + finished[0] = true; + } + }; + registerAction(new TestAction(makeFoo, emptySet, asSet(foo)) { + @Override + public ResourceSet estimateResourceConsumption(Executor executor) { + return ResourceSet.createWithRamCpuIo(/*memoryMb=*/2.0, /*cpuUsage=*/1, /*ioUsage=*/0); + } + }); + buildArtifacts(foo); + assertTrue(finished[0]); + } + + public void testCyclicActionGraph() throws Exception { + // foo -> [action] -> bar + // bar -> [action] -> baz + // baz -> [action] -> foo + Artifact foo = createDerivedArtifact("foo"); + Artifact bar = createDerivedArtifact("bar"); + Artifact baz = createDerivedArtifact("baz"); + try { + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(foo), asSet(bar))); + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(bar), asSet(baz))); + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(baz), asSet(foo))); + buildArtifacts(foo); + fail("Builder failed to detect cyclic action graph"); + } catch (BuildFailedException e) { + assertEquals(e.getMessage(), CYCLE_MSG); + } + } + + public void testSelfCyclicActionGraph() throws Exception { + // foo -> [action] -> foo + Artifact foo = createDerivedArtifact("foo"); + try { + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(foo), asSet(foo))); + buildArtifacts(foo); + fail("Builder failed to detect cyclic action graph"); + } catch (BuildFailedException e) { + assertEquals(e.getMessage(), CYCLE_MSG); + } + } + + public void testCycleInActionGraphBelowTwoActions() throws Exception { + // bar -> [action] -> foo1 + // bar -> [action] -> foo2 + // baz -> [action] -> bar + // bar -> [action] -> baz + Artifact foo1 = createDerivedArtifact("foo1"); + Artifact foo2 = createDerivedArtifact("foo2"); + Artifact bar = createDerivedArtifact("bar"); + Artifact baz = createDerivedArtifact("baz"); + try { + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(bar), asSet(foo1))); + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(bar), asSet(foo2))); + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(baz), asSet(bar))); + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(bar), asSet(baz))); + buildArtifacts(foo1, foo2); + fail("Builder failed to detect cyclic action graph"); + } catch (BuildFailedException e) { + assertEquals(e.getMessage(), CYCLE_MSG); + } + } + + + public void testCyclicActionGraphWithTail() throws Exception { + // bar -> [action] -> foo + // baz -> [action] -> bar + // bat, foo -> [action] -> baz + Artifact foo = createDerivedArtifact("foo"); + Artifact bar = createDerivedArtifact("bar"); + Artifact baz = createDerivedArtifact("baz"); + Artifact bat = createDerivedArtifact("bat"); + try { + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(bar), asSet(foo))); + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(baz), asSet(bar))); + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(bat, foo), asSet(baz))); + registerAction(new TestAction(TestAction.NO_EFFECT, ImmutableSet.<Artifact>of(), asSet(bat))); + buildArtifacts(foo); + fail("Builder failed to detect cyclic action graph"); + } catch (BuildFailedException e) { + assertEquals(e.getMessage(), CYCLE_MSG); + } + } + + public void testDuplicatedInput() throws Exception { + // <null> -> [action] -> foo + // (foo, foo) -> [action] -> bar + Artifact foo = createDerivedArtifact("foo"); + Artifact bar = createDerivedArtifact("bar"); + registerAction( + new TestAction(TestAction.NO_EFFECT, ParallelBuilderTest.<Artifact>asSet(), asSet(foo))); + registerAction( + new TestAction(TestAction.NO_EFFECT, Lists.<Artifact>newArrayList(foo, foo), asSet(bar))); + buildArtifacts(bar); + } + + + // Regression test for bug #735765, "ParallelBuilder still issues new jobs + // after one has failed, without --keep-going." The incorrect behaviour is + // that, when the first job fails, while no new jobs are added to the queue + // of runnable jobs, the queue may have lots of work in it, and the + // ParallelBuilder always completes these jobs before it returns. The + // correct behaviour is to discard all the jobs in the queue after the first + // one fails. + public void assertNoNewJobsAreRunAfterFirstFailure(final boolean catastrophe, boolean keepGoing) + throws Exception { + // Strategy: Limit parallelism to 3. Enqueue 10 runnable tasks that run + // for an appreciable period (say 100ms). Ensure that at most 3 of those + // tasks completed. This proves that all runnable tasks were dropped from + // the queue after the first batch (which included errors) was finished. + // It should be pretty robust even in the face of timing variations. + + final AtomicInteger completedTasks = new AtomicInteger(0); + + int numJobs = 50; + Artifact[] artifacts = new Artifact[numJobs]; + + for (int ii = 0; ii < numJobs; ++ii) { + Artifact out = createDerivedArtifact(ii + ".out"); + List<Artifact> inputs = (catastrophe && ii > 10) + ? ImmutableList.of(artifacts[0]) + : Artifact.NO_ARTIFACTS; + final int iCopy = ii; + registerAction(new TestAction(new Callable<Void>() { + @Override + public Void call() throws Exception { + Thread.sleep(100); // 100ms + completedTasks.getAndIncrement(); + throw new IOException("task failed"); + } + }, + inputs, ImmutableList.of(out)) { + @Override + public void execute(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + if (catastrophe && iCopy == 0) { + try { + Thread.sleep(300); // 300ms + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + completedTasks.getAndIncrement(); + throw new ActionExecutionException("This is a catastrophe", this, true); + } + super.execute(actionExecutionContext); + } + }); + artifacts[ii] = out; + } + + // Don't fail fast when we encounter the error + reporter.removeHandler(failFastHandler); + + try { + buildArtifacts(createBuilder(3, keepGoing), artifacts); + fail(); + } catch (BuildFailedException e) { + assertContainsEvent("task failed"); + } + if (completedTasks.get() >= numJobs) { + fail("Expected early termination due to failed task, but all tasks ran to completion."); + } + } + + public void testNoNewJobsAreRunAfterFirstFailure() throws Exception { + assertNoNewJobsAreRunAfterFirstFailure(false, false); + } + + public void testNoNewJobsAreRunAfterCatastrophe() throws Exception { + assertNoNewJobsAreRunAfterFirstFailure(true, true); + } + + private Artifact createInputFile(String name) throws IOException { + Artifact artifact = createSourceArtifact(name); + Path path = artifact.getPath(); + FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); + FileSystemUtils.createEmptyFile(path); + return artifact; + } + + public void testProgressReporting() throws Exception { + // Build three artifacts in 3 separate actions (baz depends on bar and bar + // depends on foo. Make sure progress is reported at the beginning of all + // three actions. + List<Artifact> sourceFiles = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + sourceFiles.add(createInputFile("file" + i)); + } + Artifact foo = createDerivedArtifact("foo"); + Artifact bar = createDerivedArtifact("bar"); + Artifact baz = createDerivedArtifact("baz"); + bar.getPath().delete(); + baz.getPath().delete(); + + final List<String> messages = new ArrayList<>(); + EventHandler handler = new EventHandler() { + + @Override + public void handle(Event event) { + EventKind k = event.getKind(); + if (k == EventKind.START || k == EventKind.FINISH) { + // Remove the tmpDir as this is user specific and the assert would + // fail below. + messages.add( + event.getMessage().replaceFirst(TestUtils.tmpDir(), "") + " " + event.getKind()); + } + } + }; + reporter.addHandler(handler); + reporter.addHandler(new PrintingEventHandler(EventKind.ALL_EVENTS)); + + registerAction(new TestAction(TestAction.NO_EFFECT, sourceFiles, asSet(foo))); + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(foo), asSet(bar))); + registerAction(new TestAction(TestAction.NO_EFFECT, asSet(bar), asSet(baz))); + buildArtifacts(baz); + // Check that the percentages increase non-linearly, because foo has 10 input files + List<String> expectedMessages = Lists.newArrayList( + "Test foo START", + "Test foo FINISH", + "Test bar START", + "Test bar FINISH", + "Test baz START", + "Test baz FINISH"); + assertThat(messages).containsAllIn(expectedMessages); + + // Now do an incremental rebuild of bar and baz, + // and check the incremental progress percentages. + messages.clear(); + bar.getPath().delete(); + baz.getPath().delete(); + // This uses a new builder instance so that we refetch timestamps from + // (in-memory) file system, rather than using cached entries. + buildArtifacts(baz); + expectedMessages = Lists.newArrayList( + "Test bar START", + "Test bar FINISH", + "Test baz START", + "Test baz FINISH"); + assertThat(messages).containsAllIn(expectedMessages); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryFunctionTest.java new file mode 100644 index 0000000000..757495102b --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryFunctionTest.java @@ -0,0 +1,217 @@ +// 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 com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +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.pkgcache.FilteringPolicies; +import com.google.devtools.build.lib.pkgcache.FilteringPolicy; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.RootedPath; +import com.google.devtools.build.skyframe.BuildDriver; +import com.google.devtools.build.skyframe.EvaluationResult; +import com.google.devtools.build.skyframe.SkyKey; +import com.google.devtools.build.skyframe.WalkableGraph; + +import java.io.IOException; + +/** + * Tests for {@link PrepareDepsOfTargetsUnderDirectoryFunction}. Insert excuses here. + */ +public class PrepareDepsOfTargetsUnderDirectoryFunctionTest extends BuildViewTestCase { + + private SkyframeExecutor skyframeExecutor; + + @Override + protected void setUp() throws Exception { + super.setUp(); + skyframeExecutor = getSkyframeExecutor(); + } + + private SkyKey createPrepDepsKey(Path root, PathFragment rootRelativePath) { + return createPrepDepsKey(root, rootRelativePath, ImmutableSet.<PathFragment>of()); + } + + private SkyKey createPrepDepsKey(Path root, PathFragment rootRelativePath, + ImmutableSet<PathFragment> excludedPaths) { + RootedPath rootedPath = RootedPath.toRootedPath(root, rootRelativePath); + return PrepareDepsOfTargetsUnderDirectoryValue.key( + PackageIdentifier.DEFAULT_REPOSITORY_NAME, rootedPath, excludedPaths); + } + + private SkyKey createPrepDepsKey(Path root, PathFragment rootRelativePath, + ImmutableSet<PathFragment> excludedPaths, FilteringPolicy filteringPolicy) { + RootedPath rootedPath = RootedPath.toRootedPath(root, rootRelativePath); + return PrepareDepsOfTargetsUnderDirectoryValue.key( + PackageIdentifier.DEFAULT_REPOSITORY_NAME, rootedPath, excludedPaths, filteringPolicy); + } + + private EvaluationResult<PrepareDepsOfTargetsUnderDirectoryValue> getEvaluationResult(SkyKey key) + throws InterruptedException { + BuildDriver driver = skyframeExecutor.getDriverForTesting(); + EvaluationResult<PrepareDepsOfTargetsUnderDirectoryValue> evaluationResult = + driver.evaluate(ImmutableList.of(key), /*keepGoing=*/false, + SequencedSkyframeExecutor.DEFAULT_THREAD_COUNT, reporter); + Preconditions.checkState(!evaluationResult.hasError()); + return evaluationResult; + } + + public void testTransitiveLoading() throws Exception { + // Given a package "a" with a genrule "a" that depends on a target in package "b", + createPackages(); + + // When package "a" is evaluated, + SkyKey key = createPrepDepsKey(rootDirectory, new PathFragment("a")); + EvaluationResult<PrepareDepsOfTargetsUnderDirectoryValue> evaluationResult = + getEvaluationResult(key); + WalkableGraph graph = Preconditions.checkNotNull(evaluationResult.getWalkableGraph()); + + // Then the TransitiveTraversalValue for "a:a" is evaluated, + SkyKey aaKey = TransitiveTraversalValue.key(Label.create("a", "a")); + assertThat(graph.exists(aaKey)).isTrue(); + + // And that TransitiveTraversalValue depends on "b:b.txt". + Iterable<SkyKey> depsOfAa = + Iterables.getOnlyElement(graph.getDirectDeps(ImmutableList.of(aaKey)).values()); + SkyKey bTxtKey = TransitiveTraversalValue.key(Label.create("b", "b.txt")); + assertThat(depsOfAa).contains(bTxtKey); + + // And the TransitiveTraversalValue for "b:b.txt" is evaluated. + assertThat(graph.exists(bTxtKey)).isTrue(); + } + + public void testTargetFilterSensitivity() throws Exception { + // Given a package "a" with a genrule "a" that depends on a target in package "b", and a test + // rule "aTest", + createPackages(); + + // When package "a" is evaluated under a test-only filtering policy, + SkyKey key = createPrepDepsKey(rootDirectory, new PathFragment("a"), + ImmutableSet.<PathFragment>of(), FilteringPolicies.FILTER_TESTS); + EvaluationResult<PrepareDepsOfTargetsUnderDirectoryValue> evaluationResult = + getEvaluationResult(key); + WalkableGraph graph = Preconditions.checkNotNull(evaluationResult.getWalkableGraph()); + + // Then the TransitiveTraversalValue for "a:a" is not evaluated, + SkyKey aaKey = TransitiveTraversalValue.key(Label.create("a", "a")); + assertThat(graph.exists(aaKey)).isFalse(); + + // But the TransitiveTraversalValue for "a:aTest" is. + SkyKey aaTestKey = TransitiveTraversalValue.key(Label.create("a", "aTest")); + assertThat(graph.exists(aaTestKey)).isTrue(); + } + + /** + * Creates a package "a" with a genrule "a" that depends on a target in a created package "b", + * and a test rule "aTest". + */ + private void createPackages() throws IOException { + scratch.file("a/BUILD", + "genrule(name='a', cmd='', srcs=['//b:b.txt'], outs=['a.out'])", + "sh_test(name='aTest', size='small', srcs=['aTest.sh'])"); + scratch.file("b/BUILD", + "exports_files(['b.txt'])"); + } + + public void testSubdirectoryExclusion() throws Exception { + // Given a package "a" with two packages below it, "a/b" and "a/c", + scratch.file("a/BUILD"); + scratch.file("a/b/BUILD"); + scratch.file("a/c/BUILD"); + + // When the top package is evaluated via PrepareDepsOfTargetsUnderDirectoryValue with "a/b" + // excluded, + PathFragment excludedPathFragment = new PathFragment("a/b"); + SkyKey key = createPrepDepsKey(rootDirectory, new PathFragment("a"), + ImmutableSet.of(excludedPathFragment)); + EvaluationResult<PrepareDepsOfTargetsUnderDirectoryValue> evaluationResult = + getEvaluationResult(key); + PrepareDepsOfTargetsUnderDirectoryValue value = evaluationResult.get(key); + + // Then the value reports that "a" is a package, + assertThat(value.isDirectoryPackage()).isTrue(); + + // And only the subdirectory corresponding to "a/c" is present in the result, + RootedPath onlySubdir = + Iterables.getOnlyElement(value.getSubdirectoryTransitivelyContainsPackages().keySet()); + assertThat(onlySubdir.getRelativePath()).isEqualTo(new PathFragment("a/c")); + + // And the "a/c" subdirectory reports a package under it. + assertThat(value.getSubdirectoryTransitivelyContainsPackages().get(onlySubdir)).isTrue(); + + // Also, the computation graph does not contain a cached value for "a/b". + WalkableGraph graph = Preconditions.checkNotNull(evaluationResult.getWalkableGraph()); + assertFalse(graph.exists(createPrepDepsKey(rootDirectory, excludedPathFragment, + ImmutableSet.<PathFragment>of()))); + + // And the computation graph does contain a cached value for "a/c" with the empty set excluded, + // because that key was evaluated. + assertTrue(graph.exists(createPrepDepsKey(rootDirectory, new PathFragment("a/c"), + ImmutableSet.<PathFragment>of()))); + } + + public void testExcludedSubdirectoryGettingPassedDown() throws Exception { + // Given a package "a", and a package below it in "a/b/c", and a non-BUILD file below it in + // "a/b/d", + scratch.file("a/BUILD"); + scratch.file("a/b/c/BUILD"); + scratch.file("a/b/d/helloworld"); + + // When the top package is evaluated for recursive package values, and "a/b/c" is excluded, + ImmutableSet<PathFragment> excludedPaths = ImmutableSet.of(new PathFragment("a/b/c")); + SkyKey key = createPrepDepsKey(rootDirectory, new PathFragment("a"), excludedPaths); + EvaluationResult<PrepareDepsOfTargetsUnderDirectoryValue> evaluationResult = + getEvaluationResult(key); + PrepareDepsOfTargetsUnderDirectoryValue value = evaluationResult.get(key); + + // Then the value reports that "a" is a package, + assertThat(value.isDirectoryPackage()).isTrue(); + + // And the subdirectory corresponding to "a/b" is present in the result, + RootedPath onlySubdir = + Iterables.getOnlyElement(value.getSubdirectoryTransitivelyContainsPackages().keySet()); + assertThat(onlySubdir.getRelativePath()).isEqualTo(new PathFragment("a/b")); + + // And the "a/b" subdirectory does not report a package under it (because it got excluded). + assertThat(value.getSubdirectoryTransitivelyContainsPackages().get(onlySubdir)).isFalse(); + + // Also, the computation graph contains a cached value for "a/b" with "a/b/c" excluded, because + // "a/b/c" does live underneath "a/b". + WalkableGraph graph = Preconditions.checkNotNull(evaluationResult.getWalkableGraph()); + SkyKey abKey = createPrepDepsKey(rootDirectory, new PathFragment("a/b"), excludedPaths); + assertThat(graph.exists(abKey)).isTrue(); + PrepareDepsOfTargetsUnderDirectoryValue abValue = + (PrepareDepsOfTargetsUnderDirectoryValue) Preconditions.checkNotNull(graph.getValue(abKey)); + + // And that value says that "a/b" is not a package, + assertThat(abValue.isDirectoryPackage()).isFalse(); + + // And only the subdirectory "a/b/d" is present in that value, + RootedPath abd = + Iterables.getOnlyElement(abValue.getSubdirectoryTransitivelyContainsPackages().keySet()); + assertThat(abd.getRelativePath()).isEqualTo(new PathFragment("a/b/d")); + + // And no package is under "a/b/d". + assertThat(abValue.getSubdirectoryTransitivelyContainsPackages().get(abd)).isFalse(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTest.java new file mode 100644 index 0000000000..5359fe9b39 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTest.java @@ -0,0 +1,317 @@ +// 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 org.junit.Assert.fail; + +import com.google.common.collect.Sets; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.BuildFailedException; +import com.google.devtools.build.lib.actions.util.TestAction; +import com.google.devtools.build.lib.testutil.BlazeTestUtils; +import com.google.devtools.build.lib.vfs.FileSystemUtils; + +import java.util.Collection; +import java.util.Collections; + +/** + * Test suite for TimestampBuilder. + * + */ +public class TimestampBuilderTest extends TimestampBuilderTestCase { + + public void testAmnesiacBuilderAlwaysRebuilds() throws Exception { + // [action] -> hello + Artifact hello = createDerivedArtifact("hello"); + Button button = createActionButton(emptySet, Sets.newHashSet(hello)); + + button.pressed = false; + buildArtifacts(amnesiacBuilder(), hello); + assertTrue(button.pressed); // built + + button.pressed = false; + buildArtifacts(amnesiacBuilder(), hello); + assertTrue(button.pressed); // rebuilt + } + + // If we re-use the same builder (even an "amnesiac" builder), it remembers + // which Actions it has already visited, and doesn't revisit them, even if + // they would otherwise be rebuilt. + // + // That is, Builders conflate traversal and dependency analysis, and don't + // revisit a node (traversal) even if it needs to be rebuilt (dependency + // analysis). We might want to separate these aspects. + public void testBuilderDoesntRevisitActions() throws Exception { + // [action] -> hello + Artifact hello = createDerivedArtifact("hello"); + Counter counter = createActionCounter(emptySet, Sets.newHashSet(hello)); + + Builder amnesiacBuilder = amnesiacBuilder(); + + counter.count = 0; + buildArtifacts(amnesiacBuilder, hello, hello); + assertEquals(counter.count, 1); // built only once + } + + public void testBuildingExistingSourcefileSuceeds() throws Exception { + Artifact hello = createSourceArtifact("hello"); + BlazeTestUtils.makeEmptyFile(hello.getPath()); + buildArtifacts(cachingBuilder(), hello); + } + + public void testBuildingNonexistentSourcefileFails() throws Exception { + reporter.removeHandler(failFastHandler); + Artifact hello = createSourceArtifact("hello"); + try { + buildArtifacts(cachingBuilder(), hello); + fail("Expected input file to be missing"); + } catch (BuildFailedException e) { + assertThat(e).hasMessage("missing input file '" + hello.getPath() + "'"); + } + } + + public void testCachingBuilderCachesUntilReset() throws Exception { + // [action] -> hello + Artifact hello = createDerivedArtifact("hello"); + Button button = createActionButton(emptySet, Sets.newHashSet(hello)); + + button.pressed = false; + buildArtifacts(cachingBuilder(), hello); + assertTrue(button.pressed); // built + + button.pressed = false; + buildArtifacts(cachingBuilder(), hello); + assertFalse(button.pressed); // not rebuilt + + inMemoryCache.reset(); + + button.pressed = false; + buildArtifacts(cachingBuilder(), hello); + assertTrue(button.pressed); // rebuilt + } + + public void testUnneededInputs() throws Exception { + Artifact hello = createSourceArtifact("hello"); + BlazeTestUtils.makeEmptyFile(hello.getPath()); + Artifact optional = createSourceArtifact("hello.optional"); + Artifact goodbye = createDerivedArtifact("goodbye"); + Button button = createActionButton(Sets.newHashSet(hello, optional), Sets.newHashSet(goodbye)); + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertTrue(button.pressed); // built + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button.pressed); // not rebuilt + + BlazeTestUtils.makeEmptyFile(optional.getPath()); + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertTrue(button.pressed); // built + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button.pressed); // not rebuilt + + optional.getPath().delete(); + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertTrue(button.pressed); // built + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button.pressed); // not rebuilt + } + + public void testModifyingInputCausesActionReexecution() throws Exception { + // hello -> [action] -> goodbye + Artifact hello = createSourceArtifact("hello"); + BlazeTestUtils.makeEmptyFile(hello.getPath()); + Artifact goodbye = createDerivedArtifact("goodbye"); + Button button = createActionButton(Sets.newHashSet(hello), Sets.newHashSet(goodbye)); + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertTrue(button.pressed); // built + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button.pressed); // not rebuilt + + // inMemoryMetadataCache.useFileDigest is false, so new timestamp is enough to force a rebuild. + FileSystemUtils.touchFile(hello.getPath()); + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertTrue(button.pressed); // rebuilt + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button.pressed); // not rebuilt + } + + public void testOnlyModifyingInputContentCausesReexecution() throws Exception { + // hello -> [action] -> goodbye + Artifact hello = createSourceArtifact("hello"); + // touch file to create the directory structure + BlazeTestUtils.makeEmptyFile(hello.getPath()); + FileSystemUtils.writeContentAsLatin1(hello.getPath(), "content1"); + + Artifact goodbye = createDerivedArtifact("goodbye"); + Button button = createActionButton(Sets.newHashSet(hello), Sets.newHashSet(goodbye)); + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertTrue(button.pressed); // built + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button.pressed); // not rebuilt + + FileSystemUtils.touchFile(hello.getPath()); + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button.pressed); // still not rebuilt + + FileSystemUtils.writeContentAsLatin1(hello.getPath(), "content2"); + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertTrue(button.pressed); // rebuilt + + button.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button.pressed); // not rebuilt + } + + public void testModifyingOutputCausesActionReexecution() throws Exception { + // [action] -> hello + Artifact hello = createDerivedArtifact("hello"); + Button button = createActionButton(emptySet, Sets.newHashSet(hello)); + + button.pressed = false; + buildArtifacts(cachingBuilder(), hello); + assertTrue(button.pressed); // built + + button.pressed = false; + buildArtifacts(cachingBuilder(), hello); + assertFalse(button.pressed); // not rebuilt + + // Touching the *output* file 'hello' causes 'action' to re-execute, to + // make things consistent again; this is not what Make would do, but it is + // correct according to this Builder. + BlazeTestUtils.changeModtime(hello.getPath()); + + button.pressed = false; + buildArtifacts(cachingBuilder(), hello); + assertTrue(button.pressed); // rebuilt + + button.pressed = false; + buildArtifacts(cachingBuilder(), hello); + assertFalse(button.pressed); // not rebuilt + } + + public void testBuildingTransitivePrerequisites() throws Exception { + // hello -> [action1] -> wazuup -> [action2] -> goodbye + Artifact hello = createSourceArtifact("hello"); + BlazeTestUtils.makeEmptyFile(hello.getPath()); + Artifact wazuup = createDerivedArtifact("wazuup"); + Button button1 = createActionButton(Sets.newHashSet(hello), Sets.newHashSet(wazuup)); + Artifact goodbye = createDerivedArtifact("goodbye"); + Button button2 = createActionButton(Sets.newHashSet(wazuup), Sets.newHashSet(goodbye)); + + button1.pressed = button2.pressed = false; + buildArtifacts(cachingBuilder(), wazuup); + assertTrue(button1.pressed); // built wazuup + assertFalse(button2.pressed); // goodbye not built + + button1.pressed = button2.pressed = false; + buildArtifacts(cachingBuilder(), wazuup); + assertFalse(button1.pressed); // wazuup not rebuilt + assertFalse(button2.pressed); // goodbye not built + + button1.pressed = button2.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button1.pressed); // wazuup not rebuilt + assertTrue(button2.pressed); // built goodbye + + button1.pressed = button2.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertFalse(button1.pressed); // wazuup not rebuilt + assertFalse(button2.pressed); // goodbye not rebuilt + + FileSystemUtils.touchFile(hello.getPath()); + + button1.pressed = button2.pressed = false; + buildArtifacts(cachingBuilder(), goodbye); + assertTrue(button1.pressed); // hello rebuilt + assertTrue(button2.pressed); // goodbye rebuilt + } + + public void testWillNotRebuildActionsWithEmptyListOfInputsSpuriously() + throws Exception { + + Artifact anOutputFile = createDerivedArtifact("anOutputFile"); + Artifact anotherOutputFile = createDerivedArtifact("anotherOutputFile"); + Collection<Artifact> noInputs = Collections.emptySet(); + + Button aButton = createActionButton(noInputs, Sets.newHashSet(anOutputFile)); + Button anotherButton = createActionButton(noInputs, + Sets.newHashSet(anotherOutputFile)); + + buildArtifacts(cachingBuilder(), anOutputFile, anotherOutputFile); + + assertTrue(aButton.pressed); + assertTrue(anotherButton.pressed); + + aButton.pressed = anotherButton.pressed = false; + + buildArtifacts(cachingBuilder(), anOutputFile, anotherOutputFile); + + assertFalse(aButton.pressed); + assertFalse(anotherButton.pressed); + } + + public void testMissingSourceFileIsAnError() throws Exception { + // A missing input to an action must be treated as an error because there's + // a risk that the action that consumes it will succeed, but with a + // different behavior (imagine that it globs over the directory, for + // example). It's not ok to simply try the action and let the action + // report "input file not found". + // + // (However, there are exceptions to this principle: C++ compilation + // actions may depend on non-existent headers from stale .d files. We need + // to allow the action to proceed to execution in this case.) + + reporter.removeHandler(failFastHandler); + Artifact in = createSourceArtifact("in"); // doesn't exist + Artifact out = createDerivedArtifact("out"); + + registerAction(new TestAction(TestAction.NO_EFFECT, Collections.singleton(in), + Collections.singleton(out))); + + try { + buildArtifacts(amnesiacBuilder(), out); // fails with ActionExecutionException + fail(); + } catch (BuildFailedException e) { + assertThat(e.getMessage()).contains("1 input file(s) do not exist"); + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java new file mode 100644 index 0000000000..da7eadc049 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java @@ -0,0 +1,195 @@ +// 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 com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; +import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; +import com.google.devtools.build.lib.bazel.rules.BazelRulesModule; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.packages.NoSuchTargetException; +import com.google.devtools.build.lib.packages.Package; +import com.google.devtools.build.lib.packages.PackageFactory; +import com.google.devtools.build.lib.packages.Rule; +import com.google.devtools.build.lib.testutil.MoreAsserts; +import com.google.devtools.build.lib.testutil.TestRuleClassProvider; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.RootedPath; +import com.google.devtools.build.skyframe.SkyFunction; +import com.google.devtools.build.skyframe.SkyKey; + +import org.mockito.Matchers; +import org.mockito.Mockito; + +import java.io.IOException; + +/** + * Test for {@link WorkspaceFileFunction}. + */ +public class WorkspaceFileFunctionTest extends BuildViewTestCase { + + private WorkspaceFileFunction skyFunc; + private FakeFileValue fakeWorkspaceFileValue; + + private static class FakeFileValue extends FileValue { + private boolean exists; + private long size; + + FakeFileValue() { + super(); + exists = true; + size = 0L; + } + + @Override + public RootedPath realRootedPath() { + throw new UnsupportedOperationException(); + } + + @Override + public FileStateValue realFileStateValue() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean exists() { + return exists; + } + + private void setExists(boolean exists) { + this.exists = exists; + } + + @Override + public long getSize() { + return size; + } + + private void setSize(long size) { + this.size = size; + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + ConfiguredRuleClassProvider ruleClassProvider = TestRuleClassProvider.getRuleClassProvider(); + skyFunc = + new WorkspaceFileFunction( + ruleClassProvider, + new PackageFactory( + TestRuleClassProvider.getRuleClassProvider(), + new BazelRulesModule().getPackageEnvironmentExtension()), + directories); + fakeWorkspaceFileValue = new FakeFileValue(); + } + + private Label getLabelMapping(Package pkg, String name) throws NoSuchTargetException { + return (Label) ((Rule) pkg.getTarget(name)).getAttributeContainer().getAttr("actual"); + } + + private RootedPath createWorkspaceFile(String... contents) throws IOException { + Path workspacePath = scratch.overwriteFile("WORKSPACE", contents); + fakeWorkspaceFileValue.setSize(workspacePath.getFileSize()); + return RootedPath.toRootedPath( + workspacePath.getParentDirectory(), new PathFragment(workspacePath.getBaseName())); + } + + private SkyFunction.Environment getEnv() { + SkyFunction.Environment env = Mockito.mock(SkyFunction.Environment.class); + Mockito.when(env.getValue(Matchers.<SkyKey>any())).thenReturn(fakeWorkspaceFileValue); + return env; + } + + public void testInvalidRepo() throws Exception { + RootedPath workspacePath = createWorkspaceFile("workspace(name = 'foo$')"); + PackageValue value = + (PackageValue) skyFunc.compute(PackageValue.workspaceKey(workspacePath), getEnv()); + Package pkg = value.getPackage(); + assertTrue(pkg.containsErrors()); + MoreAsserts.assertContainsEvent(pkg.getEvents(), "target names may not contain '$'"); + } + + public void testBindFunction() throws Exception { + String lines[] = {"bind(name = 'foo/bar',", "actual = '//foo:bar')"}; + RootedPath workspacePath = createWorkspaceFile(lines); + + SkyKey key = PackageValue.workspaceKey(workspacePath); + PackageValue value = (PackageValue) skyFunc.compute(key, getEnv()); + Package pkg = value.getPackage(); + assertEquals(Label.parseAbsolute("//foo:bar"), getLabelMapping(pkg, "foo/bar")); + MoreAsserts.assertNoEvents(pkg.getEvents()); + } + + public void testBindArgsReversed() throws Exception { + String lines[] = {"bind(actual = '//foo:bar', name = 'foo/bar')"}; + RootedPath workspacePath = createWorkspaceFile(lines); + + SkyKey key = PackageValue.workspaceKey(workspacePath); + PackageValue value = (PackageValue) skyFunc.compute(key, getEnv()); + Package pkg = value.getPackage(); + assertEquals(Label.parseAbsolute("//foo:bar"), getLabelMapping(pkg, "foo/bar")); + MoreAsserts.assertNoEvents(pkg.getEvents()); + } + + public void testNonExternalBinding() throws Exception { + // name must be a valid label name. + String lines[] = {"bind(name = 'foo:bar', actual = '//bar/baz')"}; + RootedPath workspacePath = createWorkspaceFile(lines); + + PackageValue value = + (PackageValue) skyFunc.compute(PackageValue.workspaceKey(workspacePath), getEnv()); + Package pkg = value.getPackage(); + assertTrue(pkg.containsErrors()); + MoreAsserts.assertContainsEvent(pkg.getEvents(), "target names may not contain ':'"); + } + + public void testWorkspaceFileParsingError() throws Exception { + // //external:bar:baz is not a legal package. + String lines[] = {"bind(name = 'foo/bar', actual = '//external:bar:baz')"}; + RootedPath workspacePath = createWorkspaceFile(lines); + + PackageValue value = + (PackageValue) skyFunc.compute(PackageValue.workspaceKey(workspacePath), getEnv()); + Package pkg = value.getPackage(); + assertTrue(pkg.containsErrors()); + MoreAsserts.assertContainsEvent(pkg.getEvents(), "target names may not contain ':'"); + } + + public void testNoWorkspaceFile() throws Exception { + // Even though the WORKSPACE exists, Skyframe thinks it doesn't, so it doesn't. + String lines[] = {"bind(name = 'foo/bar', actual = '//foo:bar')"}; + RootedPath workspacePath = createWorkspaceFile(lines); + fakeWorkspaceFileValue.setExists(false); + + PackageValue value = + (PackageValue) skyFunc.compute(PackageValue.workspaceKey(workspacePath), getEnv()); + Package pkg = value.getPackage(); + assertFalse(pkg.containsErrors()); + MoreAsserts.assertNoEvents(pkg.getEvents()); + } + + public void testListBindFunction() throws Exception { + String lines[] = { + "L = ['foo', 'bar']", "bind(name = '%s/%s' % (L[0], L[1]),", "actual = '//foo:bar')"}; + RootedPath workspacePath = createWorkspaceFile(lines); + + SkyKey key = PackageValue.workspaceKey(workspacePath); + PackageValue value = (PackageValue) skyFunc.compute(key, getEnv()); + Package pkg = value.getPackage(); + assertEquals(Label.parseAbsolute("//foo:bar"), getLabelMapping(pkg, "foo/bar")); + MoreAsserts.assertNoEvents(pkg.getEvents()); + } +} diff --git a/src/test/java/com/google/devtools/build/skyframe/BUILD b/src/test/java/com/google/devtools/build/skyframe/BUILD new file mode 100644 index 0000000000..7313ed09e8 --- /dev/null +++ b/src/test/java/com/google/devtools/build/skyframe/BUILD @@ -0,0 +1,53 @@ +TESTUTIL_FILES = [ + "DeterministicInMemoryGraph.java", + "NotifyingInMemoryGraph.java", + "TrackingAwaiter.java", + "GraphTester.java", + "GenericFunctionException.java", + "SomeErrorException.java", + "TrackingInvalidationReceiver.java", + "WalkableGraphUtils.java", +] + +java_library( + name = "testutil", + srcs = TESTUTIL_FILES, + visibility = [ + "//src/test/java/com/google/devtools/build/lib:__pkg__", + ], + deps = [ + "//src/main/java/com/google/devtools/build/lib:collect", + "//src/main/java/com/google/devtools/build/lib:concurrent", + "//src/main/java/com/google/devtools/build/lib:events", + "//src/main/java/com/google/devtools/build/lib:util", + "//src/main/java/com/google/devtools/build/skyframe", + "//src/test/java/com/google/devtools/build/lib:testutil", + "//third_party:guava", + "//third_party:guava-testlib", + "//third_party:jsr305", + "//third_party:truth", + ], +) + +java_test( + name = "skyframe_base_test", + srcs = glob( + ["*.java"], + exclude = TESTUTIL_FILES, + ), + args = ["com.google.devtools.build.skyframe.AllTests"], + deps = [ + ":testutil", + "//src/main/java/com/google/devtools/build/lib:collect", + "//src/main/java/com/google/devtools/build/lib:concurrent", + "//src/main/java/com/google/devtools/build/lib:events", + "//src/main/java/com/google/devtools/build/lib:util", + "//src/main/java/com/google/devtools/build/skyframe", + "//src/test/java/com/google/devtools/build/lib:testutil", + "//third_party:guava", + "//third_party:guava-testlib", + "//third_party:jsr305", + "//third_party:junit4", + "//third_party:truth", + ], +) |