sandbox: Better error messages and the noisy debug logs of the namespace-runner now have to be explicitly activated via --sandbox_debug.

Fixes #424.

--
MOS_MIGRATED_REVID=102566625
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java
index d1a1783..3baa7ad 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java
@@ -16,7 +16,6 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicates;
-import com.google.common.base.Throwables;
 import com.google.common.collect.ForwardingMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.Files;
@@ -33,14 +32,8 @@
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.config.RunUnder;
-import com.google.devtools.build.lib.events.Event;
-import com.google.devtools.build.lib.events.EventHandler;
 import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
 import com.google.devtools.build.lib.rules.test.TestRunnerAction;
-import com.google.devtools.build.lib.runtime.BlazeRuntime;
-import com.google.devtools.build.lib.shell.AbnormalTerminationException;
-import com.google.devtools.build.lib.shell.CommandException;
-import com.google.devtools.build.lib.shell.TerminationStatus;
 import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy;
 import com.google.devtools.build.lib.syntax.Label;
 import com.google.devtools.build.lib.unix.FilesystemUtils;
@@ -70,10 +63,11 @@
 public class LinuxSandboxedStrategy implements SpawnActionContext {
   private final ExecutorService backgroundWorkers;
 
-  private final BlazeRuntime blazeRuntime;
+  private final ImmutableMap<String, String> clientEnv;
   private final BlazeDirectories blazeDirs;
   private final Path execRoot;
   private final boolean verboseFailures;
+  private final boolean sandboxDebug;
   private final StandaloneSpawnStrategy standaloneStrategy;
   private final UUID uuid = UUID.randomUUID();
   private final AtomicInteger execCounter = new AtomicInteger();
@@ -112,12 +106,17 @@
   }
 
   public LinuxSandboxedStrategy(
-      BlazeRuntime blazeRuntime, boolean verboseFailures, ExecutorService backgroundWorkers) {
-    this.blazeRuntime = blazeRuntime;
-    this.blazeDirs = blazeRuntime.getDirectories();
+      Map<String, String> clientEnv,
+      BlazeDirectories blazeDirs,
+      ExecutorService backgroundWorkers,
+      boolean verboseFailures,
+      boolean sandboxDebug) {
+    this.clientEnv = ImmutableMap.copyOf(clientEnv);
+    this.blazeDirs = blazeDirs;
     this.execRoot = blazeDirs.getExecRoot();
-    this.verboseFailures = verboseFailures;
     this.backgroundWorkers = backgroundWorkers;
+    this.verboseFailures = verboseFailures;
+    this.sandboxDebug = sandboxDebug;
     this.standaloneStrategy = new StandaloneSpawnStrategy(blazeDirs.getExecRoot(), verboseFailures);
   }
 
@@ -163,7 +162,7 @@
 
     try {
       final NamespaceSandboxRunner runner =
-          new NamespaceSandboxRunner(execRoot, sandboxPath, mounts, verboseFailures);
+          new NamespaceSandboxRunner(execRoot, sandboxPath, mounts, verboseFailures, sandboxDebug);
       try {
         runner.run(
             spawn.getArguments(),
@@ -197,18 +196,8 @@
               }
             });
       }
-    } catch (AbnormalTerminationException e) {
-      TerminationStatus status = e.getResult().getTerminationStatus();
-      boolean timedOut = !status.exited() && (status.getTerminatingSignal() == 14 /* SIGALRM */);
-      throw new UserExecException("Error during execution of spawn", e, timedOut);
-    } catch (CommandException e) {
-      throw new UserExecException("Error during execution of spawn", e);
     } catch (IOException e) {
-      EventHandler handler = actionExecutionContext.getExecutor().getEventHandler();
-      handler.handle(
-          Event.error(
-              "I/O error during sandboxed execution:\n" + Throwables.getStackTraceAsString(e)));
-      throw new UserExecException("Could not execute spawn", e);
+      throw new UserExecException("I/O error during sandboxed execution", e);
     }
   }
 
@@ -477,7 +466,7 @@
           source = blazeDirs.getExecRoot().getRelative(sourceFragment);
         } else {
           List<Path> searchPath =
-              SearchPath.parse(blazeDirs.getFileSystem(), blazeRuntime.getClientEnv().get("PATH"));
+              SearchPath.parse(blazeDirs.getFileSystem(), clientEnv.get("PATH"));
           source = SearchPath.which(searchPath, runUnder.getCommand());
         }
         if (source != null) {
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java
index 1667a03..1c2034a 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java
@@ -18,11 +18,15 @@
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Files;
 import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.analysis.config.BinTools;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.shell.AbnormalTerminationException;
 import com.google.devtools.build.lib.shell.Command;
 import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.shell.TerminationStatus;
 import com.google.devtools.build.lib.unix.FilesystemUtils;
+import com.google.devtools.build.lib.util.CommandFailureUtils;
 import com.google.devtools.build.lib.util.OsUtils;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
@@ -46,15 +50,21 @@
   private final Path sandboxPath;
   private final Path sandboxExecRoot;
   private final ImmutableMap<Path, Path> mounts;
-  private final boolean debug;
+  private final boolean verboseFailures;
+  private final boolean sandboxDebug;
 
   public NamespaceSandboxRunner(
-      Path execRoot, Path sandboxPath, ImmutableMap<Path, Path> mounts, boolean debug) {
+      Path execRoot,
+      Path sandboxPath,
+      ImmutableMap<Path, Path> mounts,
+      boolean verboseFailures,
+      boolean sandboxDebug) {
     this.execRoot = execRoot;
     this.sandboxPath = sandboxPath;
     this.sandboxExecRoot = sandboxPath.getRelative(execRoot.asFragment().relativeTo("/"));
     this.mounts = mounts;
-    this.debug = debug;
+    this.verboseFailures = verboseFailures;
+    this.sandboxDebug = sandboxDebug;
   }
 
   static boolean isSupported(BlazeRuntime runtime) {
@@ -99,14 +109,14 @@
       FileOutErr outErr,
       Collection<? extends ActionInput> outputs,
       int timeout)
-      throws IOException, CommandException {
+      throws IOException, UserExecException {
     createFileSystem(outputs);
 
     List<String> args = new ArrayList<>();
 
     args.add(execRoot.getRelative("_bin/namespace-sandbox").getPathString());
 
-    if (debug) {
+    if (sandboxDebug) {
       args.add("-D");
     }
 
@@ -137,12 +147,25 @@
 
     Command cmd = new Command(args.toArray(new String[0]), env, cwd);
 
-    cmd.execute(
-        /* stdin */ new byte[] {},
-        Command.NO_OBSERVER,
-        outErr.getOutputStream(),
-        outErr.getErrorStream(),
-        /* killSubprocessOnInterrupt */ true);
+    try {
+      cmd.execute(
+          /* stdin */ new byte[] {},
+          Command.NO_OBSERVER,
+          outErr.getOutputStream(),
+          outErr.getErrorStream(),
+          /* killSubprocessOnInterrupt */ true);
+    } catch (CommandException e) {
+      boolean timedOut = false;
+      if (e instanceof AbnormalTerminationException) {
+        TerminationStatus status =
+            ((AbnormalTerminationException) e).getResult().getTerminationStatus();
+        timedOut = !status.exited() && (status.getTerminatingSignal() == 14 /* SIGALRM */);
+      }
+      String message =
+          CommandFailureUtils.describeCommandFailure(
+              verboseFailures, spawnArguments, env, cwd.getPath());
+      throw new UserExecException(message, e, timedOut);
+    }
 
     copyOutputs(outputs);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
index 20b64c8..61cd2cd 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
@@ -35,10 +35,17 @@
   public SandboxActionContextProvider(
       BlazeRuntime runtime, BuildRequest buildRequest, ExecutorService backgroundWorkers) {
     boolean verboseFailures = buildRequest.getOptions(ExecutionOptions.class).verboseFailures;
+    boolean sandboxDebug = buildRequest.getOptions(SandboxOptions.class).sandboxDebug;
     Builder<ActionContext> strategies = ImmutableList.builder();
 
     if (OS.getCurrent() == OS.LINUX) {
-      strategies.add(new LinuxSandboxedStrategy(runtime, verboseFailures, backgroundWorkers));
+      strategies.add(
+          new LinuxSandboxedStrategy(
+              runtime.getClientEnv(),
+              runtime.getDirectories(),
+              backgroundWorkers,
+              verboseFailures,
+              sandboxDebug));
     }
 
     this.strategies = strategies.build();
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
index ad4fa50..4c13ddb 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
@@ -28,4 +28,14 @@
     help = "Do not print a warning when sandboxed execution is not supported on this system."
   )
   public boolean ignoreUnsupportedSandboxing;
+
+  @Option(
+    name = "sandbox_debug",
+    defaultValue = "false",
+    category = "strategy",
+    help =
+        "Let the sandbox print debug information on execution. This might help developers of "
+            + "Bazel or Skylark rules with debugging failures due to missing input files, etc."
+  )
+  public boolean sandboxDebug;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java
index ed034d5..ae03d19 100644
--- a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java
@@ -41,7 +41,6 @@
 @ExecutionStrategy(name = { "standalone" }, contextType = SpawnActionContext.class)
 public class StandaloneSpawnStrategy implements SpawnActionContext {
   private final boolean verboseFailures;
-
   private final Path processWrapper;
 
   public StandaloneSpawnStrategy(Path execRoot, boolean verboseFailures) {
@@ -70,7 +69,7 @@
       try {
         timeout = Integer.parseInt(timeoutStr);
       } catch (NumberFormatException e) {
-        throw new UserExecException("could not parse timeout: " + e);
+        throw new UserExecException("could not parse timeout: ", e);
       }
     }
 
@@ -115,7 +114,7 @@
     } catch (CommandException e) {
       String message = CommandFailureUtils.describeCommandFailure(
           verboseFailures, spawn.getArguments(), spawn.getEnvironment(), cwd);
-      throw new UserExecException(String.format("%s: %s", message, e));
+      throw new UserExecException(message, e);
     }
   }
 
diff --git a/src/test/java/BUILD b/src/test/java/BUILD
index 8315e1c..9f3ead7 100644
--- a/src/test/java/BUILD
+++ b/src/test/java/BUILD
@@ -586,8 +586,15 @@
     args = ["com.google.devtools.build.lib.AllTests"],
     data = [":embedded_scripts"],
     deps = [
+        ":actions_testutil",
         ":analysis_testutil",
+        ":foundations_testutil",
         ":testutil",
+        "//src/main/java:actions",
+        "//src/main/java:analysis-exec-rules-skyframe",
+        "//src/main/java:events",
+        "//src/main/java:options",
+        "//src/main/java:shell",
         "//src/main/java:vfs",
         "//src/main/java/com/google/devtools/build/lib/sandbox",
         "//third_party:guava",
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java
index 74333e3..f91bb0d 100644
--- a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java
@@ -20,20 +20,14 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.sandbox.LinuxSandboxedStrategy.MountMap;
-import com.google.devtools.build.lib.testutil.TestUtils;
-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.UnixFileSystem;
 
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.io.File;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
@@ -53,28 +47,7 @@
  * tree of files given only the set of input files.
  */
 @RunWith(JUnit4.class)
-public class LinuxSandboxedStrategyTest {
-  private FileSystem testFS;
-  private Path workingDir;
-  private Path fakeSandboxDir;
-
-  @Before
-  public void setUp() throws Exception {
-    testFS = new UnixFileSystem();
-    workingDir = testFS.getPath(new File(TestUtils.tmpDir()).getCanonicalPath());
-    fakeSandboxDir = workingDir.getRelative("sandbox");
-    fakeSandboxDir.createDirectory();
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    FileSystemUtils.deleteTreesBelow(workingDir);
-  }
-
-  private Path getSandboxPath(Path entry) {
-    return fakeSandboxDir.getRelative(entry.asFragment().relativeTo("/"));
-  }
-
+public class LinuxSandboxedStrategyTest extends LinuxSandboxedStrategyTestCase {
   /**
    * Strips the working directory (which can be very long) from the file names in the input map, to
    * make assertion failures easier to read.
@@ -82,16 +55,73 @@
   private ImmutableMap<String, String> userFriendlyMap(Map<Path, Path> input) {
     ImmutableMap.Builder<String, String> userFriendlyMap = ImmutableMap.builder();
     for (Entry<Path, Path> entry : input.entrySet()) {
-      String key = entry.getKey().getPathString().replace(workingDir.getPathString(), "");
-      String value = entry.getValue().getPathString().replace(workingDir.getPathString(), "");
+      String key = entry.getKey().getPathString().replace(workspaceDir.getPathString(), "");
+      String value = entry.getValue().getPathString().replace(workspaceDir.getPathString(), "");
       userFriendlyMap.put(key, value);
     }
     return userFriendlyMap.build();
   }
 
+  /**
+   * Takes a map of file specifications, creates the necessary files / symlinks / dirs,
+   * mounts files listed in customMount at their canonical location in the sandbox and returns the
+   * output of {@code LinuxSandboxedStrategy#fixMounts} for it.
+   */
+  private ImmutableMap<String, String> userFriendlyMounts(
+      Map<String, String> linksAndFiles, List<String> customMounts) throws IOException {
+    return userFriendlyMap(mounts(linksAndFiles, customMounts));
+  }
+
+  private ImmutableMap<Path, Path> mounts(
+      Map<String, String> linksAndFiles, List<String> customMounts) throws IOException {
+    createTreeStructure(linksAndFiles);
+
+    ImmutableMap.Builder<Path, Path> mounts = ImmutableMap.builder();
+    for (String customMount : customMounts) {
+      Path customMountPath = workspaceDir.getRelative(customMount);
+      mounts.put(getSandboxPath(customMountPath), customMountPath);
+    }
+    return LinuxSandboxedStrategy.validateMounts(
+        fakeSandboxDir,
+        LinuxSandboxedStrategy.withResolvedSymlinks(
+            fakeSandboxDir, LinuxSandboxedStrategy.withRecursedDirs(mounts.build())));
+  }
+
+  /**
+   * Takes a map of file specifications, creates the necessary files / symlinks / dirs,
+   * mounts the first file of the specification at its canonical location in the sandbox and returns
+   * the output of {@code LinuxSandboxedStrategy#fixMounts} for it.
+   */
+  private Map<String, String> userFriendlyMounts(Map<String, String> linksAndFiles)
+      throws IOException {
+    return userFriendlyMap(mounts(linksAndFiles));
+  }
+
+  private Map<Path, Path> mounts(Map<String, String> linksAndFiles) throws IOException {
+    return mounts(
+        linksAndFiles, ImmutableList.of(Iterables.getFirst(linksAndFiles.keySet(), null)));
+  }
+
+  /**
+   * Returns a map of mount entries for a list files, which can be used to assert that all
+   * expected mounts have been made by the LinuxSandboxedStrategy.
+   */
+  private ImmutableMap<String, String> userFriendlyAsserts(List<String> asserts) {
+    return userFriendlyMap(asserts(asserts));
+  }
+
+  private ImmutableMap<Path, Path> asserts(List<String> asserts) {
+    ImmutableMap.Builder<Path, Path> pathifiedAsserts = ImmutableMap.builder();
+    for (String fileName : asserts) {
+      Path inputPath = workspaceDir.getRelative(fileName);
+      pathifiedAsserts.put(getSandboxPath(inputPath), inputPath);
+    }
+    return pathifiedAsserts.build();
+  }
+
   private void createTreeStructure(Map<String, String> linksAndFiles) throws IOException {
     for (Entry<String, String> entry : linksAndFiles.entrySet()) {
-      Path filePath = workingDir.getRelative(entry.getKey());
+      Path filePath = workspaceDir.getRelative(entry.getKey());
       String linkTarget = entry.getValue();
 
       FileSystemUtils.createDirectoryAndParents(filePath.getParentDirectory());
@@ -106,65 +136,8 @@
     }
   }
 
-  /**
-   * Takes a map of file specifications, creates the necessary files / symlinks / dirs,
-   * mounts files listed in customMount at their canonical location in the sandbox and returns the
-   * output of {@code LinuxSandboxedStrategy#fixMounts} for it.
-   */
-  private ImmutableMap<Path, Path> mounts(
-      Map<String, String> linksAndFiles, List<String> customMounts) throws IOException {
-    createTreeStructure(linksAndFiles);
-
-    ImmutableMap.Builder<Path, Path> mounts = ImmutableMap.builder();
-    for (String customMount : customMounts) {
-      Path customMountPath = workingDir.getRelative(customMount);
-      mounts.put(getSandboxPath(customMountPath), customMountPath);
-    }
-    return LinuxSandboxedStrategy.validateMounts(
-        fakeSandboxDir,
-        LinuxSandboxedStrategy.withResolvedSymlinks(
-            fakeSandboxDir, LinuxSandboxedStrategy.withRecursedDirs(mounts.build())));
-  }
-
-  private ImmutableMap<String, String> userFriendlyMounts(
-      Map<String, String> linksAndFiles, List<String> customMounts) throws IOException {
-    return userFriendlyMap(mounts(linksAndFiles, customMounts));
-  }
-
-  /**
-   * Takes a map of file specifications, creates the necessary files / symlinks / dirs,
-   * mounts the first file of the specification at its canonical location in the sandbox and returns
-   * the output of {@code LinuxSandboxedStrategy#fixMounts} for it.
-   */
-  private Map<Path, Path> mounts(Map<String, String> linksAndFiles) throws IOException {
-    return mounts(
-        linksAndFiles, ImmutableList.of(Iterables.getFirst(linksAndFiles.keySet(), null)));
-  }
-
-  private Map<String, String> userFriendlyMounts(Map<String, String> linksAndFiles)
-      throws IOException {
-    return userFriendlyMap(mounts(linksAndFiles));
-  }
-
-  /**
-   * Returns a map of mount entries for a list files, which can be used to assert that all
-   * expected mounts have been made by the LinuxSandboxedStrategy.
-   */
-  private ImmutableMap<Path, Path> asserts(List<String> asserts) {
-    ImmutableMap.Builder<Path, Path> pathifiedAsserts = ImmutableMap.builder();
-    for (String fileName : asserts) {
-      Path inputPath = workingDir.getRelative(fileName);
-      pathifiedAsserts.put(getSandboxPath(inputPath), inputPath);
-    }
-    return pathifiedAsserts.build();
-  }
-
-  private ImmutableMap<String, String> userFriendlyAsserts(List<String> asserts) {
-    return userFriendlyMap(asserts(asserts));
-  }
-
   @Test
-  public void resolvesRelativeFileToFileSymlinkInSameDir() throws IOException {
+  public void testResolvesRelativeFileToFileSymlinkInSameDir() throws IOException {
     Map<String, String> testFiles = new LinkedHashMap<>();
     testFiles.put("symlink.txt", "goal.txt");
     testFiles.put("goal.txt", "");
@@ -177,7 +150,7 @@
   }
 
   @Test
-  public void resolvesRelativeFileToFileSymlinkInSubDir() throws IOException {
+  public void testResolvesRelativeFileToFileSymlinkInSubDir() throws IOException {
     Map<String, String> testFiles =
         ImmutableMap.of(
             "symlink.txt", "x/goal.txt",
@@ -188,7 +161,7 @@
   }
 
   @Test
-  public void resolvesRelativeFileToFileSymlinkInParentDir() throws IOException {
+  public void testResolvesRelativeFileToFileSymlinkInParentDir() throws IOException {
     Map<String, String> testFiles =
         ImmutableMap.of(
             "x/symlink.txt", "../goal.txt",
@@ -200,7 +173,7 @@
   }
 
   @Test
-  public void recursesSubDirs() throws IOException {
+  public void testRecursesSubDirs() throws IOException {
     ImmutableList<String> inputFile = ImmutableList.of("a/b");
 
     Map<String, String> testFiles =
@@ -219,7 +192,7 @@
    * Test that the algorithm correctly identifies and refuses symlink loops.
    */
   @Test
-  public void catchesSymlinkLoop() throws IOException {
+  public void testCatchesSymlinkLoop() throws IOException {
     try {
       mounts(
           ImmutableMap.of(
@@ -231,7 +204,7 @@
           .hasMessage(
               String.format(
                   "%s (Too many levels of symbolic links)",
-                  workingDir.getRelative("a").getPathString()));
+                  workspaceDir.getRelative("a").getPathString()));
     }
   }
 
@@ -240,7 +213,7 @@
    * directories (e.g. "a -> dir/file/file").
    */
   @Test
-  public void catchesIllegalSymlink() throws IOException {
+  public void testCatchesIllegalSymlink() throws IOException {
     try {
       mounts(
           ImmutableMap.of(
@@ -250,19 +223,20 @@
     } catch (IOException e) {
       assertThat(e)
           .hasMessage(
-              String.format("%s (Not a directory)", workingDir.getRelative("a/c").getPathString()));
+              String.format(
+                  "%s (Not a directory)", workspaceDir.getRelative("a/c").getPathString()));
     }
   }
 
   @Test
   public void testParseManifestFile() throws IOException {
-    Path targetDir = workingDir.getRelative("runfiles");
+    Path targetDir = workspaceDir.getRelative("runfiles");
     targetDir.createDirectory();
 
-    Path testFile = workingDir.getRelative("testfile");
+    Path testFile = workspaceDir.getRelative("testfile");
     FileSystemUtils.createEmptyFile(testFile);
 
-    Path manifestFile = workingDir.getRelative("MANIFEST");
+    Path manifestFile = workspaceDir.getRelative("MANIFEST");
     FileSystemUtils.writeContent(
         manifestFile,
         Charset.defaultCharset(),
@@ -279,47 +253,47 @@
                     fakeSandboxDir.getRelative("runfiles/x/testfile"),
                     testFile,
                     fakeSandboxDir.getRelative("runfiles/x/emptyfile"),
-                    testFS.getPath("/dev/null"))));
+                    fileSystem.getPath("/dev/null"))));
   }
 
   @Test
   public void testMountMapWithNormalMounts() throws IOException {
     // Allowed: Just two normal mounts (a -> sandbox/a, b -> sandbox/b)
     MountMap<Path, Path> mounts = new MountMap<>();
-    mounts.put(fakeSandboxDir.getRelative("a"), workingDir.getRelative("a"));
-    mounts.put(fakeSandboxDir.getRelative("b"), workingDir.getRelative("b"));
+    mounts.put(fakeSandboxDir.getRelative("a"), workspaceDir.getRelative("a"));
+    mounts.put(fakeSandboxDir.getRelative("b"), workspaceDir.getRelative("b"));
     assertThat(mounts)
         .isEqualTo(
             ImmutableMap.of(
-                fakeSandboxDir.getRelative("a"), workingDir.getRelative("a"),
-                fakeSandboxDir.getRelative("b"), workingDir.getRelative("b")));
+                fakeSandboxDir.getRelative("a"), workspaceDir.getRelative("a"),
+                fakeSandboxDir.getRelative("b"), workspaceDir.getRelative("b")));
   }
 
   @Test
   public void testMountMapWithSameMountTwice() throws IOException {
     // Allowed: Mount same thing twice (a -> sandbox/a, a -> sandbox/a, b -> sandbox/b)
     MountMap<Path, Path> mounts = new MountMap<>();
-    mounts.put(fakeSandboxDir.getRelative("a"), workingDir.getRelative("a"));
-    mounts.put(fakeSandboxDir.getRelative("a"), workingDir.getRelative("a"));
-    mounts.put(fakeSandboxDir.getRelative("b"), workingDir.getRelative("b"));
+    mounts.put(fakeSandboxDir.getRelative("a"), workspaceDir.getRelative("a"));
+    mounts.put(fakeSandboxDir.getRelative("a"), workspaceDir.getRelative("a"));
+    mounts.put(fakeSandboxDir.getRelative("b"), workspaceDir.getRelative("b"));
     assertThat(mounts)
         .isEqualTo(
             ImmutableMap.of(
-                fakeSandboxDir.getRelative("a"), workingDir.getRelative("a"),
-                fakeSandboxDir.getRelative("b"), workingDir.getRelative("b")));
+                fakeSandboxDir.getRelative("a"), workspaceDir.getRelative("a"),
+                fakeSandboxDir.getRelative("b"), workspaceDir.getRelative("b")));
   }
 
   @Test
   public void testMountMapWithOneThingTwoTargets() throws IOException {
     // Allowed: Mount one thing in two targets (x -> sandbox/a, x -> sandbox/b)
     MountMap<Path, Path> mounts = new MountMap<>();
-    mounts.put(fakeSandboxDir.getRelative("a"), workingDir.getRelative("x"));
-    mounts.put(fakeSandboxDir.getRelative("b"), workingDir.getRelative("x"));
+    mounts.put(fakeSandboxDir.getRelative("a"), workspaceDir.getRelative("x"));
+    mounts.put(fakeSandboxDir.getRelative("b"), workspaceDir.getRelative("x"));
     assertThat(mounts)
         .isEqualTo(
             ImmutableMap.of(
-                fakeSandboxDir.getRelative("a"), workingDir.getRelative("x"),
-                fakeSandboxDir.getRelative("b"), workingDir.getRelative("x")));
+                fakeSandboxDir.getRelative("a"), workspaceDir.getRelative("x"),
+                fakeSandboxDir.getRelative("b"), workspaceDir.getRelative("x")));
   }
 
   @Test
@@ -327,17 +301,18 @@
     // Forbidden: Mount two things onto the same target (x -> sandbox/a, y -> sandbox/a)
     try {
       MountMap<Path, Path> mounts = new MountMap<>();
-      mounts.put(fakeSandboxDir.getRelative("x"), workingDir.getRelative("a"));
-      mounts.put(fakeSandboxDir.getRelative("x"), workingDir.getRelative("b"));
+      mounts.put(fakeSandboxDir.getRelative("x"), workspaceDir.getRelative("a"));
+      mounts.put(fakeSandboxDir.getRelative("x"), workspaceDir.getRelative("b"));
       fail();
     } catch (IllegalArgumentException e) {
       assertThat(e)
           .hasMessage(
               String.format(
                   "Cannot mount both '%s' and '%s' onto '%s'",
-                  workingDir.getRelative("a"),
-                  workingDir.getRelative("b"),
+                  workspaceDir.getRelative("a"),
+                  workspaceDir.getRelative("b"),
                   fakeSandboxDir.getRelative("x")));
     }
   }
+
 }
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java
new file mode 100644
index 0000000..7fb6c8b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java
@@ -0,0 +1,126 @@
+// Copyright 2015 Google Inc. 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.sandbox;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.eventbus.EventBus;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.devtools.build.lib.actions.ActionContextProvider;
+import com.google.devtools.build.lib.actions.BlazeExecutor;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.events.PrintingEventHandler;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.testutil.BlazeTestUtils;
+import com.google.devtools.build.lib.testutil.TestFileOutErr;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.util.BlazeClock;
+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.util.FileSystems;
+import com.google.devtools.common.options.OptionsParser;
+
+import org.junit.Before;
+
+import java.io.IOException;
+
+/**
+ * Common parts of all {@link LinuxSandboxedStrategy} tests.
+ */
+public class LinuxSandboxedStrategyTestCase {
+  private Reporter reporter = new Reporter(PrintingEventHandler.ERRORS_AND_WARNINGS_TO_STDERR);
+  private Path outputBase;
+
+  protected FileSystem fileSystem;
+  protected Path workspaceDir;
+  protected Path fakeSandboxDir;
+  protected Path fakeSandboxExecRoot;
+
+  protected BlazeExecutor executor;
+  protected BlazeDirectories blazeDirs;
+
+  protected TestFileOutErr outErr = new TestFileOutErr();
+
+  protected String out() {
+    return outErr.outAsLatin1();
+  }
+
+  protected String err() {
+    return outErr.errAsLatin1();
+  }
+
+  protected Path getSandboxPath(Path entry) {
+    return fakeSandboxDir.getRelative(entry.asFragment().relativeTo("/"));
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    Path testRoot = createTestRoot();
+
+    workspaceDir = testRoot.getRelative("workspace");
+    workspaceDir.createDirectory();
+
+    outputBase = testRoot.getRelative("outputBase");
+    outputBase.createDirectory();
+
+    fakeSandboxDir = testRoot.getRelative("sandbox");
+    fakeSandboxDir.createDirectory();
+
+    blazeDirs = new BlazeDirectories(outputBase, outputBase, workspaceDir);
+    BlazeTestUtils.getIntegrationBinTools(blazeDirs);
+
+    OptionsParser optionsParser =
+        OptionsParser.newOptionsParser(ExecutionOptions.class, SandboxOptions.class);
+    optionsParser.parse("--verbose_failures");
+
+    EventBus bus = new EventBus();
+
+    this.executor =
+        new BlazeExecutor(
+            blazeDirs.getExecRoot(),
+            blazeDirs.getOutputPath(),
+            reporter,
+            bus,
+            BlazeClock.instance(),
+            optionsParser,
+            /* verboseFailures */ true,
+            /* showSubcommands */ false,
+            ImmutableList.<ActionContext>of(),
+            ImmutableMap.<String, SpawnActionContext>of(
+                "",
+                new LinuxSandboxedStrategy(
+                    ImmutableMap.<String, String>of(),
+                    blazeDirs,
+                    MoreExecutors.newDirectExecutorService(),
+                    true,
+                    false)),
+            ImmutableList.<ActionContextProvider>of());
+  }
+
+  private Path createTestRoot() throws IOException {
+    fileSystem = FileSystems.initDefaultAsNative();
+    Path testRoot = fileSystem.getPath(TestUtils.tmpDir());
+    try {
+      FileSystemUtils.deleteTreesBelow(testRoot);
+    } catch (IOException e) {
+      System.err.println("Failed to remove directory " + testRoot + ": " + e.getMessage());
+      throw e;
+    }
+    return testRoot;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/LocalLinuxSandboxedStrategyTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/LocalLinuxSandboxedStrategyTest.java
new file mode 100644
index 0000000..7563d1f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/LocalLinuxSandboxedStrategyTest.java
@@ -0,0 +1,93 @@
+// Copyright 2015 Google Inc. 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.sandbox;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionMetadata;
+import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.exec.SingleBuildFileCache;
+import com.google.devtools.build.lib.shell.BadExitStatusException;
+import com.google.devtools.build.lib.testutil.TestSpec;
+import com.google.devtools.build.lib.util.CommandFailureUtils;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.vfs.Path;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Tests for {@code LinuxSandboxedStrategy} that must run locally, because they need to actually
+ * run the namespace-sandbox binary.
+ */
+@TestSpec(localOnly = true, supportedOs = OS.LINUX)
+@RunWith(JUnit4.class)
+public class LocalLinuxSandboxedStrategyTest extends LinuxSandboxedStrategyTestCase {
+  protected Spawn createSpawn(String... arguments) {
+    Map<String, String> environment = ImmutableMap.<String, String>of();
+    Map<String, String> executionInfo = ImmutableMap.<String, String>of();
+    ActionMetadata action = new ActionsTestUtil.NullAction();
+    ResourceSet localResources = ResourceSet.ZERO;
+    return new BaseSpawn(
+        Arrays.asList(arguments), environment, executionInfo, action, localResources);
+  }
+
+  protected ActionExecutionContext createContext() {
+    Path execRoot = executor.getExecRoot();
+    return new ActionExecutionContext(
+        executor,
+        new SingleBuildFileCache(execRoot.getPathString(), execRoot.getFileSystem()),
+        null,
+        outErr,
+        null);
+  }
+
+  @Test
+  public void testExecutionSuccess() throws Exception {
+    Spawn spawn = createSpawn("/bin/sh", "-c", "echo Hello, world.; touch dummy");
+    executor.getSpawnActionContext(spawn.getMnemonic()).exec(spawn, createContext());
+    assertThat(out()).isEqualTo("Hello, world.\n");
+    assertThat(err()).isEmpty();
+  }
+
+  @Test
+  public void testExecutionFailurePrintsCorrectMessage() throws Exception {
+    Spawn spawn = createSpawn("/bin/sh", "-c", "echo ERROR >&2; exit 1");
+    try {
+      executor.getSpawnActionContext(spawn.getMnemonic()).exec(spawn, createContext());
+      fail();
+    } catch (UserExecException e) {
+      assertThat(err()).isEqualTo("ERROR\n");
+      assertThat(e.getMessage())
+          .startsWith(
+              CommandFailureUtils.describeCommandFailure(
+                  true,
+                  spawn.getArguments(),
+                  spawn.getEnvironment(),
+                  blazeDirs.getExecRoot().toString()));
+      assertThat(e.getCause()).isInstanceOf(BadExitStatusException.class);
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/SandboxLocalTests.java b/src/test/java/com/google/devtools/build/lib/sandbox/SandboxLocalTests.java
new file mode 100644
index 0000000..5a72ade
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/SandboxLocalTests.java
@@ -0,0 +1,38 @@
+// Copyright 2015 Google Inc. 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.sandbox;
+
+import com.google.common.base.Predicates;
+import com.google.devtools.build.lib.testutil.BlazeTestSuiteBuilder;
+import com.google.devtools.build.lib.testutil.CustomSuite;
+
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+/**
+ * Test suite that runs all tests that are local-only.
+ */
+@RunWith(CustomSuite.class)
+public class SandboxLocalTests extends BlazeTestSuiteBuilder {
+  public static Set<Class<?>> suite() {
+    return new SandboxLocalTests()
+        .getBuilder()
+        .matchClasses(
+            Predicates.and(
+                BlazeTestSuiteBuilder.TEST_IS_LOCAL_ONLY,
+                BlazeTestSuiteBuilder.TEST_SUPPORTS_CURRENT_OS))
+        .create();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/SandboxTests.java b/src/test/java/com/google/devtools/build/lib/sandbox/SandboxTests.java
new file mode 100644
index 0000000..ee676cc
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/SandboxTests.java
@@ -0,0 +1,35 @@
+// Copyright 2015 Google Inc. 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.sandbox;
+
+import com.google.common.base.Predicates;
+import com.google.devtools.build.lib.testutil.BlazeTestSuiteBuilder;
+import com.google.devtools.build.lib.testutil.CustomSuite;
+
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+/**
+ * Test suite that runs all tests that are not local-only.
+ */
+@RunWith(CustomSuite.class)
+public class SandboxTests extends BlazeTestSuiteBuilder {
+  public static Set<Class<?>> suite() {
+    return new SandboxTests()
+        .getBuilder()
+        .matchClasses(Predicates.not(BlazeTestSuiteBuilder.TEST_IS_LOCAL_ONLY))
+        .create();
+  }
+}