Windows,test wrapper: pass command line arguments

The Windows native test wrapper now passes command
line arguments to the test binary.

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

Change-Id: I71e1c3424978542f5f36061a933dcfaab76b9b05

Closes #6278.

Change-Id: I71e1c3424978542f5f36061a933dcfaab76b9b05
PiperOrigin-RevId: 215359150
diff --git a/src/test/py/bazel/test_wrapper_test.py b/src/test/py/bazel/test_wrapper_test.py
index ec8f190..6058b1a 100644
--- a/src/test/py/bazel/test_wrapper_test.py
+++ b/src/test/py/bazel/test_wrapper_test.py
@@ -33,6 +33,25 @@
 
   def _CreateMockWorkspace(self):
     self.ScratchFile('WORKSPACE')
+    # All test targets are called <something>.bat, for the benefit of Windows.
+    # This makes test execution faster on Windows for the following reason:
+    #
+    # When building a sh_test rule, the main output's name is the same as the
+    # rule. On Unixes, this output is a symlink to the main script (the first
+    # entry in `srcs`), on Windows it's a copy of the file. In fact the main
+    # "script" does not have to be a script, it may be any executable file.
+    #
+    # On Unixes anything with the +x permission can be executed; the file's
+    # shebang line specifies the interpreter. On Windows, there's no such
+    # mechanism; Bazel runs the main script (which is typically a ".sh" file)
+    # through Bash. However, if the main file is a native executable, it's
+    # faster to run it directly than through Bash (plus it removes the need for
+    # Bash).
+    #
+    # Therefore on Windows, if the main script is a native executable (such as a
+    # ".bat" file) and has the same extension as the main file, Bazel (in case
+    # of sh_test) makes a copy of the file and runs it directly, rather than
+    # through Bash.
     self.ScratchFile('foo/BUILD', [
         'sh_test(',
         '    name = "passing_test.bat",',
@@ -59,7 +78,11 @@
         'sh_test(',
         '    name = "unexported_test.bat",',
         '    srcs = ["unexported.bat"],',
-        '    shard_count = 2,',
+        ')',
+        'sh_test(',
+        '    name = "testargs_test.bat",',
+        '    srcs = ["testargs.bat"],',
+        '    args = ["foo", "a b", "", "bar"],',
         ')',
     ])
     self.ScratchFile('foo/passing.bat', ['@exit /B 0'], executable=True)
@@ -92,6 +115,21 @@
             '@echo BAD=%TEST_UNDECLARED_OUTPUTS_MANIFEST%',
         ],
         executable=True)
+    self.ScratchFile(
+        'foo/testargs.bat',
+        [
+            '@echo arg=(%~nx0)',  # basename of $0
+            '@echo arg=(%1)',
+            '@echo arg=(%2)',
+            '@echo arg=(%3)',
+            '@echo arg=(%4)',
+            '@echo arg=(%5)',
+            '@echo arg=(%6)',
+            '@echo arg=(%7)',
+            '@echo arg=(%8)',
+            '@echo arg=(%9)',
+        ],
+        executable=True)
 
   def _AssertPassingTest(self, flag):
     exit_code, _, stderr = self.RunBazel([
@@ -229,6 +267,30 @@
     if not good or bad:
       self._FailWithOutput(stderr + stdout)
 
+  def _AssertTestArgs(self, flag, expected):
+    exit_code, bazel_bin, stderr = self.RunBazel(['info', 'bazel-bin'])
+    self.AssertExitCode(exit_code, 0, stderr)
+    bazel_bin = bazel_bin[0]
+
+    exit_code, stdout, stderr = self.RunBazel([
+        'test',
+        '//foo:testargs_test.bat',
+        '-t-',
+        '--test_output=all',
+        '--test_arg=baz',
+        '--test_arg="x y"',
+        '--test_arg=""',
+        '--test_arg=qux',
+        flag,
+    ])
+    self.AssertExitCode(exit_code, 0, stderr)
+
+    actual = []
+    for line in stderr + stdout:
+      if line.startswith('arg='):
+        actual.append(str(line[len('arg='):]))
+    self.assertListEqual(expected, actual)
+
   def testTestExecutionWithTestSetupSh(self):
     self._CreateMockWorkspace()
     flag = '--nowindows_native_test_wrapper'
@@ -238,6 +300,28 @@
     self._AssertRunfiles(flag)
     self._AssertShardedTest(flag)
     self._AssertUnexportsEnvvars(flag)
+    self._AssertTestArgs(
+        flag,
+        [
+            '(testargs_test.bat)',
+            '(foo)',
+            '(a)',
+            '(b)',
+            '(bar)',
+            # Note: debugging shows that test-setup.sh receives more-or-less
+            # good arguments (let's ignore issues #6276 and #6277 for now), but
+            # mangles the last few.
+            # I (laszlocsomor@) don't know the reason (as of 2018-10-01) but
+            # since I'm planning to phase out test-setup.sh on Windows in favor
+            # of the native test wrapper, I don't intend to debug this further.
+            # The test is here merely to guard against unwanted future change of
+            # behavior.
+            '(baz)',
+            '("\\"x)',
+            '(y\\"")',
+            '("\\\\\\")',
+            '(qux")'
+        ])
 
   def testTestExecutionWithTestWrapperExe(self):
     self._CreateMockWorkspace()
@@ -251,6 +335,26 @@
     self._AssertRunfiles(flag)
     self._AssertShardedTest(flag)
     self._AssertUnexportsEnvvars(flag)
+    self._AssertTestArgs(
+        flag,
+        [
+            '(testargs_test.bat)',
+            '(foo)',
+            # TODO(laszlocsomor): assert that "a b" is passed as one argument,
+            # not two, after https://github.com/bazelbuild/bazel/issues/6277
+            # is fixed.
+            '(a)',
+            '(b)',
+            # TODO(laszlocsomor): assert that the empty string argument is
+            # passed, after https://github.com/bazelbuild/bazel/issues/6276
+            # is fixed.
+            '(bar)',
+            '(baz)',
+            '("x y")',
+            '("")',
+            '(qux)',
+            '()'
+        ])
 
 
 if __name__ == '__main__':
diff --git a/tools/test/windows/test_wrapper.cc b/tools/test/windows/test_wrapper.cc
index ce8b2bb..accdf13 100644
--- a/tools/test/windows/test_wrapper.cc
+++ b/tools/test/windows/test_wrapper.cc
@@ -28,6 +28,7 @@
 #include <functional>
 #include <memory>
 #include <string>
+#include <vector>
 
 #include "src/main/cpp/util/file_platform.h"
 #include "src/main/cpp/util/path_platform.h"
@@ -83,6 +84,14 @@
   printf("ERROR(" __FILE__ ":%d) %s\n", line, msg);
 }
 
+void LogError(const int line, const wchar_t* msg) {
+#define _WSTR_HELPER_1(x) L##x
+#define _WSTR_HELPER_2(x) _WSTR_HELPER_1(x)
+  wprintf(L"ERROR(" _WSTR_HELPER_2(__FILE__) L":%d) %s\n", line, msg);
+#undef _WSTR_HELPER_2
+#undef _WSTR_HELPER_1
+}
+
 void LogErrorWithValue(const int line, const char* msg, DWORD error_code) {
   printf("ERROR(" __FILE__ ":%d) error code: %d (0x%08x): %s\n", line,
          error_code, error_code, msg);
@@ -491,14 +500,84 @@
   return result->Set(test_path);
 }
 
-bool StartSubprocess(const Path& path, HANDLE* process) {
+bool AddCommandLineArg(const wchar_t* arg, const size_t arg_size,
+                       const bool first, wchar_t* cmdline,
+                       const size_t cmdline_limit, size_t* inout_cmdline_len) {
+  if (arg_size == 0) {
+    const size_t len = (first ? 0 : 1) + 2;
+    if (*inout_cmdline_len + len >= cmdline_limit) {
+      LogError(__LINE__,
+               (std::wstring(L"Failed to add command line argument \"") + arg +
+                L"\"; command would be too long")
+                   .c_str());
+      return false;
+    }
+
+    size_t offset = *inout_cmdline_len;
+    if (!first) {
+      cmdline[offset] = L' ';
+      offset += 1;
+    }
+    cmdline[offset] = L'"';
+    cmdline[offset + 1] = L'"';
+    *inout_cmdline_len += len;
+    return true;
+  } else {
+    const size_t len = (first ? 0 : 1) + arg_size;
+    if (*inout_cmdline_len + len >= cmdline_limit) {
+      LogError(__LINE__,
+               (std::wstring(L"Failed to add command line argument \"") + arg +
+                L"\"; command would be too long")
+                   .c_str());
+      return false;
+    }
+
+    size_t offset = *inout_cmdline_len;
+    if (!first) {
+      cmdline[offset] = L' ';
+      offset += 1;
+    }
+    wcsncpy(cmdline + offset, arg, arg_size);
+    offset += arg_size;
+    *inout_cmdline_len += len;
+    return true;
+  }
+}
+
+bool CreateCommandLine(const Path& path,
+                       const std::vector<const wchar_t*>& args,
+                       std::unique_ptr<WCHAR[]>* result) {
   // kMaxCmdline value: see lpCommandLine parameter of CreateProcessW.
-  static constexpr size_t kMaxCmdline = 32768;
+  static constexpr size_t kMaxCmdline = 32767;
 
-  std::unique_ptr<WCHAR[]> cmdline(new WCHAR[kMaxCmdline]);
-  size_t len = path.Get().size();
-  wcsncpy(cmdline.get(), path.Get().c_str(), len + 1);
+  // Add an extra character for the final null-terminator.
+  result->reset(new WCHAR[kMaxCmdline + 1]);
 
+  size_t total_len = 0;
+  if (!AddCommandLineArg(path.Get().c_str(), path.Get().size(), true,
+                         result->get(), kMaxCmdline, &total_len)) {
+    return false;
+  }
+
+  for (const auto arg : args) {
+    if (!AddCommandLineArg(arg, wcslen(arg), false, result->get(), kMaxCmdline,
+                           &total_len)) {
+      return false;
+    }
+  }
+  // Add final null-terminator. There's surely enough room for it:
+  // AddCommandLineArg kept validating that we stay under the limit of
+  // kMaxCmdline, and the buffer is one WCHAR larger than that.
+  result->get()[total_len] = 0;
+  return true;
+}
+
+bool StartSubprocess(const Path& path, const std::vector<const wchar_t*>& args,
+                     HANDLE* process) {
+  std::unique_ptr<WCHAR[]> cmdline;
+  if (!CreateCommandLine(path, args, &cmdline)) {
+    return false;
+  }
   PROCESS_INFORMATION processInfo;
   STARTUPINFOW startupInfo = {0};
 
@@ -540,7 +619,8 @@
 }
 
 bool ParseArgs(int argc, wchar_t** argv, Path* out_argv0,
-               std::wstring* out_test_path_arg, bool* out_suppress_output) {
+               std::wstring* out_test_path_arg, bool* out_suppress_output,
+               std::vector<const wchar_t*>* out_args) {
   if (!out_argv0->Set(argv[0])) {
     return false;
   }
@@ -561,12 +641,18 @@
   }
 
   *out_test_path_arg = argv[0];
+  out_args->clear();
+  out_args->reserve(argc - 1);
+  for (int i = 1; i < argc; i++) {
+    out_args->push_back(argv[i]);
+  }
   return true;
 }
 
-int RunSubprocess(const Path& test_path) {
+int RunSubprocess(const Path& test_path,
+                  const std::vector<const wchar_t*>& args) {
   HANDLE process;
-  if (!StartSubprocess(test_path, &process)) {
+  if (!StartSubprocess(test_path, args, &process)) {
     return 1;
   }
   Defer close_process([process]() { CloseHandle(process); });
@@ -608,7 +694,8 @@
   bool suppress_output = false;
   Path test_path, exec_root, srcdir, tmpdir, xml_output;
   UndeclaredOutputs undecl;
-  if (!ParseArgs(argc, argv, &argv0, &test_path_arg, &suppress_output) ||
+  std::vector<const wchar_t*> args;
+  if (!ParseArgs(argc, argv, &argv0, &test_path_arg, &suppress_output, &args) ||
       !PrintTestLogStartMarker(suppress_output) ||
       !FindTestBinary(argv0, test_path_arg, &test_path) ||
       !GetCwd(&exec_root) || !ExportUserName() ||
@@ -621,5 +708,5 @@
     return 1;
   }
 
-  return RunSubprocess(test_path);
+  return RunSubprocess(test_path, args);
 }