From 6e78b6d7ca5b7374842658435b572ec0f7e9a003 Mon Sep 17 00:00:00 2001 From: Aaron Dierking Date: Wed, 30 Jan 2019 14:18:35 -0800 Subject: [PATCH] tests: port the test harness to Windows This provides a functional Windows port of bsdtestharness and its dependencies so that we can start running the test suite on Windows. Some tests already build and run correctly, while others either fail at runtime or don't compile. I'll be following up with fixes for individual tests later. --- tests/CMakeLists.txt | 17 ++- tests/bsdtestharness.c | 88 ++++++++++++--- tests/bsdtests.c | 22 ++-- tests/bsdtests.h | 10 +- tests/dispatch_test.c | 25 ++++- tests/dispatch_test.h | 11 +- tests/generic_win_port.c | 223 +++++++++++++++++++++++++++++++++++++++ tests/generic_win_port.h | 55 ++++++++++ 8 files changed, 412 insertions(+), 39 deletions(-) create mode 100644 tests/generic_win_port.c create mode 100644 tests/generic_win_port.h diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5909ad14c..3a4684f6c 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 @@ if(BSD_OVERLAY_FOUND) 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 @@ function(add_unit_test name) 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_group PRIVATE m) 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 f7b6ea31d..ca52d6e97 100644 --- a/tests/bsdtestharness.c +++ b/tests/bsdtestharness.c @@ -20,33 +20,29 @@ #include #include -#include #include #include #if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#include +#include +#include +#include #include +#elif defined(_WIN32) +#include +#include +#include #endif #include #ifdef __APPLE__ #include #include #endif -#include -#include -#if defined(__linux__) || defined(__FreeBSD__) -#include -#endif #include +#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 @@ main(int argc, char *argv[]) _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 @@ main(int argc, char *argv[]) //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 @@ main(int argc, char *argv[]) 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 @@ main(int argc, char *argv[]) kill(pid, SIGCONT); dispatch_main(); -#endif // SIMPLE_TEST_HARNESS +#endif return 0; } diff --git a/tests/bsdtests.c b/tests/bsdtests.c index 09700fae5..af716626b 100644 --- a/tests/bsdtests.c +++ b/tests/bsdtests.c @@ -29,14 +29,16 @@ #include #endif #include -#include -#include #include #ifdef __APPLE__ #include #include -#endif #include +#include +#endif +#if defined(_WIN32) +#include +#endif #include #include "bsdtests.h" @@ -454,18 +456,11 @@ test_start(const char* desc) 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(const char *name) { 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 e82082337..e8e292e6e 100644 --- a/tests/bsdtests.h +++ b/tests/bsdtests.h @@ -63,18 +63,22 @@ __BASENAME__(const char *_str_) } #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 @@ void _test_skip(const char* file, long line, const char* desc); #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 70cf90fef..9809543af 100644 --- a/tests/dispatch_test.c +++ b/tests/dispatch_test.c @@ -29,13 +29,16 @@ #include #if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) #include -#endif #if __has_include() #define HAS_SYS_EVENT_H 1 #include #else #include #endif +#elif defined(_WIN32) +#include +#include +#endif #include #include @@ -68,6 +71,20 @@ dispatch_test_check_evfilt_read_for_fd(int fd) 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, ¤tPosition, 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 @@ dispatch_test_get_large_file(void) 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 @@ dispatch_test_release_large_file(const char *path) (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 0f5be8af6..415e41974 100644 --- a/tests/dispatch_test.h +++ b/tests/dispatch_test.h @@ -18,12 +18,13 @@ * @APPLE_APACHE_LICENSE_HEADER_END@ */ -#include #include #include #if defined(__linux__) || defined(__FreeBSD__) #include +#elif defined(_WIN32) +#include #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 @@ int sysctlbyname(const char *name, void *oldp, size_t *oldlenp, void *newp, 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 000000000..d9a52f477 --- /dev/null +++ b/tests/generic_win_port.c @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +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: + // + // + 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 000000000..cf96a21a0 --- /dev/null +++ b/tests/generic_win_port.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +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);