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);
}