Windows: native test wrapper can run simple tests

The Bash-less test wrapper can now run simple
tests, indicate passing or failing as needed, and
print the test's output.

There's still more work to make the test wrapper
feature-compatible with test-setup.sh, but this
commit already makes good progress.

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

Change-Id: If5fb473d09323969db34ee381f1b54354fb6a97d

Closes #6037.

Change-Id: I8c1045f7ec3d63b45068182602d745d36c09e25e
PiperOrigin-RevId: 211349900
diff --git a/scripts/bootstrap/buildenv.sh b/scripts/bootstrap/buildenv.sh
index 0c7e8d4..0973810 100755
--- a/scripts/bootstrap/buildenv.sh
+++ b/scripts/bootstrap/buildenv.sh
@@ -308,8 +308,51 @@
   local dest=$2
 
   if [[ "${PLATFORM}" == "windows" ]]; then
-    cmd.exe /C "mklink /J \"$(cygpath -w "$dest")\" \"$(cygpath -w "$source")\""
+    cmd.exe /C "mklink /J \"$(cygpath -w "$dest")\" \"$(cygpath -w "$source")\"" >&/dev/null
   else
     ln -s "${source}" "${dest}"
   fi
 }
+
+function link_file() {
+  local source=$1
+  local dest=$2
+
+  if [[ "${PLATFORM}" == "windows" ]]; then
+    # Attempt creating a symlink to the file. This is supported without
+    # elevation (Administrator privileges) on Windows 10 version 1709 when
+    # Developer Mode is enabled.
+    if ! cmd.exe /C "mklink \"$(cygpath -w "$dest")\" \"$(cygpath -w "$source")\"" >&/dev/null; then
+      # If the previous call failed to create a symlink, just copy the file.
+      cp "$source" "$dest"
+    fi
+  else
+    ln -s "${source}" "${dest}"
+  fi
+}
+
+# Link direct children (subdirectories and files) of a directory.
+# Usage:
+#   link_children "$PWD" "tools" "${BAZEL_TOOLS_REPO}"
+# This creates:
+#   ${BAZEL_TOOLS_REPO}/tools/android -> $PWD/tools/android
+#   ${BAZEL_TOOLS_REPO}/tools/bash -> $PWD/tools/bash
+#   ... and so on for all files and directories directly under "tools".
+function link_children() {
+  local -r source_dir=${1%/}
+  local -r source_subdir=${2%/}
+  local -r dest_dir=${3%/}
+
+  for e in $(find "${source_dir}/${source_subdir}" -mindepth 1 -maxdepth 1 -type d); do
+    local dest_path="${dest_dir}/${e#$source_dir/}"
+    if [[ ! -d "$dest_path" ]]; then
+      link_dir "$e" "$dest_path"
+    fi
+  done
+  for e in $(find "${source_dir}/${source_subdir}" -mindepth 1 -maxdepth 1 -type f); do
+    local dest_path="${dest_dir}/${e#$source_dir/}"
+    if [[ ! -f "$dest_path" ]]; then
+      link_file "$e" "$dest_path"
+    fi
+  done
+}
diff --git a/scripts/bootstrap/compile.sh b/scripts/bootstrap/compile.sh
index f489960..17ee809 100755
--- a/scripts/bootstrap/compile.sh
+++ b/scripts/bootstrap/compile.sh
@@ -220,7 +220,23 @@
 EOF
   link_dir ${PWD}/src ${BAZEL_TOOLS_REPO}/src
   link_dir ${PWD}/third_party ${BAZEL_TOOLS_REPO}/third_party
-  link_dir ${PWD}/tools ${BAZEL_TOOLS_REPO}/tools
+
+  # Create @bazel_tools//tools/cpp/runfiles
+  mkdir -p ${BAZEL_TOOLS_REPO}/tools/cpp/runfiles
+  link_file "${PWD}/tools/cpp/runfiles/runfiles_src.h" \
+      "${BAZEL_TOOLS_REPO}/tools/cpp/runfiles/runfiles.h"
+  # Transform //tools/cpp/runfiles:runfiles_src.cc to
+  # @bazel_tools//tools/cpp/runfiles:runfiles.cc
+  # Keep this transformation logic in sync with the
+  # //tools/cpp/runfiles:srcs_for_embedded_tools genrule.
+  sed 's|^#include.*/runfiles_src.h.*|#include \"tools/cpp/runfiles/runfiles.h\"|' \
+      "${PWD}/tools/cpp/runfiles/runfiles_src.cc" > \
+      "${BAZEL_TOOLS_REPO}/tools/cpp/runfiles/runfiles.cc"
+  link_file "${PWD}/tools/cpp/runfiles/BUILD.tools" \
+      "${BAZEL_TOOLS_REPO}/tools/cpp/runfiles/BUILD"
+  # Create the rest of @bazel_tools//tools/...
+  link_children "${PWD}" tools/cpp "${BAZEL_TOOLS_REPO}"
+  link_children "${PWD}" tools "${BAZEL_TOOLS_REPO}"
 
   # Set up @bazel_tools//platforms properly
   mkdir -p ${BAZEL_TOOLS_REPO}/platforms
diff --git a/src/main/cpp/util/BUILD b/src/main/cpp/util/BUILD
index f40c00e..8ef6810 100644
--- a/src/main/cpp/util/BUILD
+++ b/src/main/cpp/util/BUILD
@@ -60,6 +60,7 @@
         "//src/tools/singlejar:__pkg__",
         "//third_party/def_parser:__pkg__",
         "//tools/cpp/runfiles:__pkg__",
+        "//tools/test:__pkg__",
     ],
     deps = [
         ":blaze_exit_code",
diff --git a/src/test/py/bazel/test_wrapper_test.py b/src/test/py/bazel/test_wrapper_test.py
index 4a346a7..27e5b77 100644
--- a/src/test/py/bazel/test_wrapper_test.py
+++ b/src/test/py/bazel/test_wrapper_test.py
@@ -19,32 +19,51 @@
 
 class TestWrapperTest(test_base.TestBase):
 
-  def testTestExecutionWithTestSetupShAndWithTestWrapperExe(self):
+  def _CreateMockWorkspace(self):
     self.ScratchFile('WORKSPACE')
     self.ScratchFile('foo/BUILD', [
-        'py_test(',
-        '    name = "x_test",',
-        '    srcs = ["x_test.py"],',
+        'sh_test(',
+        '    name = "passing_test.bat",',
+        '    srcs = ["passing.bat"],',
+        ')',
+        'sh_test(',
+        '    name = "failing_test.bat",',
+        '    srcs = ["failing.bat"],',
+        ')',
+        'sh_test(',
+        '    name = "printing_test.bat",',
+        '    srcs = ["printing.bat"],',
         ')',
     ])
-    self.ScratchFile(
-        'foo/x_test.py', [
-            'from __future__ import print_function',
-            'import unittest',
-            '',
-            'class XTest(unittest.TestCase):',
-            '    def testFoo(self):',
-            '        print("lorem ipsum")',
-            '',
-            'if __name__ == "__main__":',
-            '  unittest.main()',
-        ],
-        executable=True)
+    self.ScratchFile('foo/passing.bat', ['@exit /B 0'], executable=True)
+    self.ScratchFile('foo/failing.bat', ['@exit /B 1'], executable=True)
+    self.ScratchFile('foo/printing.bat', ['@echo lorem ipsum'], executable=True)
 
-    # Run test with test-setup.sh
+  def _AssertPassingTest(self, flag):
+    exit_code, _, stderr = self.RunBazel([
+        'test',
+        '//foo:passing_test.bat',
+        '-t-',
+        flag,
+    ])
+    self.AssertExitCode(exit_code, 0, stderr)
+
+  def _AssertFailingTest(self, flag):
+    exit_code, _, stderr = self.RunBazel([
+        'test',
+        '//foo:failing_test.bat',
+        '-t-',
+        flag,
+    ])
+    self.AssertExitCode(exit_code, 3, stderr)
+
+  def _AssertPrintingTest(self, flag):
     exit_code, stdout, stderr = self.RunBazel([
-        'test', '//foo:x_test', '--test_output=streamed', '-t-',
-        '--nowindows_native_test_wrapper'
+        'test',
+        '//foo:printing_test.bat',
+        '--test_output=streamed',
+        '-t-',
+        flag,
     ])
     self.AssertExitCode(exit_code, 0, stderr)
     found = False
@@ -54,14 +73,18 @@
     if not found:
       self.fail('FAIL: output:\n%s\n---' % '\n'.join(stderr + stdout))
 
-    # Run test with test_wrapper.exe
-    exit_code, _, stderr = self.RunBazel([
-        'test', '//foo:x_test', '--test_output=streamed', '-t-',
-        '--windows_native_test_wrapper'
-    ])
-
-    # As of 2018-08-17, test_wrapper.exe cannot yet run tests.
-    self.AssertExitCode(exit_code, 3, stderr)
+  def testTestExecutionWithTestSetupShAndWithTestWrapperExe(self):
+    self._CreateMockWorkspace()
+    flag = '--nowindows_native_test_wrapper'
+    self._AssertPassingTest(flag)
+    self._AssertFailingTest(flag)
+    self._AssertPrintingTest(flag)
+    # As of 2018-08-30, the Windows native test runner can run simple tests,
+    # though it does not set up the test's environment yet.
+    flag = '--windows_native_test_wrapper'
+    self._AssertPassingTest(flag)
+    self._AssertFailingTest(flag)
+    self._AssertPrintingTest(flag)
 
 
 if __name__ == '__main__':
diff --git a/tools/test/BUILD b/tools/test/BUILD
index 7f7e831..a8972e1 100644
--- a/tools/test/BUILD
+++ b/tools/test/BUILD
@@ -45,6 +45,13 @@
         "//conditions:default": ["empty_test_wrapper.cc"],
     }),
     visibility = ["//visibility:private"],
+    deps = select({
+        "@bazel_tools//src/conditions:windows": [
+            "//src/main/cpp/util:filesystem",
+            "@bazel_tools//tools/cpp/runfiles",
+        ],
+        "//conditions:default": [],
+    }),
 )
 
 filegroup(
diff --git a/tools/test/windows/test_wrapper.cc b/tools/test/windows/test_wrapper.cc
index dc911fd..0f7ee65 100644
--- a/tools/test/windows/test_wrapper.cc
+++ b/tools/test/windows/test_wrapper.cc
@@ -16,8 +16,203 @@
 // Design:
 // https://github.com/laszlocsomor/proposals/blob/win-test-runner/designs/2018-07-18-windows-native-test-runner.md
 
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <wchar.h>
+
+#include <functional>
+#include <memory>
+#include <string>
+
+#include "src/main/cpp/util/path_platform.h"
+#include "tools/cpp/runfiles/runfiles.h"
+
+namespace {
+
+class Defer {
+ public:
+  explicit Defer(std::function<void()> f) : f_(f) {}
+  ~Defer() { f_(); }
+
+ private:
+  std::function<void()> f_;
+};
+
+void LogError(const int line, const char* msg) {
+  fprintf(stderr, "ERROR(" __FILE__ ":%d) %s\n", line, msg);
+}
+
+void LogErrorWithValue(const int line, const char* msg, DWORD error_code) {
+  fprintf(stderr, "ERROR(" __FILE__ ":%d) error code: %d (0x%08x): %s\n", line,
+          error_code, error_code, msg);
+}
+
+void LogErrorWithArgAndValue(const int line, const char* msg, const char* arg,
+                             DWORD error_code) {
+  fprintf(stderr,
+          "ERROR(" __FILE__ ":%d) error code: %d (0x%08x), argument: %s: %s\n",
+          line, error_code, error_code, arg, msg);
+}
+
+bool GetEnv(const char* name, std::string* result) {
+  static constexpr size_t kSmallBuf = MAX_PATH;
+  char value[kSmallBuf];
+  DWORD size = GetEnvironmentVariableA(name, value, kSmallBuf);
+  DWORD err = GetLastError();
+  if (size == 0 && err == ERROR_ENVVAR_NOT_FOUND) {
+    result->clear();
+    return true;
+  } else if (0 < size && size < kSmallBuf) {
+    *result = value;
+    return true;
+  } else if (size >= kSmallBuf) {
+    std::unique_ptr<char[]> value_big(new char[size]);
+    GetEnvironmentVariableA(name, value_big.get(), size);
+    *result = value_big.get();
+    return true;
+  } else {
+    LogErrorWithArgAndValue(__LINE__, "Failed to read envvar", name, err);
+    return false;
+  }
+}
+
+inline void PrintTestLogStartMarker() {
+  // This header marks where --test_output=streamed will start being printed.
+  printf(
+      "------------------------------------------------------------------------"
+      "-----\n");
+}
+
+inline bool GetWorkspaceName(std::string* result) {
+  return GetEnv("TEST_WORKSPACE", result) && !result->empty();
+}
+
+inline void StripLeadingDotSlash(std::string* s) {
+  if (s->size() >= 2 && (*s)[0] == '.' && (*s)[1] == '/') {
+    *s = s->substr(2);
+  }
+}
+
+bool FindTestBinary(const std::string& argv0, std::string test_path,
+                    std::wstring* result) {
+  if (!blaze_util::IsAbsolute(test_path)) {
+    std::string error;
+    std::unique_ptr<bazel::tools::cpp::runfiles::Runfiles> runfiles(
+        bazel::tools::cpp::runfiles::Runfiles::Create(argv0, &error));
+    if (runfiles == nullptr) {
+      LogError(__LINE__, "Failed to load runfiles");
+      return false;
+    }
+
+    std::string workspace;
+    if (!GetWorkspaceName(&workspace)) {
+      LogError(__LINE__, "Failed to read %TEST_WORKSPACE%");
+      return false;
+    }
+
+    StripLeadingDotSlash(&test_path);
+    test_path = runfiles->Rlocation(workspace + "/" + test_path);
+  }
+
+  std::string error;
+  if (!blaze_util::AsWindowsPath(test_path, result, &error)) {
+    LogError(__LINE__, error.c_str());
+    return false;
+  }
+  return true;
+}
+
+bool StartSubprocess(const wchar_t* path, HANDLE* process) {
+  // kMaxCmdline value: see lpCommandLine parameter of CreateProcessW.
+  static constexpr size_t kMaxCmdline = 32768;
+
+  std::unique_ptr<WCHAR[]> cmdline(new WCHAR[kMaxCmdline]);
+  size_t len = wcslen(path);
+  wcsncpy(cmdline.get(), path, len + 1);
+
+  PROCESS_INFORMATION processInfo;
+  STARTUPINFOW startupInfo = {0};
+
+  if (CreateProcessW(NULL, cmdline.get(), NULL, NULL, FALSE, 0, NULL, NULL,
+                     &startupInfo, &processInfo) != 0) {
+    CloseHandle(processInfo.hThread);
+    *process = processInfo.hProcess;
+    return true;
+  } else {
+    LogErrorWithValue(__LINE__, "CreateProcessW failed", GetLastError());
+    return false;
+  }
+}
+
+int WaitForSubprocess(HANDLE process) {
+  DWORD result = WaitForSingleObject(process, INFINITE);
+  switch (result) {
+    case WAIT_OBJECT_0: {
+      DWORD exit_code;
+      if (!GetExitCodeProcess(process, &exit_code)) {
+        LogErrorWithValue(__LINE__, "GetExitCodeProcess failed",
+                          GetLastError());
+        return 1;
+      }
+      return exit_code;
+    }
+    case WAIT_FAILED:
+      LogErrorWithValue(__LINE__, "WaitForSingleObject failed", GetLastError());
+      return 1;
+    default:
+      LogErrorWithValue(
+          __LINE__, "WaitForSingleObject returned unexpected result", result);
+      return 1;
+  }
+}
+
+}  // namespace
+
 int main(int argc, char** argv) {
   // TODO(laszlocsomor): Implement the functionality described in
   // https://github.com/laszlocsomor/proposals/blob/win-test-runner/designs/2018-07-18-windows-native-test-runner.md
-  return 0;
+
+  const char* argv0 = argv[0];
+  argc--;
+  argv++;
+  bool suppress_output = false;
+  if (argc > 0 && strcmp(argv[0], "--no_echo") == 0) {
+    // Don't print anything to stdout in this special case.
+    // Currently needed for persistent test runner.
+    suppress_output = true;
+    argc--;
+    argv++;
+  } else {
+    std::string test_target;
+    if (!GetEnv("TEST_TARGET", &test_target)) {
+      return 1;
+    }
+    printf("Executing tests from %s\n", test_target.c_str());
+  }
+
+  if (argc < 1) {
+    LogError(__LINE__, "Usage: $0 [--no_echo] <test_path> [test_args...]");
+    return 1;
+  }
+
+  if (!suppress_output) {
+    PrintTestLogStartMarker();
+  }
+
+  const char* test_path_arg = argv[0];
+  std::wstring test_path;
+  if (!FindTestBinary(argv0, test_path_arg, &test_path)) {
+    return 1;
+  }
+
+  HANDLE process;
+  if (!StartSubprocess(test_path.c_str(), &process)) {
+    return 1;
+  }
+  Defer close_process([process]() { CloseHandle(process); });
+
+  return WaitForSubprocess(process);
 }