aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Ulf Adams <ulfjack@google.com>2017-03-21 10:08:03 +0000
committerGravatar Yue Gan <yueg@google.com>2017-03-21 12:53:49 +0000
commitc0a84443526539c36557a6b6bfd0e41623d62fd0 (patch)
treecad733194b2fb397676b4d60e9d062c8d1aa0e02
parent30e3276642fae54ff1be951c52e3286b715409ea (diff)
Add SpawnInputExpander helper class to arrange runfiles for spawn strategies
This new class is a combination of SpawnHelper and our internal code; the plan is to migrate all spawn strategies to the new class. The strict flag should be enabled by default, but that's a breaking change, so we need to do it later. - Use it in SandboxStrategy. - Add ActionInput.getExecPath to return a PathFragment; this avoids lots of back and forth between path fragments and strings. This is a step towards #1593. The previous attempt was missing a one-line patch in StandaloneTestStrategy, which broke all tests with sandboxing. StandaloneTestStrategy was fixed in a separate change, so this should be safe now. -- PiperOrigin-RevId: 150733457 MOS_MIGRATED_REVID=150733457
-rw-r--r--src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java208
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java40
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java3
-rw-r--r--src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java242
4 files changed, 489 insertions, 4 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java
new file mode 100644
index 0000000000..2f69ab044a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java
@@ -0,0 +1,208 @@
+// Copyright 2017 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.exec;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.io.LineProcessor;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander;
+import com.google.devtools.build.lib.actions.RunfilesSupplier;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.rules.fileset.FilesetActionContext;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * A helper class for spawn strategies to turn runfiles suppliers into input mappings. This class
+ * performs no I/O operations, but only rearranges the files according to how the runfiles should be
+ * laid out.
+ */
+public class SpawnInputExpander {
+ public static final ActionInput EMPTY_FILE = ActionInputHelper.fromPath("/dev/null");
+
+ private final boolean strict;
+
+ /**
+ * Creates a new instance. If strict is true, then the expander checks for directories in runfiles
+ * and throws an exception if it finds any. Otherwise it silently ignores directories in runfiles
+ * and adds a mapping for them. At this time, directories in filesets are always silently added
+ * as mappings.
+ *
+ * <p>Directories in inputs are a correctness issue: Bazel only tracks dependencies at the action
+ * level, and it does not track dependencies on directories. Making a directory available to a
+ * spawn even though it's contents are not tracked as dependencies leads to incorrect incremental
+ * builds, since changes to the contents do not trigger action invalidation.
+ *
+ * <p>As such, all spawn strategies should always be strict and not make directories available to
+ * the subprocess. However, that's a breaking change, and therefore we make it depend on this flag
+ * for now.
+ */
+ public SpawnInputExpander(boolean strict) {
+ this.strict = strict;
+ }
+
+ private void addMapping(
+ Map<PathFragment, ActionInput> inputMappings,
+ PathFragment targetLocation,
+ ActionInput input) {
+ Preconditions.checkArgument(!targetLocation.isAbsolute(), targetLocation);
+ if (!inputMappings.containsKey(targetLocation)) {
+ inputMappings.put(targetLocation, input);
+ }
+ }
+
+ /** Adds runfiles inputs from runfilesSupplier to inputMappings. */
+ @VisibleForTesting
+ void addRunfilesToInputs(
+ Map<PathFragment, ActionInput> inputMap,
+ RunfilesSupplier runfilesSupplier,
+ ActionInputFileCache actionFileCache) throws IOException {
+ Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings = null;
+ rootsAndMappings = runfilesSupplier.getMappings();
+
+ for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings :
+ rootsAndMappings.entrySet()) {
+ PathFragment root = rootAndMappings.getKey();
+ Preconditions.checkState(!root.isAbsolute(), root);
+ for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) {
+ PathFragment location = root.getRelative(mapping.getKey());
+ Artifact localArtifact = mapping.getValue();
+ if (localArtifact != null) {
+ if (strict && !actionFileCache.isFile(localArtifact)) {
+ throw new IOException("Not a file: " + localArtifact.getPath().getPathString());
+ }
+ addMapping(inputMap, location, localArtifact);
+ } else {
+ addMapping(inputMap, location, EMPTY_FILE);
+ }
+ }
+ }
+ }
+
+ /**
+ * Parses the fileset manifest file, adding to the inputMappings where
+ * appropriate. Lines referring to directories are recursed.
+ */
+ @VisibleForTesting
+ void parseFilesetManifest(
+ Map<PathFragment, ActionInput> inputMappings, Artifact manifest, String workspaceName)
+ throws IOException {
+ Path file = manifest.getRoot().getPath().getRelative(
+ AnalysisUtils.getManifestPathFromFilesetPath(manifest.getExecPath()).getPathString());
+ FileSystemUtils.asByteSource(file).asCharSource(UTF_8)
+ .readLines(new ManifestLineProcessor(inputMappings, workspaceName, manifest.getExecPath()));
+ }
+
+ private final class ManifestLineProcessor implements LineProcessor<Object> {
+ private final Map<PathFragment, ActionInput> inputMap;
+ private final String workspaceName;
+ private final PathFragment targetPrefix;
+ private int lineNum = 0;
+
+ ManifestLineProcessor(
+ Map<PathFragment, ActionInput> inputMap,
+ String workspaceName,
+ PathFragment targetPrefix) {
+ this.inputMap = inputMap;
+ this.workspaceName = workspaceName;
+ this.targetPrefix = targetPrefix;
+ }
+
+ @Override
+ public boolean processLine(String line) throws IOException {
+ if (++lineNum % 2 == 0) {
+ // Digest line, skip.
+ return true;
+ }
+ if (line.isEmpty()) {
+ return true;
+ }
+
+ ActionInput artifact;
+ PathFragment location;
+ int pos = line.indexOf(' ');
+ if (pos == -1) {
+ location = new PathFragment(line);
+ artifact = EMPTY_FILE;
+ } else {
+ String targetPath = line.substring(pos + 1);
+ if (targetPath.charAt(0) != '/') {
+ throw new IOException(String.format("runfiles target is not absolute: %s", targetPath));
+ }
+ artifact = targetPath.isEmpty() ? EMPTY_FILE : ActionInputHelper.fromPath(targetPath);
+
+ location = new PathFragment(line.substring(0, pos));
+ if (!workspaceName.isEmpty()) {
+ if (!location.getSegment(0).equals(workspaceName)) {
+ throw new IOException(
+ String.format(
+ "fileset manifest line must start with '%s': '%s'", workspaceName, location));
+ } else {
+ // Erase "<workspaceName>/".
+ location = location.subFragment(1, location.segmentCount());
+ }
+ }
+ }
+
+ addMapping(inputMap, targetPrefix.getRelative(location), artifact);
+ return true;
+ }
+
+ @Override
+ public Object getResult() {
+ return null; // Unused.
+ }
+ }
+
+ private void addInputs(
+ Map<PathFragment, ActionInput> inputMap, Spawn spawn, ArtifactExpander artifactExpander) {
+ List<ActionInput> inputs =
+ ActionInputHelper.expandArtifacts(spawn.getInputFiles(), artifactExpander);
+ for (ActionInput input : inputs) {
+ addMapping(inputMap, input.getExecPath(), input);
+ }
+ }
+
+ /**
+ * Convert the inputs of the given spawn to a map from exec-root relative paths to action inputs.
+ * In some cases, this generates empty files, for which it uses {@link #EMPTY_FILE}.
+ */
+ public SortedMap<PathFragment, ActionInput> getInputMapping(
+ Spawn spawn, ArtifactExpander artifactExpander, ActionInputFileCache actionInputFileCache,
+ FilesetActionContext filesetContext)
+ throws IOException {
+ TreeMap<PathFragment, ActionInput> inputMap = new TreeMap<>();
+ addInputs(inputMap, spawn, artifactExpander);
+ addRunfilesToInputs(
+ inputMap, spawn.getRunfilesSupplier(), actionInputFileCache);
+ for (Artifact manifest : spawn.getFilesetManifests()) {
+ parseFilesetManifest(inputMap, manifest, filesetContext.getWorkspaceName());
+ }
+ return inputMap;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
index 274377831b..c22be6662d 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
@@ -17,6 +17,8 @@ package com.google.devtools.build.lib.sandbox;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.EnvironmentalExecException;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.SandboxedSpawnActionContext;
@@ -28,12 +30,17 @@ import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.exec.SpawnInputExpander;
+import com.google.devtools.build.lib.rules.fileset.FilesetActionContext;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
/** Abstract common ancestor for sandbox strategies implementing the common parts. */
@@ -44,7 +51,7 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext {
private final Path execRoot;
private final boolean verboseFailures;
private final SandboxOptions sandboxOptions;
- private final SpawnHelpers spawnHelpers;
+ private final SpawnInputExpander spawnInputExpander;
public SandboxStrategy(
BuildRequest buildRequest,
@@ -56,7 +63,7 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext {
this.execRoot = blazeDirs.getExecRoot();
this.verboseFailures = verboseFailures;
this.sandboxOptions = sandboxOptions;
- this.spawnHelpers = new SpawnHelpers(blazeDirs.getExecRoot());
+ this.spawnInputExpander = new SpawnInputExpander(/*strict=*/false);
}
protected void runSpawn(
@@ -145,7 +152,34 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext {
public Map<PathFragment, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext)
throws ExecException {
try {
- return spawnHelpers.getMounts(spawn, executionContext);
+ Map<PathFragment, ActionInput> inputMap = spawnInputExpander
+ .getInputMapping(
+ spawn,
+ executionContext.getArtifactExpander(),
+ executionContext.getActionInputFileCache(),
+ executionContext.getExecutor().getContext(FilesetActionContext.class));
+ // SpawnInputExpander#getInputMapping uses ArtifactExpander#expandArtifacts to expand
+ // middlemen and tree artifacts, which expands empty tree artifacts to no entry. However,
+ // actions that accept TreeArtifacts as inputs generally expect that the empty directory is
+ // created. So we add those explicitly here.
+ // TODO(ulfjack): Move this code to SpawnInputExpander.
+ for (ActionInput input : spawn.getInputFiles()) {
+ if (input instanceof Artifact && ((Artifact) input).isTreeArtifact()) {
+ List<Artifact> containedArtifacts = new ArrayList<>();
+ executionContext.getArtifactExpander().expand((Artifact) input, containedArtifacts);
+ // Attempting to mount a non-empty directory results in ERR_DIRECTORY_NOT_EMPTY, so we
+ // only mount empty TreeArtifacts as directories.
+ if (containedArtifacts.isEmpty()) {
+ inputMap.put(input.getExecPath(), input);
+ }
+ }
+ }
+
+ Map<PathFragment, Path> mounts = new TreeMap<>();
+ for (Map.Entry<PathFragment, ActionInput> e : inputMap.entrySet()) {
+ mounts.put(e.getKey(), execRoot.getRelative(e.getValue().getExecPath()));
+ }
+ return mounts;
} catch (IOException e) {
throw new EnvironmentalExecException("Could not prepare mounts for sandbox execution", e);
}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java b/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java
index 173f1cefa6..a158f02961 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java
@@ -92,7 +92,8 @@ public final class SymlinkedExecRoot implements SandboxExecRoot {
throws IOException {
for (PathFragment inputPath : inputs) {
Path dir = sandboxExecRoot.getRelative(inputPath).getParentDirectory();
- Preconditions.checkArgument(dir.startsWith(sandboxExecRoot));
+ Preconditions.checkArgument(
+ dir.startsWith(sandboxExecRoot), "Bad relative path: '%s'", inputPath);
FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, dir);
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java b/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java
new file mode 100644
index 0000000000..fdfa655371
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java
@@ -0,0 +1,242 @@
+// Copyright 2017 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.exec;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.EmptyRunfilesSupplier;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.actions.RunfilesSupplier;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesSupplierImpl;
+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.PathFragment;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+/**
+ * Tests for {@link SpawnInputExpander}.
+ */
+@RunWith(JUnit4.class)
+public class SpawnInputExpanderTest {
+ private FileSystem fs;
+ private SpawnInputExpander expander;
+ private Map<PathFragment, ActionInput> inputMappings;
+
+ @Before
+ public final void createSpawnInputExpander() throws Exception {
+ fs = new InMemoryFileSystem();
+ expander = new SpawnInputExpander(/*strict=*/true);
+ inputMappings = Maps.newHashMap();
+ }
+
+ private void scratchFile(String file, String... lines) throws Exception {
+ Path path = fs.getPath(file);
+ FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
+ FileSystemUtils.writeLinesAs(path, StandardCharsets.UTF_8, lines);
+ }
+
+ @Test
+ public void testEmptyRunfiles() throws Exception {
+ RunfilesSupplier supplier = EmptyRunfilesSupplier.INSTANCE;
+ expander.addRunfilesToInputs(inputMappings, supplier, null);
+ assertThat(inputMappings).isEmpty();
+ }
+
+ @Test
+ public void testRunfilesSingleFile() throws Exception {
+ Artifact artifact =
+ new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
+ Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build();
+ RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles);
+ ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
+ Mockito.when(mockCache.isFile(artifact)).thenReturn(true);
+
+ expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
+ assertThat(inputMappings).hasSize(1);
+ assertThat(inputMappings)
+ .containsEntry(new PathFragment("runfiles/workspace/dir/file"), artifact);
+ }
+
+ @Test
+ public void testRunfilesDirectoryStrict() throws Exception {
+ Artifact artifact =
+ new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
+ Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build();
+ RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles);
+ ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
+ Mockito.when(mockCache.isFile(artifact)).thenReturn(false);
+
+ try {
+ expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
+ fail();
+ } catch (IOException expected) {
+ assertThat(expected.getMessage().contains("Not a file: /root/dir/file")).isTrue();
+ }
+ }
+
+ @Test
+ public void testRunfilesDirectoryNonStrict() throws Exception {
+ Artifact artifact =
+ new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
+ Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build();
+ RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles);
+ ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
+ Mockito.when(mockCache.isFile(artifact)).thenReturn(false);
+
+ expander = new SpawnInputExpander(/*strict=*/false);
+ expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
+ assertThat(inputMappings).hasSize(1);
+ assertThat(inputMappings)
+ .containsEntry(new PathFragment("runfiles/workspace/dir/file"), artifact);
+ }
+
+ @Test
+ public void testRunfilesTwoFiles() throws Exception {
+ Artifact artifact1 =
+ new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
+ Artifact artifact2 =
+ new Artifact(fs.getPath("/root/dir/baz"), Root.asSourceRoot(fs.getPath("/root")));
+ Runfiles runfiles = new Runfiles.Builder("workspace")
+ .addArtifact(artifact1)
+ .addArtifact(artifact2)
+ .build();
+ RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles);
+ ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
+ Mockito.when(mockCache.isFile(artifact1)).thenReturn(true);
+ Mockito.when(mockCache.isFile(artifact2)).thenReturn(true);
+
+ expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
+ assertThat(inputMappings).hasSize(2);
+ assertThat(inputMappings)
+ .containsEntry(new PathFragment("runfiles/workspace/dir/file"), artifact1);
+ assertThat(inputMappings)
+ .containsEntry(new PathFragment("runfiles/workspace/dir/baz"), artifact2);
+ }
+
+ @Test
+ public void testRunfilesSymlink() throws Exception {
+ Artifact artifact =
+ new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
+ Runfiles runfiles = new Runfiles.Builder("workspace")
+ .addSymlink(new PathFragment("symlink"), artifact).build();
+ RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles);
+ ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
+ Mockito.when(mockCache.isFile(artifact)).thenReturn(true);
+
+ expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
+ assertThat(inputMappings).hasSize(1);
+ assertThat(inputMappings)
+ .containsEntry(new PathFragment("runfiles/workspace/symlink"), artifact);
+ }
+
+ @Test
+ public void testRunfilesRootSymlink() throws Exception {
+ Artifact artifact =
+ new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
+ Runfiles runfiles = new Runfiles.Builder("workspace")
+ .addRootSymlink(new PathFragment("symlink"), artifact).build();
+ RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles);
+ ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
+ Mockito.when(mockCache.isFile(artifact)).thenReturn(true);
+
+ expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
+ assertThat(inputMappings).hasSize(2);
+ assertThat(inputMappings).containsEntry(new PathFragment("runfiles/symlink"), artifact);
+ // If there's no other entry, Runfiles adds an empty file in the workspace to make sure the
+ // directory gets created.
+ assertThat(inputMappings)
+ .containsEntry(
+ new PathFragment("runfiles/workspace/.runfile"), SpawnInputExpander.EMPTY_FILE);
+ }
+
+ @Test
+ public void testEmptyManifest() throws Exception {
+ // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST".
+ scratchFile("/root/_foo/MANIFEST");
+
+ Artifact artifact =
+ new Artifact(fs.getPath("/root/foo"), Root.asSourceRoot(fs.getPath("/root")));
+ expander.parseFilesetManifest(inputMappings, artifact, "workspace");
+ assertThat(inputMappings).isEmpty();
+ }
+
+ @Test
+ public void testManifestWithSingleFile() throws Exception {
+ // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST".
+ scratchFile(
+ "/root/_foo/MANIFEST",
+ "workspace/bar /dir/file",
+ "<some digest>");
+
+ Artifact artifact =
+ new Artifact(fs.getPath("/root/foo"), Root.asSourceRoot(fs.getPath("/root")));
+ expander.parseFilesetManifest(inputMappings, artifact, "workspace");
+ assertThat(inputMappings).hasSize(1);
+ assertThat(inputMappings)
+ .containsEntry(new PathFragment("foo/bar"), ActionInputHelper.fromPath("/dir/file"));
+ }
+
+ @Test
+ public void testManifestWithTwoFiles() throws Exception {
+ // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST".
+ scratchFile(
+ "/root/_foo/MANIFEST",
+ "workspace/bar /dir/file",
+ "<some digest>",
+ "workspace/baz /dir/file",
+ "<some digest>");
+
+ Artifact artifact =
+ new Artifact(fs.getPath("/root/foo"), Root.asSourceRoot(fs.getPath("/root")));
+ expander.parseFilesetManifest(inputMappings, artifact, "workspace");
+ assertThat(inputMappings).hasSize(2);
+ assertThat(inputMappings)
+ .containsEntry(new PathFragment("foo/bar"), ActionInputHelper.fromPath("/dir/file"));
+ assertThat(inputMappings)
+ .containsEntry(new PathFragment("foo/baz"), ActionInputHelper.fromPath("/dir/file"));
+ }
+
+ @Test
+ public void testManifestWithDirectory() throws Exception {
+ // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST".
+ scratchFile(
+ "/root/_foo/MANIFEST",
+ "workspace/bar /some",
+ "<some digest>");
+
+ Artifact artifact =
+ new Artifact(fs.getPath("/root/foo"), Root.asSourceRoot(fs.getPath("/root")));
+ expander.parseFilesetManifest(inputMappings, artifact, "workspace");
+ assertThat(inputMappings).hasSize(1);
+ assertThat(inputMappings)
+ .containsEntry(
+ new PathFragment("foo/bar"), ActionInputHelper.fromPath("/some"));
+ }
+}