Merge pull request #440 from adierking/windowstests

tests: port the test harness to Windows
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 5909ad1..3a4684f 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,7 +1,7 @@
 
 if(CMAKE_SYSTEM_NAME STREQUAL Windows)
     execute_process(COMMAND
-                      "${CMAKE_COMMAND}" -E copy "${PROJECT_SOURCE_DIR}/private"
+                      "${CMAKE_COMMAND}" -E copy_directory "${PROJECT_SOURCE_DIR}/private"
                       "${CMAKE_CURRENT_BINARY_DIR}/dispatch")
     execute_process(COMMAND
                       "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/leaks-wrapper.sh"
@@ -36,6 +36,15 @@
                          PRIVATE
                            ${BSD_OVERLAY_CFLAGS})
 endif()
+if (WIN32)
+  target_sources(bsdtests
+                 PRIVATE
+                   generic_win_port.c)
+  target_compile_definitions(bsdtests
+                             PUBLIC
+                               _CRT_NONSTDC_NO_WARNINGS
+                               _CRT_SECURE_NO_WARNINGS)
+endif ()
 
 add_executable(bsdtestharness
                bsdtestharness.c)
@@ -91,11 +100,11 @@
   endif()
   if("${CMAKE_C_SIMULATE_ID}" STREQUAL "MSVC")
     target_compile_options(${name} PRIVATE -Xclang -fblocks)
+    target_compile_options(${name} PRIVATE /W3 -Wno-deprecated-declarations)
   else()
     target_compile_options(${name} PRIVATE -fblocks)
+    target_compile_options(${name} PRIVATE -Wall -Wno-deprecated-declarations)
   endif()
-  # TODO(compnerd) make this portable
-  target_compile_options(${name} PRIVATE -Wall -Wno-deprecated-declarations)
   dispatch_set_linker(${name})
   target_link_libraries(${name}
                         PRIVATE
@@ -187,4 +196,4 @@
 target_link_libraries(dispatch_timer_short PRIVATE m)
 
 # test-specific compile options
-target_compile_options(dispatch_c99 PRIVATE -std=c99)
+set_target_properties(dispatch_c99 PROPERTIES C_STANDARD 99)
diff --git a/tests/bsdtestharness.c b/tests/bsdtestharness.c
index f7b6ea3..ca52d6e 100644
--- a/tests/bsdtestharness.c
+++ b/tests/bsdtestharness.c
@@ -20,33 +20,29 @@
 
 #include <dispatch/dispatch.h>
 #include <assert.h>
-#include <spawn.h>
 #include <stdio.h>
 #include <stdlib.h>
 #if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
+#include <spawn.h>
+#include <sys/resource.h>
+#include <sys/time.h>
+#include <sys/wait.h>
 #include <unistd.h>
+#elif defined(_WIN32)
+#include <generic_win_port.h>
+#include <Psapi.h>
+#include <Windows.h>
 #endif
 #include <signal.h>
 #ifdef __APPLE__
 #include <mach/clock_types.h>
 #include <mach-o/arch.h>
 #endif
-#include <sys/resource.h>
-#include <sys/time.h>
-#if defined(__linux__) || defined(__FreeBSD__)
-#include <sys/wait.h>
-#endif
 
 #include <bsdtests.h>
 
+#if !defined(_WIN32)
 extern char **environ;
-
-#ifdef __linux__
-// Linux lacks the DISPATCH_SOURCE_TYPE_PROC functionality
-// the real test harness needs.
-#define SIMPLE_TEST_HARNESS 1
-#else
-#define SIMPLE_TEST_HARNESS 0
 #endif
 
 int
@@ -131,6 +127,23 @@
 			_Exit(EXIT_FAILURE);
 		}
 	}
+#elif defined(_WIN32)
+	(void)res;
+	WCHAR *cmdline = argv_to_command_line(newargv);
+	if (!cmdline) {
+		fprintf(stderr, "argv_to_command_line() failed\n");
+		exit(EXIT_FAILURE);
+	}
+	STARTUPINFOW si = {.cb = sizeof(si)};
+	PROCESS_INFORMATION pi;
+	BOOL created = CreateProcessW(NULL, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
+	DWORD error = GetLastError();
+	free(cmdline);
+	if (!created) {
+		print_winapi_error("CreateProcessW", error);
+		exit(EXIT_FAILURE);
+	}
+	pid = (pid_t)pi.dwProcessId;
 #else
 #error "bsdtestharness not implemented on this platform"
 #endif
@@ -138,18 +151,19 @@
 	//fprintf(stderr, "pid = %d\n", pid);
 	assert(pid > 0);
 
-#if SIMPLE_TEST_HARNESS
+#if defined(__linux__)
 	int status;
 	struct rusage usage;
 	struct timeval tv_stop, tv_wall;
 
+	int res2 = wait4(pid, &status, 0, &usage);
+	(void)res2;
+
 	gettimeofday(&tv_stop, NULL);
 	tv_wall.tv_sec = tv_stop.tv_sec - tv_start.tv_sec;
 	tv_wall.tv_sec -= (tv_stop.tv_usec < tv_start.tv_usec);
 	tv_wall.tv_usec = labs(tv_stop.tv_usec - tv_start.tv_usec);
 
-	int res2 = wait4(pid, &status, 0, &usage);
-	(void)res2;
 	assert(res2 != -1);
 	test_long("Process exited", (WIFEXITED(status) && WEXITSTATUS(status) && WEXITSTATUS(status) != 0xff) || WIFSIGNALED(status), 0);
 	printf("[PERF]\twall time: %ld.%06ld\n", tv_wall.tv_sec, tv_wall.tv_usec);
@@ -161,6 +175,46 @@
 	printf("[PERF]\tvoluntary context switches: %ld\n", usage.ru_nvcsw);
 	printf("[PERF]\tinvoluntary context switches: %ld\n", usage.ru_nivcsw);
 	exit((WIFEXITED(status) && WEXITSTATUS(status)) || WIFSIGNALED(status));
+#elif defined(_WIN32)
+	if (WaitForSingleObject(pi.hProcess, INFINITE) != WAIT_OBJECT_0) {
+		print_winapi_error("WaitForSingleObject", GetLastError());
+		exit(EXIT_FAILURE);
+	}
+
+	struct timeval tv_stop, tv_wall;
+	gettimeofday(&tv_stop, NULL);
+	tv_wall.tv_sec = tv_stop.tv_sec - tv_start.tv_sec;
+	tv_wall.tv_sec -= (tv_stop.tv_usec < tv_start.tv_usec);
+	tv_wall.tv_usec = labs(tv_stop.tv_usec - tv_start.tv_usec);
+
+	DWORD status;
+	if (!GetExitCodeProcess(pi.hProcess, &status)) {
+		print_winapi_error("GetExitCodeProcess", GetLastError());
+		exit(EXIT_FAILURE);
+	}
+
+	FILETIME create_time, exit_time, kernel_time, user_time;
+	if (!GetProcessTimes(pi.hProcess, &create_time, &exit_time, &kernel_time, &user_time)) {
+		print_winapi_error("GetProcessTimes", GetLastError());
+		exit(EXIT_FAILURE);
+	}
+	struct timeval utime, stime;
+	filetime_to_timeval(&utime, &user_time);
+	filetime_to_timeval(&stime, &kernel_time);
+
+	PROCESS_MEMORY_COUNTERS counters;
+	if (!GetProcessMemoryInfo(pi.hProcess, &counters, sizeof(counters))) {
+		print_winapi_error("GetProcessMemoryInfo", GetLastError());
+		exit(EXIT_FAILURE);
+	}
+
+	test_long("Process exited", status == 0 || status == 0xff, 1);
+	printf("[PERF]\twall time: %ld.%06ld\n", tv_wall.tv_sec, tv_wall.tv_usec);
+	printf("[PERF]\tuser time: %ld.%06ld\n", utime.tv_sec, utime.tv_usec);
+	printf("[PERF]\tsystem time: %ld.%06ld\n", stime.tv_sec, stime.tv_usec);
+	printf("[PERF]\tmax working set size: %zu\n", counters.PeakWorkingSetSize);
+	printf("[PERF]\tpage faults: %lu\n", counters.PageFaultCount);
+	exit(status ? EXIT_FAILURE : EXIT_SUCCESS);
 #else
 	dispatch_queue_t main_q = dispatch_get_main_queue();
 
@@ -219,7 +273,7 @@
 	kill(pid, SIGCONT);
 
 	dispatch_main();
-#endif // SIMPLE_TEST_HARNESS
+#endif
 
 	return 0;
 }
diff --git a/tests/bsdtests.c b/tests/bsdtests.c
index 09700fa..af71662 100644
--- a/tests/bsdtests.c
+++ b/tests/bsdtests.c
@@ -29,14 +29,16 @@
 #include <unistd.h>
 #endif
 #include <errno.h>
-#include <sys/errno.h>
-#include <sys/wait.h>
 #include <string.h>
 #ifdef __APPLE__
 #include <crt_externs.h>
 #include <mach/mach_error.h>
-#endif
 #include <spawn.h>
+#include <sys/wait.h>
+#endif
+#if defined(_WIN32)
+#include <generic_win_port.h>
+#endif
 #include <inttypes.h>
 #include "bsdtests.h"
 
@@ -454,18 +456,11 @@
 	usleep(100000);	// give 'gdb --waitfor=' a chance to find this proc
 }
 
-#if defined(__linux__) || defined(__FreeBSD__)
-static char** get_environment(void)
-{
-	extern char **environ; 
-	return environ;
-}
-#else
+#if defined(__APPLE__) && defined(__MACH__)
 static char** get_environment(void)
 {
 	return (* _NSGetEnviron());
 }
-#endif
 
 void
 test_leaks_pid(const char *name, pid_t pid)
@@ -517,15 +512,18 @@
 {
 	test_leaks_pid(name, getpid());
 }
+#endif
 
 void
 test_stop_after_delay(void *delay)
 {
 	if (delay != NULL) {
-		sleep((uint)(intptr_t)delay);
+		sleep((unsigned int)(intptr_t)delay);
 	}
 
+#if defined(__APPLE__) && defined(__MACH__)
 	test_leaks(NULL);
+#endif
 
 	fflush(stdout);
 	_exit(_test_exit_code);
diff --git a/tests/bsdtests.h b/tests/bsdtests.h
index e820823..e8e292e 100644
--- a/tests/bsdtests.h
+++ b/tests/bsdtests.h
@@ -63,18 +63,22 @@
 }
 #define __SOURCE_FILE__	__BASENAME__(__FILE__)
 
-__BEGIN_DECLS
+#if defined(__cplusplus)
+extern "C" {
+#endif
 
 /**
  * test_start() provides the TEST token. Use this once per test "tool"
  */
 void test_start(const char* desc);
 
+#if defined(__APPLE__) && defined(__MACH__)
 /**
  * Explicitly runs the 'leaks' test without stopping the process.
  */
 void test_leaks_pid(const char *name, pid_t pid);
 void test_leaks(const char *name);
+#endif
 
 /**
  * test_stop() checks for leaks during the tests using leaks-wrapper. Use this at the end of each "tool"
@@ -179,6 +183,8 @@
 #define test_skip2(m) _test_skip("", 0, m)
 void test_skip_format(const char *format, ...) __printflike(1,2);
 
-__END_DECLS
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif
 
 #endif /* __BSD_TEST_H__ */
diff --git a/tests/dispatch_test.c b/tests/dispatch_test.c
index 70cf90f..9809543 100644
--- a/tests/dispatch_test.c
+++ b/tests/dispatch_test.c
@@ -29,13 +29,16 @@
 #include <stdio.h>
 #if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
 #include <unistd.h>
-#endif
 #if __has_include(<sys/event.h>)
 #define HAS_SYS_EVENT_H 1
 #include <sys/event.h>
 #else
 #include <sys/poll.h>
 #endif
+#elif defined(_WIN32)
+#include <io.h>
+#include <Windows.h>
+#endif
 #include <assert.h>
 
 #include <dispatch/dispatch.h>
@@ -68,6 +71,20 @@
 	int r = kevent(kq, &ke, 1, &ke, 1, &t);
 	close(kq);
 	return r > 0;
+#elif defined(_WIN32)
+	HANDLE handle = (HANDLE)_get_osfhandle(fd);
+	// A zero-distance move retrieves the file pointer
+	LARGE_INTEGER currentPosition;
+	LARGE_INTEGER distance = {.QuadPart = 0};
+	if (!SetFilePointerEx(handle, distance, &currentPosition, FILE_CURRENT)) {
+		return false;
+	}
+	// If we are not at the end, assume the file is readable
+	LARGE_INTEGER fileSize;
+	if (GetFileSizeEx(handle, &fileSize) == 0) {
+		return false;
+	}
+	return currentPosition.QuadPart < fileSize.QuadPart;
 #else
 	struct pollfd pfd = {
 		.fd = fd,
@@ -141,6 +158,10 @@
 	close(temp_fd);
 	free(file_buf);
 	return path;
+#elif defined(_WIN32)
+	// TODO
+	fprintf(stderr, "dispatch_test_get_large_file() not implemented on Windows\n");
+	abort();
 #else
 #error "dispatch_test_get_large_file not implemented on this platform"
 #endif
@@ -154,6 +175,8 @@
 	(void)path;
 #elif defined(__unix__)
 	unlink(path);
+#elif defined(_WIN32)
+	// TODO
 #else
 #error "dispatch_test_release_large_file not implemented on this platform"
 #endif
diff --git a/tests/dispatch_test.h b/tests/dispatch_test.h
index 0f5be8a..415e419 100644
--- a/tests/dispatch_test.h
+++ b/tests/dispatch_test.h
@@ -18,12 +18,13 @@
  * @APPLE_APACHE_LICENSE_HEADER_END@
  */
 
-#include <sys/cdefs.h>
 #include <stdbool.h>
 #include <dispatch/dispatch.h>
 
 #if defined(__linux__) || defined(__FreeBSD__)
 #include <generic_unix_port.h>
+#elif defined(_WIN32)
+#include <generic_win_port.h>
 #endif
 
 #define test_group_wait(g) do { \
@@ -33,7 +34,9 @@
 		test_stop(); \
 	} } while (0)
 
-__BEGIN_DECLS
+#if defined(__cplusplus)
+extern "C" {
+#endif
 
 void dispatch_test_start(const char* desc);
 
@@ -50,4 +53,6 @@
 		size_t *newpl);
 #endif
 
-__END_DECLS
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif
diff --git a/tests/generic_win_port.c b/tests/generic_win_port.c
new file mode 100644
index 0000000..d9a52f4
--- /dev/null
+++ b/tests/generic_win_port.c
@@ -0,0 +1,223 @@
+#include <generic_win_port.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+#include <wchar.h>
+#include <Windows.h>
+
+static bool
+expand_wstr(WCHAR **str, size_t *capacity, size_t needed)
+{
+	if (*capacity >= needed) {
+		return true;
+	}
+	if (needed > UNICODE_STRING_MAX_CHARS) {
+		return false;
+	}
+	size_t new_capacity = *capacity ?: needed;
+	while (new_capacity < needed) {
+		new_capacity *= 2;
+	}
+	WCHAR *new_str = realloc(*str, new_capacity * sizeof(WCHAR));
+	if (!new_str) {
+		return false;
+	}
+	*str = new_str;
+	*capacity = new_capacity;
+	return true;
+}
+
+static bool
+append_wstr(WCHAR **str, size_t *capacity, size_t *len, WCHAR *suffix)
+{
+	size_t suffix_len = wcslen(suffix);
+	if (!expand_wstr(str, capacity, *len + suffix_len)) {
+		return false;
+	}
+	memcpy(*str + *len, suffix, suffix_len * sizeof(WCHAR));
+	*len += suffix_len;
+	return true;
+}
+
+WCHAR *
+argv_to_command_line(char **argv)
+{
+	// This is basically the reverse of CommandLineToArgvW(). We want to convert
+	// an argv array into a command-line compatible with CreateProcessW().
+	//
+	// See also:
+	// <https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-commandlinetoargvw>
+	// <https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/>
+	size_t len = 0, capacity = 0;
+	WCHAR *cmdline = NULL;
+	if (!expand_wstr(&cmdline, &capacity, 256)) {
+		goto error;
+	}
+	for (size_t i = 0; argv[i]; i++) {
+		// Separate arguments with spaces.
+		if (i > 0 && !append_wstr(&cmdline, &capacity, &len, L" ")) {
+			goto error;
+		}
+		// Surround the argument with quotes if it's empty or contains special
+		// characters.
+		char *cur = argv[i];
+		bool quoted = (*cur == '\0' || cur[strcspn(cur, " \t\n\v\"")] != '\0');
+		if (quoted && !append_wstr(&cmdline, &capacity, &len, L"\"")) {
+			goto error;
+		}
+		while (*cur != '\0') {
+			if (*cur == '"') {
+				// Quotes must be escaped with a backslash.
+				if (!append_wstr(&cmdline, &capacity, &len, L"\\\"")) {
+					goto error;
+				}
+				cur++;
+			} else if (*cur == '\\') {
+				// Windows treats backslashes differently depending on whether
+				// they're followed by a quote. If the backslashes aren't
+				// followed by a quote, then all slashes are copied into the
+				// argument string. Otherwise, only n/2 slashes are included.
+				// Count the number of slashes and double them if they're
+				// followed by a quote.
+				size_t backslashes = strspn(cur, "\\");
+				cur += backslashes;
+				// If the argument needs to be surrounded with quotes, we must
+				// also check if the backslashes are at the end of the argument
+				// because the added quote will follow them.
+				if (*cur == '"' || (quoted && *cur == '\0')) {
+					backslashes *= 2;
+				}
+				if (!expand_wstr(&cmdline, &capacity, len + backslashes)) {
+					goto error;
+				}
+				wmemset(&cmdline[len], L'\\', backslashes);
+				len += backslashes;
+			} else {
+				// Widen as many characters as possible.
+				size_t mb_len = strcspn(cur, "\"\\");
+				int wide_len = MultiByteToWideChar(CP_UTF8, 0, cur, mb_len,
+						NULL, 0);
+				if (wide_len == 0) {
+					goto error;
+				}
+				if (!expand_wstr(&cmdline, &capacity, len + wide_len)) {
+					goto error;
+				}
+				wide_len = MultiByteToWideChar(CP_UTF8, 0, cur, mb_len,
+						&cmdline[len], wide_len);
+				if (wide_len == 0) {
+					goto error;
+				}
+				cur += mb_len;
+				len += wide_len;
+			}
+		}
+		if (quoted && !append_wstr(&cmdline, &capacity, &len, L"\"")) {
+			goto error;
+		}
+	}
+	if (!expand_wstr(&cmdline, &capacity, len + 1)) {
+		goto error;
+	}
+	cmdline[len] = L'\0';
+	return cmdline;
+error:
+	free(cmdline);
+	return NULL;
+}
+
+int
+asprintf(char **strp, const char *format, ...)
+{
+	va_list arg1;
+	va_start(arg1, format);
+	int len = vsnprintf(NULL, 0, format, arg1);
+	va_end(arg1);
+	if (len >= 0) {
+		size_t size = (size_t)len + 1;
+		*strp = malloc(size);
+		if (!*strp) {
+			return -1;
+		}
+		va_list arg2;
+		va_start(arg2, format);
+		len = vsnprintf(*strp, size, format, arg2);
+		va_end(arg2);
+	}
+	return len;
+}
+
+void
+filetime_to_timeval(struct timeval *tp, const FILETIME *ft)
+{
+	int64_t ticks = ft->dwLowDateTime | (((int64_t)ft->dwHighDateTime) << 32);
+	static const int64_t ticks_per_sec = 10LL * 1000LL * 1000LL;
+	static const int64_t ticks_per_usec = 10LL;
+	if (ticks >= 0) {
+		tp->tv_sec = (long)(ticks / ticks_per_sec);
+		tp->tv_usec = (long)((ticks % ticks_per_sec) / ticks_per_usec);
+	} else {
+		tp->tv_sec = (long)((ticks + 1) / ticks_per_sec - 1);
+		tp->tv_usec = (long)((ticks_per_sec - 1 + (ticks + 1) % ticks_per_sec) / ticks_per_usec);
+	}
+}
+
+pid_t
+getpid(void)
+{
+	return (pid_t)GetCurrentProcessId();
+}
+
+int
+gettimeofday(struct timeval *tp, void *tzp)
+{
+	(void)tzp;
+	FILETIME ft;
+	GetSystemTimePreciseAsFileTime(&ft);
+	int64_t ticks = ft.dwLowDateTime | (((int64_t)ft.dwHighDateTime) << 32);
+	ticks -= 116444736000000000LL;  // Convert to Unix time
+	FILETIME unix_ft = {.dwLowDateTime = (DWORD)ticks, .dwHighDateTime = ticks >> 32};
+	filetime_to_timeval(tp, &unix_ft);
+	return 0;
+}
+
+void
+print_winapi_error(const char *function_name, DWORD error)
+{
+	char *message = NULL;
+	DWORD len = FormatMessageA(
+			FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
+			NULL,
+			error,
+			0,
+			(LPSTR)&message,
+			0,
+			NULL);
+	if (len > 0) {
+		// Note: FormatMessage includes a newline at the end of the message
+		fprintf(stderr, "%s: %s", function_name, message);
+		LocalFree(message);
+	} else {
+		fprintf(stderr, "%s: error %lu\n", function_name, error);
+	}
+}
+
+unsigned int
+sleep(unsigned int seconds)
+{
+	Sleep(seconds * 1000);
+	return 0;
+}
+
+int
+usleep(unsigned int usec)
+{
+	DWORD ms = usec / 1000;
+	if (ms == 0 && usec != 0) {
+		ms = 1;
+	}
+	Sleep(ms);
+	return 0;
+}
diff --git a/tests/generic_win_port.h b/tests/generic_win_port.h
new file mode 100644
index 0000000..cf96a21
--- /dev/null
+++ b/tests/generic_win_port.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <stdint.h>
+#include <Windows.h>
+
+typedef int kern_return_t;
+typedef int pid_t;
+
+#if defined(_WIN64)
+typedef long long ssize_t;
+#else
+typedef long ssize_t;
+#endif
+
+static inline int32_t
+OSAtomicIncrement32(volatile int32_t *var)
+{
+	return __c11_atomic_fetch_add((_Atomic(int)*)var, 1, __ATOMIC_RELAXED)+1;
+}
+
+static inline int32_t
+OSAtomicIncrement32Barrier(volatile int32_t *var)
+{
+	return __c11_atomic_fetch_add((_Atomic(int)*)var, 1, __ATOMIC_SEQ_CST)+1;
+}
+
+static inline int32_t
+OSAtomicAdd32(int32_t val, volatile int32_t *var)
+{
+	return __c11_atomic_fetch_add((_Atomic(int)*)var, val, __ATOMIC_RELAXED)+val;
+}
+
+WCHAR *
+argv_to_command_line(char **argv);
+
+int
+asprintf(char **strp, const char *format, ...);
+
+void
+filetime_to_timeval(struct timeval *tp, const FILETIME *ft);
+
+pid_t
+getpid(void);
+
+int
+gettimeofday(struct timeval *tp, void *tzp);
+
+void
+print_winapi_error(const char *function_name, DWORD error);
+
+unsigned int
+sleep(unsigned int seconds);
+
+int
+usleep(unsigned int usec);