Move creating directories for action outputs from `SkyframeActionExecutor` to a
separate class.
The logic creating output directories for actions is complicated enough to
deserve a class of its own. Move the logic to the new class and add proper unit
tests now that we have a clean-cut on its API.
Add test helpers for easy listing of leaf entries in directories.
PiperOrigin-RevId: 375115886
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionOutputDirectoryHelper.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionOutputDirectoryHelper.java
new file mode 100644
index 0000000..59984af
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionOutputDirectoryHelper.java
@@ -0,0 +1,246 @@
+// Copyright 2014 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.cache.CacheBuilder;
+import com.google.common.cache.CacheBuilderSpec;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Striped;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactPathResolver;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * Helper class to create directories for {@linkplain com.google.devtools.build.lib.actions.Action
+ * action} outputs.
+ */
+public final class ActionOutputDirectoryHelper {
+
+ // Used to prevent check-then-act races in #createOutputDirectories. See the comment there for
+ // more detail.
+ private static final Striped<Lock> outputDirectoryDeletionLock = Striped.lock(64);
+
+ // Directories which are known to be created as regular directories within this invocation. This
+ // implies parent directories are also regular directories.
+ private final Map<PathFragment, DirectoryState> knownDirectories;
+
+ private enum DirectoryState {
+ FOUND,
+ CREATED
+ }
+
+ ActionOutputDirectoryHelper(CacheBuilderSpec cacheBuilderSpec) {
+ knownDirectories =
+ CacheBuilder.from(cacheBuilderSpec)
+ .concurrencyLevel(Runtime.getRuntime().availableProcessors())
+ .<PathFragment, DirectoryState>build()
+ .asMap();
+ }
+
+ /**
+ * Create output directories for an ActionFS. The action-local filesystem starts empty, so we
+ * expect the output directory creation to always succeed. There can be no interference from state
+ * left behind by prior builds or other actions intra-build.
+ */
+ void createActionFsOutputDirectories(
+ ImmutableSet<Artifact> actionOutputs, ArtifactPathResolver artifactPathResolver)
+ throws CreateOutputDirectoryException {
+ Set<Path> done = new HashSet<>(); // avoid redundant calls for the same directory.
+ for (Artifact outputFile : actionOutputs) {
+ Path outputDir;
+ if (outputFile.isTreeArtifact()) {
+ outputDir = artifactPathResolver.toPath(outputFile);
+ } else {
+ outputDir = artifactPathResolver.toPath(outputFile).getParentDirectory();
+ }
+
+ if (done.add(outputDir)) {
+ try {
+ outputDir.createDirectoryAndParents();
+ } catch (IOException e) {
+ throw new CreateOutputDirectoryException(outputDir.asFragment(), e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates output directories, with ancestors, for all action outputs.
+ *
+ * <p>For regular file outputs, creates the parent directories, for tree outputs creates the tree
+ * directory (with ancestors in both cases).
+ *
+ * <p>It is only valid to use this method when not using an action file system, in which case
+ * please use {@link #createActionFsOutputDirectories} instead. In particular, {@linkplain
+ * #knownDirectories the cache of directories}, shared across actions will cause common
+ * directories to be created in action file system for only one of the actions as opposed to all
+ * of them.
+ */
+ void createOutputDirectories(ImmutableSet<Artifact> actionOutputs)
+ throws CreateOutputDirectoryException {
+ Set<Path> done = new HashSet<>(); // avoid redundant calls for the same directory.
+ for (Artifact outputFile : actionOutputs) {
+ Path outputDir;
+ // Given we know that we are not using action file system, we can get safely get paths
+ // directly from the artifacts.
+ if (outputFile.isTreeArtifact()) {
+ outputDir = outputFile.getPath();
+ } else {
+ outputDir = outputFile.getPath().getParentDirectory();
+ }
+
+ if (done.add(outputDir)) {
+ try {
+ createAndCheckForSymlinks(outputDir, outputFile);
+ continue;
+ } catch (IOException e) {
+ /* Fall through to plan B. */
+ }
+
+ // Possibly some direct ancestors are not directories. In that case, we traverse the
+ // ancestors downward, deleting any non-directories. This handles the case where a file
+ // becomes a directory. The traversal is done downward because otherwise we may delete
+ // files through a symlink in a parent directory. Since Blaze never creates such
+ // directories within a build, we have no idea where on disk we're actually deleting.
+ //
+ // Symlinks should not be followed so in order to clean up symlinks pointing to Fileset
+ // outputs from previous builds. See bug [incremental build of Fileset fails if
+ // Fileset.out was changed to be a subdirectory of the old value].
+ try {
+ Path p = outputFile.getRoot().getRoot().asPath();
+ for (String segment : outputDir.relativeTo(p).segments()) {
+ p = p.getRelative(segment);
+
+ // This lock ensures that the only thread that observes a filesystem transition in
+ // which the path p first exists and then does not is the thread that calls
+ // p.delete() and causes the transition.
+ //
+ // If it were otherwise, then some thread A could test p.exists(), see that it does,
+ // then test p.isDirectory(), see that p isn't a directory (because, say, thread
+ // B deleted it), and then call p.delete(). That could result in two different kinds
+ // of failures:
+ //
+ // 1) In the time between when thread A sees that p is not a directory and when thread
+ // A calls p.delete(), thread B may reach the call to createDirectoryAndParents
+ // and create a directory at p, which thread A then deletes. Thread B would then try
+ // adding outputs to the directory it thought was there, and fail.
+ //
+ // 2) In the time between when thread A sees that p is not a directory and when thread
+ // A calls p.delete(), thread B may create a directory at p, and then either create a
+ // subdirectory beneath it or add outputs to it. Then when thread A tries to delete p,
+ // it would fail.
+ Lock lock = outputDirectoryDeletionLock.get(p);
+ lock.lock();
+ try {
+ FileStatus stat = p.statIfFound(Symlinks.NOFOLLOW);
+ if (stat == null) {
+ // Missing entry: Break out and create expected directories.
+ break;
+ }
+ if (stat.isDirectory()) {
+ // If this directory used to be a tree artifact it won't be writable.
+ p.setWritable(true);
+ knownDirectories.put(p.asFragment(), DirectoryState.FOUND);
+ } else {
+ // p may be a file or symlink (possibly from a Fileset in a previous build).
+ p.delete(); // throws IOException
+ break;
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+ outputDir.createDirectoryAndParents();
+ } catch (IOException e) {
+ throw new CreateOutputDirectoryException(outputDir.asFragment(), e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Create an output directory and ensure that no symlinks exists between the output root and the
+ * output file. These are all expected to be regular directories. Violations of this expectations
+ * can only come from state left behind by previous invocations or external filesystem mutation.
+ */
+ private void createAndCheckForSymlinks(Path dir, Artifact outputFile) throws IOException {
+ Path rootPath = outputFile.getRoot().getRoot().asPath();
+ PathFragment root = rootPath.asFragment();
+
+ // If the output root has not been created yet, do so now.
+ if (!knownDirectories.containsKey(root)) {
+ FileStatus stat = rootPath.statNullable(Symlinks.NOFOLLOW);
+ if (stat == null) {
+ rootPath.createDirectoryAndParents();
+ knownDirectories.put(root, DirectoryState.CREATED);
+ } else {
+ knownDirectories.put(root, DirectoryState.FOUND);
+ }
+ }
+
+ // Walk up until the first known directory is found (must be root or below).
+ List<Path> checkDirs = new ArrayList<>();
+ while (!knownDirectories.containsKey(dir.asFragment())) {
+ checkDirs.add(dir);
+ dir = dir.getParentDirectory();
+ }
+
+ // Check in reverse order (parent directory first):
+ // - If symlink -> Exception.
+ // - If non-existent -> Create directory and all children.
+ boolean parentCreated = knownDirectories.get(dir.asFragment()) == DirectoryState.CREATED;
+ for (Path path : Lists.reverse(checkDirs)) {
+ if (parentCreated) {
+ // If we have created this directory's parent, we know that it doesn't exist or else we
+ // would know about it already. Even if a parallel thread has created it in the meantime,
+ // createDirectory() will return normally and we can assume that a regular directory exists
+ // afterwards.
+ path.createDirectory();
+ knownDirectories.put(path.asFragment(), DirectoryState.CREATED);
+ continue;
+ }
+ FileStatus stat = path.statNullable(Symlinks.NOFOLLOW);
+ if (stat != null && !stat.isDirectory()) {
+ throw new IOException(dir + " is not a regular directory");
+ }
+ if (stat == null) {
+ parentCreated = true;
+ path.createDirectory();
+ knownDirectories.put(path.asFragment(), DirectoryState.CREATED);
+ } else {
+ knownDirectories.put(path.asFragment(), DirectoryState.FOUND);
+ }
+ }
+ }
+
+ static final class CreateOutputDirectoryException extends IOException {
+ final PathFragment directoryPath;
+
+ private CreateOutputDirectoryException(PathFragment directoryPath, IOException cause) {
+ super(cause.getMessage(), cause);
+ this.directoryPath = directoryPath;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
index 5f54fee..89bcca6 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -19,6 +19,7 @@
"ActionArtifactCycleReporter.java",
"ActionExecutionFunction.java",
"ActionLookupConflictFindingFunction.java",
+ "ActionOutputDirectoryHelper.java",
"AspectCompletionValue.java",
"AspectCompletor.java",
"AspectFunction.java",
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
index 1a024fc..7c9fd4e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
@@ -14,17 +14,13 @@
package com.google.devtools.build.lib.skyframe;
import com.google.common.base.Preconditions;
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
-import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.flogger.GoogleLogger;
import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.Striped;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionCacheChecker;
@@ -97,17 +93,16 @@
import com.google.devtools.build.lib.skyframe.ActionExecutionState.ActionStep;
import com.google.devtools.build.lib.skyframe.ActionExecutionState.ActionStepOrResult;
import com.google.devtools.build.lib.skyframe.ActionExecutionState.SharedActionCallback;
+import com.google.devtools.build.lib.skyframe.ActionOutputDirectoryHelper.CreateOutputDirectoryException;
import com.google.devtools.build.lib.util.CrashFailureDetails;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.io.FileOutErr;
-import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.OutputService;
import com.google.devtools.build.lib.vfs.OutputService.ActionFileSystemType;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
-import com.google.devtools.build.lib.vfs.Symlinks;
import com.google.devtools.build.lib.vfs.UnixGlob.FilesystemCalls;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyKey;
@@ -115,15 +110,12 @@
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
import java.util.function.Supplier;
import javax.annotation.Nullable;
@@ -135,10 +127,6 @@
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
- // Used to prevent check-then-act races in #createOutputDirectories. See the comment there for
- // more detail.
- private static final Striped<Lock> outputDirectoryDeletionLock = Striped.lock(64);
-
private static final MetadataInjector THROWING_METADATA_INJECTOR_FOR_ACTIONFS =
new MetadataInjector() {
@Override
@@ -204,14 +192,7 @@
// at the start of its next attempt,
private ConcurrentMap<OwnerlessArtifactWrapper, ImmutableList<SkyKey>> lostDiscoveredInputsMap;
- // Directories which are known to be created as regular directories within this invocation. This
- // implies parent directories are also regular directories.
- private Map<PathFragment, DirectoryState> knownDirectories;
-
- private enum DirectoryState {
- FOUND,
- CREATED
- }
+ private ActionOutputDirectoryHelper outputDirectoryHelper;
private OptionsProvider options;
private boolean useAsyncExecution;
@@ -296,11 +277,9 @@
this.replayActionOutErr = options.getOptions(BuildRequestOptions.class).replayActionOutErr;
this.outputService = outputService;
- Cache<PathFragment, DirectoryState> cache =
- CacheBuilder.from(options.getOptions(BuildRequestOptions.class).directoryCreationCacheSpec)
- .concurrencyLevel(Runtime.getRuntime().availableProcessors())
- .build();
- this.knownDirectories = cache.asMap();
+ this.outputDirectoryHelper =
+ new ActionOutputDirectoryHelper(
+ options.getOptions(BuildRequestOptions.class).directoryCreationCacheSpec);
// Retaining discovered inputs is only worthwhile for incremental builds or builds with extra
// actions, which consume their shadowed action's discovered inputs.
@@ -376,7 +355,7 @@
this.lostDiscoveredInputsMap = null;
this.actionCacheChecker = null;
this.topDownActionCache = null;
- this.knownDirectories = null;
+ this.outputDirectoryHelper = null;
}
/**
@@ -1014,9 +993,9 @@
setupActionFsFileOutErr(actionExecutionContext.getFileOutErr(), action);
}
if (actionFileSystemType().inMemoryFileSystem()) {
- createActionFsOutputDirectories(action, actionExecutionContext);
+ createActionFsOutputDirectories(action, actionExecutionContext.getPathResolver());
} else {
- createOutputDirectories(action, actionExecutionContext);
+ createOutputDirectories(action);
}
} catch (ActionExecutionException e) {
// This try-catch block cannot trigger rewinding, so it is safe to notify the status
@@ -1322,25 +1301,14 @@
* expect the output directory creation to always succeed. There can be no interference from state
* left behind by prior builds or other actions intra-build.
*/
- private void createActionFsOutputDirectories(Action action, ActionExecutionContext context)
- throws ActionExecutionException {
+ private void createActionFsOutputDirectories(
+ Action action, ArtifactPathResolver artifactPathResolver) throws ActionExecutionException {
try {
- Set<Path> done = new HashSet<>(); // avoid redundant calls for the same directory.
- for (Artifact outputFile : action.getOutputs()) {
- Path outputDir;
- if (outputFile.isTreeArtifact()) {
- outputDir = context.getPathResolver().toPath(outputFile);
- } else {
- outputDir = context.getPathResolver().toPath(outputFile).getParentDirectory();
- }
-
- if (done.add(outputDir)) {
- outputDir.createDirectoryAndParents();
- }
- }
- } catch (IOException e) {
+ outputDirectoryHelper.createActionFsOutputDirectories(
+ action.getOutputs(), artifactPathResolver);
+ } catch (CreateOutputDirectoryException e) {
throw toActionExecutionException(
- "failed to create output directory",
+ String.format("failed to create output directory '%s'", e.directoryPath),
e,
action,
null,
@@ -1348,152 +1316,17 @@
}
}
- /**
- * Create an output directory and ensure that no symlinks exists between the output root and the
- * output file. These are all expected to be regular directories. Violations of this expectations
- * can only come from state left behind by previous invocations or external filesystem mutation.
- */
- private void createAndCheckForSymlinks(
- final Path dir, final Artifact outputFile, ActionExecutionContext context)
- throws IOException {
- Path rootPath = outputFile.getRoot().getRoot().asPath();
- PathFragment root = rootPath.asFragment();
- Path curDir = context.getPathResolver().convertPath(dir);
-
- // If the output root has not been created yet, do so now.
- if (!knownDirectories.containsKey(root)) {
- FileStatus stat = rootPath.statNullable(Symlinks.NOFOLLOW);
- if (stat == null) {
- outputFile.getRoot().getRoot().asPath().createDirectoryAndParents();
- knownDirectories.put(root, DirectoryState.CREATED);
- } else {
- knownDirectories.put(root, DirectoryState.FOUND);
- }
- }
-
- // Walk up until the first known directory is found (must be root or below).
- List<Path> checkDirs = new ArrayList<>();
- while (!knownDirectories.containsKey(curDir.asFragment())) {
- checkDirs.add(curDir);
- curDir = curDir.getParentDirectory();
- }
-
- // Check in reverse order (parent directory first):
- // - If symlink -> Exception.
- // - If non-existent -> Create directory and all children.
- boolean parentCreated = knownDirectories.get(curDir.asFragment()) == DirectoryState.CREATED;
- for (Path path : Lists.reverse(checkDirs)) {
- if (parentCreated) {
- // If we have created this directory's parent, we know that it doesn't exist or else we
- // would know about it already. Even if a parallel thread has created it in the meantime,
- // createDirectory() will return normally and we can assume that a regular directory exists
- // afterwards.
- path.createDirectory();
- knownDirectories.put(path.asFragment(), DirectoryState.CREATED);
- continue;
- }
- FileStatus stat = path.statNullable(Symlinks.NOFOLLOW);
- if (stat != null && !stat.isDirectory()) {
- throw new IOException(curDir + " is not a regular directory");
- }
- if (stat == null) {
- parentCreated = true;
- path.createDirectory();
- knownDirectories.put(path.asFragment(), DirectoryState.CREATED);
- } else {
- knownDirectories.put(path.asFragment(), DirectoryState.FOUND);
- }
- }
- }
-
- private void createOutputDirectories(Action action, ActionExecutionContext context)
- throws ActionExecutionException {
+ private void createOutputDirectories(Action action) throws ActionExecutionException {
try {
- Set<Path> done = new HashSet<>(); // avoid redundant calls for the same directory.
- for (Artifact outputFile : action.getOutputs()) {
- Path outputDir;
- if (outputFile.isTreeArtifact()) {
- outputDir = context.getPathResolver().toPath(outputFile);
- } else {
- outputDir = context.getPathResolver().toPath(outputFile).getParentDirectory();
- }
-
- if (done.add(outputDir)) {
- try {
- createAndCheckForSymlinks(outputDir, outputFile, context);
- continue;
- } catch (IOException e) {
- /* Fall through to plan B. */
- }
-
- // Possibly some direct ancestors are not directories. In that case, we traverse the
- // ancestors downward, deleting any non-directories. This handles the case where a file
- // becomes a directory. The traversal is done downward because otherwise we may delete
- // files through a symlink in a parent directory. Since Blaze never creates such
- // directories within a build, we have no idea where on disk we're actually deleting.
- //
- // Symlinks should not be followed so in order to clean up symlinks pointing to Fileset
- // outputs from previous builds. See bug [incremental build of Fileset fails if
- // Fileset.out was changed to be a subdirectory of the old value].
- try {
- Path p =
- context.getPathResolver().transformRoot(outputFile.getRoot().getRoot()).asPath();
- for (String segment : outputDir.relativeTo(p).segments()) {
- p = p.getRelative(segment);
-
- // This lock ensures that the only thread that observes a filesystem transition in
- // which the path p first exists and then does not is the thread that calls
- // p.delete() and causes the transition.
- //
- // If it were otherwise, then some thread A could test p.exists(), see that it does,
- // then test p.isDirectory(), see that p isn't a directory (because, say, thread
- // B deleted it), and then call p.delete(). That could result in two different kinds
- // of failures:
- //
- // 1) In the time between when thread A sees that p is not a directory and when thread
- // A calls p.delete(), thread B may reach the call to createDirectoryAndParents
- // and create a directory at p, which thread A then deletes. Thread B would then try
- // adding outputs to the directory it thought was there, and fail.
- //
- // 2) In the time between when thread A sees that p is not a directory and when thread
- // A calls p.delete(), thread B may create a directory at p, and then either create a
- // subdirectory beneath it or add outputs to it. Then when thread A tries to delete p,
- // it would fail.
- Lock lock = outputDirectoryDeletionLock.get(p);
- lock.lock();
- try {
- FileStatus stat = p.statIfFound(Symlinks.NOFOLLOW);
- if (stat == null) {
- // Missing entry: Break out and create expected directories.
- break;
- }
- if (stat.isDirectory()) {
- // If this directory used to be a tree artifact it won't be writable.
- p.setWritable(true);
- knownDirectories.put(p.asFragment(), DirectoryState.FOUND);
- } else {
- // p may be a file or symlink (possibly from a Fileset in a previous build).
- p.delete(); // throws IOException
- break;
- }
- } finally {
- lock.unlock();
- }
- }
- outputDir.createDirectoryAndParents();
- } catch (IOException e) {
- String message =
- String.format(
- "failed to create output directory '%s': %s", outputDir, e.getMessage());
- DetailedExitCode code =
- createDetailedExitCode(message, Code.ACTION_OUTPUT_DIRECTORY_CREATION_FAILURE);
- throw new ActionExecutionException(message, e, action, false, code);
- }
- }
- }
- } catch (ActionExecutionException ex) {
- printError(ex.getMessage(), action, null);
- throw ex;
+ outputDirectoryHelper.createOutputDirectories(action.getOutputs());
+ } catch (CreateOutputDirectoryException e) {
+ throw toActionExecutionException(
+ String.format(
+ "failed to create output directory '%s': %s", e.directoryPath, e.getMessage()),
+ e,
+ action,
+ /*actionOutput=*/ null,
+ Code.ACTION_OUTPUT_DIRECTORY_CREATION_FAILURE);
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/testing/common/BUILD b/src/main/java/com/google/devtools/build/lib/testing/common/BUILD
index 747a907..0055b03 100644
--- a/src/main/java/com/google/devtools/build/lib/testing/common/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/testing/common/BUILD
@@ -1,6 +1,7 @@
load("@rules_java//java:defs.bzl", "java_library")
package(
+ default_testonly = True,
default_visibility = ["//src:__subpackages__"],
)
@@ -8,13 +9,13 @@
filegroup(
name = "srcs",
+ testonly = False,
srcs = glob(["**"]),
visibility = ["//src:__subpackages__"],
)
java_library(
name = "fake-options",
- testonly = True,
srcs = ["FakeOptions.java"],
deps = [
"//src/main/java/com/google/devtools/common/options",
@@ -22,3 +23,12 @@
"//third_party:jsr305",
],
)
+
+java_library(
+ name = "directory_listing_helper",
+ srcs = ["DirectoryListingHelper.java"],
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib/vfs",
+ "//third_party:guava",
+ ],
+)
diff --git a/src/main/java/com/google/devtools/build/lib/testing/common/DirectoryListingHelper.java b/src/main/java/com/google/devtools/build/lib/testing/common/DirectoryListingHelper.java
new file mode 100644
index 0000000..459572e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/testing/common/DirectoryListingHelper.java
@@ -0,0 +1,68 @@
+// Copyright 2021 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.testing.common;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import java.io.IOException;
+
+/** Namespace for helpers to test recursive directory listings. */
+public final class DirectoryListingHelper {
+
+ private DirectoryListingHelper() {}
+
+ /** Shorthand for {@link Dirent} of {@link Dirent.Type#DIRECTORY} type with a given name. */
+ public static Dirent directory(String name) {
+ return new Dirent(name, Dirent.Type.DIRECTORY);
+ }
+
+ /**
+ * Returns all of the leaf {@linkplain Dirent dirents} under a given directory.
+ *
+ * <p>For directory structure of:
+ *
+ * <pre>
+ * dir/dir2
+ * dir/file1
+ * dir/subdir/file2
+ * </pre>
+ *
+ * will return: {@code FILE(dir/file1), FILE(dir/subdir/file2), DIRECTORY(dir/dir2)}.
+ */
+ public static ImmutableList<Dirent> leafDirectoryEntries(Path path) throws IOException {
+ ImmutableList.Builder<Dirent> entries = ImmutableList.builder();
+ leafDirectoryEntriesInternal(path, "", entries);
+ return entries.build();
+ }
+
+ private static void leafDirectoryEntriesInternal(
+ Path path, String prefix, ImmutableList.Builder<Dirent> entries) throws IOException {
+ boolean isEmpty = true;
+ for (Dirent dirent : path.readdir(Symlinks.NOFOLLOW)) {
+ if (dirent.getType() == Dirent.Type.DIRECTORY) {
+ leafDirectoryEntriesInternal(
+ path.getChild(dirent.getName()),
+ prefix.isEmpty() ? dirent.getName() : prefix + "/" + dirent.getName(),
+ entries);
+ }
+ isEmpty = false;
+ }
+
+ if (isEmpty) {
+ entries.add(new Dirent(prefix, Dirent.Type.DIRECTORY));
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ActionOutputDirectoryHelperTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ActionOutputDirectoryHelperTest.java
new file mode 100644
index 0000000..b2bdcc2
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ActionOutputDirectoryHelperTest.java
@@ -0,0 +1,218 @@
+// Copyright 2021 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testing.common.DirectoryListingHelper.leafDirectoryEntries;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.cache.CacheBuilderSpec;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
+import com.google.devtools.build.lib.actions.ArtifactPathResolver;
+import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.ArtifactRoot.RootType;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.skyframe.ActionOutputDirectoryHelper.CreateOutputDirectoryException;
+import com.google.devtools.build.lib.testing.common.DirectoryListingHelper;
+import com.google.devtools.build.lib.testutil.Scratch;
+import com.google.devtools.build.lib.vfs.DelegateFileSystem;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ActionOutputDirectoryHelper}. */
+@RunWith(TestParameterInjector.class)
+public class ActionOutputDirectoryHelperTest {
+
+ private static final CacheBuilderSpec CACHE_ENABLED =
+ CacheBuilderSpec.parse("maximumSize=100000");
+
+ private Path execRoot;
+ private ArtifactRoot outputRoot;
+
+ @Before
+ public void createArtifactRootAndOutputDirectoryHelper() throws IOException {
+ Scratch scratch = new Scratch();
+ execRoot = scratch.dir("/execroot");
+ outputRoot = createOutputRoot(execRoot);
+ }
+
+ private enum OutputSet {
+ SINGLE_FILE(
+ /*fileOutputs=*/ ImmutableSet.of("a/b"),
+ /*treeOutputs=*/ ImmutableSet.of(),
+ /*expectedDirectories=*/ ImmutableList.of("a")),
+ DEEP_DIRECTORY_STRUCTURE(
+ /*fileOutputs=*/ ImmutableSet.of("a/b/c/d/e/f/g/h/i/j/k/l/m"),
+ /*treeOutputs=*/ ImmutableSet.of(),
+ /*expectedDirectories=*/ ImmutableList.of("a/b/c/d/e/f/g/h/i/j/k/l")),
+ MULTIPLE_FILES(
+ /*fileOutputs=*/ ImmutableSet.of("a/b/c", "a/c", "a/d/1", "a/d/2"),
+ /*treeOutputs=*/ ImmutableSet.of(),
+ /*expectedDirectories=*/ ImmutableList.of("a/b", "a/d")),
+ TREE_OUTPUT(
+ /*fileOutputs=*/ ImmutableSet.of(),
+ /*treeOutputs=*/ ImmutableSet.of("a/b"),
+ /*expectedDirectories=*/ ImmutableList.of("a/b"));
+
+ OutputSet(
+ ImmutableSet<String> fileOutputs,
+ ImmutableSet<String> treeOutputs,
+ ImmutableList<String> expectedDirectories) {
+ this.fileOutputs = fileOutputs;
+ this.treeOutputs = treeOutputs;
+ this.expectedDirectories = expectedDirectories;
+ }
+
+ ImmutableSet<Artifact> actionOutputs(ActionOutputDirectoryHelperTest test) {
+ ImmutableSet.Builder<Artifact> outs =
+ ImmutableSet.builderWithExpectedSize(fileOutputs.size() + treeOutputs.size());
+ fileOutputs.stream().map(test::createOutput).forEach(outs::add);
+ treeOutputs.stream().map(test::createTreeOutput).forEach(outs::add);
+ return outs.build();
+ }
+
+ ImmutableList<Dirent> expectedDirectoryEntries() {
+ return expectedDirectories.stream()
+ .map(DirectoryListingHelper::directory)
+ .collect(toImmutableList());
+ }
+
+ private final ImmutableSet<String> fileOutputs;
+ private final ImmutableSet<String> treeOutputs;
+ private final ImmutableList<String> expectedDirectories;
+ }
+
+ @Test
+ public void createOutputDirectories_createsExpectedDirectories(@TestParameter OutputSet outputSet)
+ throws Exception {
+ ActionOutputDirectoryHelper outputDirectoryHelper = createActionOutputDirectoryHelper();
+
+ outputDirectoryHelper.createOutputDirectories(outputSet.actionOutputs(this));
+
+ assertThat(leafDirectoryEntries(outputRoot.getRoot().asPath()))
+ .containsExactlyElementsIn(outputSet.expectedDirectoryEntries());
+ }
+
+ @Test
+ public void createActionFsOutputDirectories_createsExpectedDirectoriesInActionFs(
+ @TestParameter OutputSet outputSet) throws Exception {
+ ActionOutputDirectoryHelper outputDirectoryHelper = createActionOutputDirectoryHelper();
+ FileSystem actionFileSystem = new Scratch().getFileSystem();
+ ArtifactPathResolver resolver =
+ ArtifactPathResolver.createPathResolver(actionFileSystem, execRoot);
+
+ outputDirectoryHelper.createActionFsOutputDirectories(outputSet.actionOutputs(this), resolver);
+
+ Path outputRootPath = outputRoot.getRoot().asPath();
+ assertThat(outputRootPath.exists()).isFalse();
+ assertThat(leafDirectoryEntries(actionFileSystem.getPath(outputRootPath.asFragment())))
+ .containsExactlyElementsIn(outputSet.expectedDirectoryEntries());
+ }
+
+ @Test
+ public void createOutputDirectories_ioExceptionWhenCreatingDirectory_fails() {
+ ActionOutputDirectoryHelper outputDirectoryHelper = createActionOutputDirectoryHelper();
+ IOException injectedException = new IOException("oh no!");
+ PathFragment outputRootPath = outputRoot.getRoot().asPath().asFragment();
+ FileSystem fsWithFailures =
+ createFileSystemInjectingException(outputRootPath.getRelative("dir"), injectedException);
+ ArtifactRoot rootWithFailure = createOutputRoot(fsWithFailures.getPath(execRoot.asFragment()));
+ ImmutableSet<Artifact> outputs = ImmutableSet.of(createOutput(rootWithFailure, "dir/file"));
+
+ CreateOutputDirectoryException e =
+ assertThrows(
+ CreateOutputDirectoryException.class,
+ () -> outputDirectoryHelper.createOutputDirectories(outputs));
+
+ assertThat(e.directoryPath).isEqualTo(outputRootPath.getRelative("dir"));
+ assertThat(e).hasCauseThat().isSameInstanceAs(injectedException);
+ }
+
+ @Test
+ public void createActionFsOutputDirectories_ioExceptionWhenCreatingDirectory_fails() {
+ ActionOutputDirectoryHelper outputDirectoryHelper = createActionOutputDirectoryHelper();
+ IOException injectedException = new IOException("oh no!");
+ PathFragment outputRootPath = outputRoot.getRoot().asPath().asFragment();
+ FileSystem fsWithFailures =
+ createFileSystemInjectingException(outputRootPath.getRelative("dir"), injectedException);
+ ArtifactRoot rootWithFailure = createOutputRoot(fsWithFailures.getPath(execRoot.asFragment()));
+ ImmutableSet<Artifact> outputs = ImmutableSet.of(createOutput(rootWithFailure, "dir/file"));
+ FileSystem actionFileSystem = new DelegateFileSystem(fsWithFailures) {};
+ ArtifactPathResolver pathResolver =
+ ArtifactPathResolver.createPathResolver(actionFileSystem, execRoot);
+
+ CreateOutputDirectoryException e =
+ assertThrows(
+ CreateOutputDirectoryException.class,
+ () -> outputDirectoryHelper.createActionFsOutputDirectories(outputs, pathResolver));
+
+ assertThat(e.directoryPath).isEqualTo(outputRootPath.getRelative("dir"));
+ assertThat(e).hasCauseThat().isSameInstanceAs(injectedException);
+ }
+
+ private FileSystem createFileSystemInjectingException(
+ PathFragment failingPath, IOException injectedException) {
+ return new DelegateFileSystem(execRoot.getFileSystem()) {
+ @Override
+ public boolean createDirectory(PathFragment path) throws IOException {
+ if (path.equals(failingPath)) {
+ throw injectedException;
+ }
+ return super.createDirectory(path);
+ }
+
+ @Override
+ public void createDirectoryAndParents(PathFragment path) throws IOException {
+ if (path.equals(failingPath)) {
+ throw injectedException;
+ }
+ super.createDirectoryAndParents(path);
+ }
+ };
+ }
+
+ private SpecialArtifact createTreeOutput(String relativeExecPath) {
+ return ActionsTestUtil.createTreeArtifactWithGeneratingAction(
+ outputRoot, outputRoot.getExecPath().getRelative(relativeExecPath));
+ }
+
+ private Artifact createOutput(String relativeExecPath) {
+ return createOutput(outputRoot, relativeExecPath);
+ }
+
+ private static Artifact createOutput(ArtifactRoot outputRoot, String relativeExecPath) {
+ return ActionsTestUtil.createArtifactWithRootRelativePath(
+ outputRoot, PathFragment.create(relativeExecPath));
+ }
+
+ private static ArtifactRoot createOutputRoot(Path execRoot) {
+ return ArtifactRoot.asDerivedRoot(execRoot, RootType.Output, "out");
+ }
+
+ private static ActionOutputDirectoryHelper createActionOutputDirectoryHelper() {
+ return new ActionOutputDirectoryHelper(CACHE_ENABLED);
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
index 9f6795d..701cb3e 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -124,7 +124,6 @@
"//src/main/java/com/google/devtools/build/lib/analysis:config/build_configuration",
"//src/main/java/com/google/devtools/build/lib/analysis:config/build_options",
"//src/main/java/com/google/devtools/build/lib/analysis:config/compilation_mode",
- "//src/main/java/com/google/devtools/build/lib/analysis:config/config_matching_provider",
"//src/main/java/com/google/devtools/build/lib/analysis:config/core_options",
"//src/main/java/com/google/devtools/build/lib/analysis:config/fragment",
"//src/main/java/com/google/devtools/build/lib/analysis:config/fragment_class_set",
@@ -152,7 +151,6 @@
"//src/main/java/com/google/devtools/build/lib/bazel:main",
"//src/main/java/com/google/devtools/build/lib/bazel/rules",
"//src/main/java/com/google/devtools/build/lib/bugreport",
- "//src/main/java/com/google/devtools/build/lib/causes",
"//src/main/java/com/google/devtools/build/lib/clock",
"//src/main/java/com/google/devtools/build/lib/cmdline",
"//src/main/java/com/google/devtools/build/lib/collect",
@@ -252,6 +250,7 @@
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils:depsutils",
+ "//src/main/java/com/google/devtools/build/lib/testing/common:directory_listing_helper",
"//src/main/java/net/starlark/java/annot",
"//src/main/java/com/google/devtools/build/lib/util",
"//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception",
@@ -273,7 +272,6 @@
"//src/test/java/com/google/devtools/build/lib/actions/util",
"//src/test/java/com/google/devtools/build/lib/analysis/testing",
"//src/test/java/com/google/devtools/build/lib/analysis/util",
- "//src/test/java/com/google/devtools/build/lib/analysis/util:test-build-options",
"//src/test/java/com/google/devtools/build/lib/events:testutil",
"//src/test/java/com/google/devtools/build/lib/packages:testutil",
"//src/test/java/com/google/devtools/build/lib/rules/platform:testutil",
@@ -294,6 +292,7 @@
"//third_party:mockito",
"//third_party:truth",
"//third_party/protobuf:protobuf_java",
+ "@com_google_testparameterinjector//:testparameterinjector",
],
)