aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/test/java/com/google
diff options
context:
space:
mode:
authorGravatar Kristina Chodorow <kchodorow@google.com>2015-11-16 23:19:13 +0000
committerGravatar Damien Martin-Guillerez <dmarting@google.com>2015-11-17 10:52:56 +0000
commit335f0672e981d66a32ffac70d5b6aa58254a80c7 (patch)
treea2699f37c385cf47901b45acf74ed9285208d244 /src/test/java/com/google
parent1648470c7fe1048e23fadcd02d6e81c693f52259 (diff)
Open source skyframe tests
-- MOS_MIGRATED_REVID=107983315
Diffstat (limited to 'src/test/java/com/google')
-rw-r--r--src/test/java/com/google/devtools/build/lib/BUILD3
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java557
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/PackageLookupFunctionTest.java231
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/ParallelBuilderMediumTest.java44
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/ParallelBuilderTest.java863
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryFunctionTest.java217
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTest.java317
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java195
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/BUILD53
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",
+ ],
+)