subprocesses: customizable SubprocessFactory

Individual SubprocessBuilder instances can now use
a SubprocessFactory object other than the static
SubprocessBuilder.factory.

Also, WindowsSubprocessFactory is no longer a
singleton because it now stores state: whether to
use windows-style argument escaping or to use the
(broken) Bash-style escaping (causing https://github.com/bazelbuild/bazel/issues/7122).

These two changes allow:
- a safer way to mock out SubprocessFactory in
  tests, because it's no longer necessary to
  change global state (i.e. the static
  SubprocessBuilder.factory member)
- testing old and new argument escaping semantics
  in WindowsSubprocessTest
- adding a flag that switches between old and new
  semantics in the SubprocessFactory, and thus
  allows rolling out the bugfix with just a flag
  flip

See https://github.com/bazelbuild/bazel/issues/7122

PiperOrigin-RevId: 234105692
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsSubprocessTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsSubprocessTest.java
index 4fb0968..d392230 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/WindowsSubprocessTest.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsSubprocessTest.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.shell.ShellUtils;
 import com.google.devtools.build.lib.shell.Subprocess;
 import com.google.devtools.build.lib.shell.SubprocessBuilder;
 import com.google.devtools.build.lib.testutil.TestSpec;
@@ -26,6 +27,7 @@
 import com.google.devtools.build.lib.windows.jni.WindowsProcesses;
 import com.google.devtools.build.runfiles.Runfiles;
 import java.io.File;
+import java.util.function.Function;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -63,11 +65,10 @@
     }
   }
 
-  @Test
-  public void testSystemRootIsSetByDefault() throws Exception {
-    SubprocessBuilder subprocessBuilder = new SubprocessBuilder();
+  private void assertSystemRootIsSetByDefault(boolean windowsStyleArgEscaping) throws Exception {
+    SubprocessBuilder subprocessBuilder =
+        new SubprocessBuilder(new WindowsSubprocessFactory(windowsStyleArgEscaping));
     subprocessBuilder.setWorkingDirectory(new File("."));
-    subprocessBuilder.setSubprocessFactory(WindowsSubprocessFactory.INSTANCE);
     subprocessBuilder.setArgv(mockBinary, "-jar", mockSubprocess, "O$SYSTEMROOT");
     process = subprocessBuilder.start();
     process.waitFor();
@@ -79,10 +80,19 @@
   }
 
   @Test
-  public void testSystemDriveIsSetByDefault() throws Exception {
-    SubprocessBuilder subprocessBuilder = new SubprocessBuilder();
+  public void testSystemRootIsSetByDefaultNoWindowsStyleArgEscaping() throws Exception {
+    assertSystemRootIsSetByDefault(false);
+  }
+
+  @Test
+  public void testSystemRootIsSetByDefaultWithWindowsStyleArgEscaping() throws Exception {
+    assertSystemRootIsSetByDefault(true);
+  }
+
+  private void assertSystemDriveIsSetByDefault(boolean windowsStyleArgEscaping) throws Exception {
+    SubprocessBuilder subprocessBuilder =
+        new SubprocessBuilder(new WindowsSubprocessFactory(windowsStyleArgEscaping));
     subprocessBuilder.setWorkingDirectory(new File("."));
-    subprocessBuilder.setSubprocessFactory(WindowsSubprocessFactory.INSTANCE);
     subprocessBuilder.setArgv(mockBinary, "-jar", mockSubprocess, "O$SYSTEMDRIVE");
     process = subprocessBuilder.start();
     process.waitFor();
@@ -94,10 +104,19 @@
   }
 
   @Test
-  public void testSystemRootIsSet() throws Exception {
-    SubprocessBuilder subprocessBuilder = new SubprocessBuilder();
+  public void testSystemDriveIsSetByDefaultNoWindowsStyleArgEscaping() throws Exception {
+    assertSystemDriveIsSetByDefault(false);
+  }
+
+  @Test
+  public void testSystemDriveIsSetByDefaultWithWindowsStyleArgEscaping() throws Exception {
+    assertSystemDriveIsSetByDefault(true);
+  }
+
+  private void assertSystemRootIsSet(boolean windowsStyleArgEscaping) throws Exception {
+    SubprocessBuilder subprocessBuilder =
+        new SubprocessBuilder(new WindowsSubprocessFactory(windowsStyleArgEscaping));
     subprocessBuilder.setWorkingDirectory(new File("."));
-    subprocessBuilder.setSubprocessFactory(WindowsSubprocessFactory.INSTANCE);
     subprocessBuilder.setArgv(mockBinary, "-jar", mockSubprocess, "O$SYSTEMROOT");
     // Case shouldn't matter on Windows
     subprocessBuilder.setEnv(ImmutableMap.of("SystemRoot", "C:\\MySystemRoot"));
@@ -111,10 +130,19 @@
   }
 
   @Test
-  public void testSystemDriveIsSet() throws Exception {
-    SubprocessBuilder subprocessBuilder = new SubprocessBuilder();
+  public void testSystemRootIsSetNoWindowsStyleArgEscaping() throws Exception {
+    assertSystemRootIsSet(false);
+  }
+
+  @Test
+  public void testSystemRootIsSetWithWindowsStyleArgEscaping() throws Exception {
+    assertSystemRootIsSet(true);
+  }
+
+  private void assertSystemDriveIsSet(boolean windowsStyleArgEscaping) throws Exception {
+    SubprocessBuilder subprocessBuilder =
+        new SubprocessBuilder(new WindowsSubprocessFactory(windowsStyleArgEscaping));
     subprocessBuilder.setWorkingDirectory(new File("."));
-    subprocessBuilder.setSubprocessFactory(WindowsSubprocessFactory.INSTANCE);
     subprocessBuilder.setArgv(mockBinary, "-jar", mockSubprocess, "O$SYSTEMDRIVE");
     // Case shouldn't matter on Windows
     subprocessBuilder.setEnv(ImmutableMap.of("SystemDrive", "X:"));
@@ -127,6 +155,16 @@
     assertThat(new String(buf, UTF_8).trim()).isEqualTo("X:");
   }
 
+  @Test
+  public void testSystemDriveIsSetNoWindowsStyleArgEscaping() throws Exception {
+    assertSystemDriveIsSet(false);
+  }
+
+  @Test
+  public void testSystemDriveIsSetWithWindowsStyleArgEscaping() throws Exception {
+    assertSystemDriveIsSet(true);
+  }
+
   /**
    * An argument and its command-line-escaped counterpart.
    *
@@ -143,7 +181,9 @@
   };
 
   /** Asserts that a subprocess correctly receives command line arguments. */
-  private void assertSubprocessReceivesArgsAsIntended(ArgPair... args) throws Exception {
+  private void assertSubprocessReceivesArgsAsIntended(
+      boolean windowsStyleArgEscaping, Function<String, String> escaper, ArgPair... args)
+      throws Exception {
     // Look up the path of the printarg.exe utility.
     String printArgExe =
         runfiles.rlocation("io_bazel/src/test/java/com/google/devtools/build/lib/printarg.exe");
@@ -151,13 +191,12 @@
 
     for (ArgPair arg : args) {
       // Assert that the command-line encoding logic works as intended.
-      assertThat(WindowsProcesses.quoteCommandLine(ImmutableList.of(arg.original)))
-          .isEqualTo(arg.escaped);
+      assertThat(escaper.apply(arg.original)).isEqualTo(arg.escaped);
 
       // Create a separate subprocess just for this argument.
-      SubprocessBuilder subprocessBuilder = new SubprocessBuilder();
+      SubprocessBuilder subprocessBuilder =
+          new SubprocessBuilder(new WindowsSubprocessFactory(windowsStyleArgEscaping));
       subprocessBuilder.setWorkingDirectory(new File("."));
-      subprocessBuilder.setSubprocessFactory(WindowsSubprocessFactory.INSTANCE);
       subprocessBuilder.setArgv(printArgExe, arg.original);
       process = subprocessBuilder.start();
       process.waitFor();
@@ -173,8 +212,10 @@
   }
 
   @Test
-  public void testSubprocessReceivesArgsAsIntended() throws Exception {
+  public void testSubprocessReceivesArgsAsIntendedNoWindowsStyleArgEscaping() throws Exception {
     assertSubprocessReceivesArgsAsIntended(
+        false,
+        x -> WindowsProcesses.quoteCommandLine(ImmutableList.of(x)),
         new ArgPair("", "\"\""),
         new ArgPair(" ", "\" \""),
         new ArgPair("foo", "foo"),
@@ -184,4 +225,54 @@
     // it fails to properly escape things like a single backslash followed by a quote, e.g. a\"b
     // Fix the escaping logic and add more test here.
   }
+
+  @Test
+  public void testSubprocessReceivesArgsAsIntendedWithWindowsStyleArgEscaping() throws Exception {
+    assertSubprocessReceivesArgsAsIntended(
+        true,
+        x -> ShellUtils.windowsEscapeArg(x),
+        new ArgPair("", "\"\""),
+        new ArgPair(" ", "\" \""),
+        new ArgPair("\"", "\"\\\"\""),
+        new ArgPair("\"\\", "\"\\\"\\\\\""),
+        new ArgPair("\\", "\\"),
+        new ArgPair("\\\"", "\"\\\\\\\"\""),
+        new ArgPair("with space", "\"with space\""),
+        new ArgPair("with^caret", "with^caret"),
+        new ArgPair("space ^caret", "\"space ^caret\""),
+        new ArgPair("caret^ space", "\"caret^ space\""),
+        new ArgPair("with\"quote", "\"with\\\"quote\""),
+        new ArgPair("with\\backslash", "with\\backslash"),
+        new ArgPair("one\\ backslash and \\space", "\"one\\ backslash and \\space\""),
+        new ArgPair("two\\\\backslashes", "two\\\\backslashes"),
+        new ArgPair("two\\\\ backslashes \\\\and space", "\"two\\\\ backslashes \\\\and space\""),
+        new ArgPair("one\\\"x", "\"one\\\\\\\"x\""),
+        new ArgPair("two\\\\\"x", "\"two\\\\\\\\\\\"x\""),
+        new ArgPair("a \\ b", "\"a \\ b\""),
+        new ArgPair("a \\\" b", "\"a \\\\\\\" b\""),
+        new ArgPair("A", "A"),
+        new ArgPair("\"a\"", "\"\\\"a\\\"\""),
+        new ArgPair("B C", "\"B C\""),
+        new ArgPair("\"b c\"", "\"\\\"b c\\\"\""),
+        new ArgPair("D\"E", "\"D\\\"E\""),
+        new ArgPair("\"d\"e\"", "\"\\\"d\\\"e\\\"\""),
+        new ArgPair("C:\\F G", "\"C:\\F G\""),
+        new ArgPair("\"C:\\f g\"", "\"\\\"C:\\f g\\\"\""),
+        new ArgPair("C:\\H\"I", "\"C:\\H\\\"I\""),
+        new ArgPair("\"C:\\h\"i\"", "\"\\\"C:\\h\\\"i\\\"\""),
+        new ArgPair("C:\\J\\\"K", "\"C:\\J\\\\\\\"K\""),
+        new ArgPair("\"C:\\j\\\"k\"", "\"\\\"C:\\j\\\\\\\"k\\\"\""),
+        new ArgPair("C:\\L M ", "\"C:\\L M \""),
+        new ArgPair("\"C:\\l m \"", "\"\\\"C:\\l m \\\"\""),
+        new ArgPair("C:\\N O\\", "\"C:\\N O\\\\\""),
+        new ArgPair("\"C:\\n o\\\"", "\"\\\"C:\\n o\\\\\\\"\""),
+        new ArgPair("C:\\P Q\\ ", "\"C:\\P Q\\ \""),
+        new ArgPair("\"C:\\p q\\ \"", "\"\\\"C:\\p q\\ \\\"\""),
+        new ArgPair("C:\\R\\S\\", "C:\\R\\S\\"),
+        new ArgPair("C:\\R x\\S\\", "\"C:\\R x\\S\\\\\""),
+        new ArgPair("\"C:\\r\\s\\\"", "\"\\\"C:\\r\\s\\\\\\\"\""),
+        new ArgPair("\"C:\\r x\\s\\\"", "\"\\\"C:\\r x\\s\\\\\\\"\""),
+        new ArgPair("C:\\T U\\W\\", "\"C:\\T U\\W\\\\\""),
+        new ArgPair("\"C:\\t u\\w\\\"", "\"\\\"C:\\t u\\w\\\\\\\"\""));
+  }
 }