From ad94c9297e59f4dccb06cb4dbbcb38dbc8d64edf Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Tue, 5 Apr 2011 01:43:17 +0200 Subject: [PATCH] First shot at test harness --- libol-tests.vcxproj | 95 +++++++++++++++ libol.sln | 6 + libol.vcxproj | 17 +-- test/{echo.c => echo-server.c} | 16 ++- test/echo.h | 4 - test/test-fail-always.c | 7 ++ test/test-list.h | 13 ++ test/test-pass-always.c | 6 + test/test-ping-pong.c | 30 +++-- test/test-runner-win32.c | 210 +++++++++++++++++++++++++++++++++ test/test-runner-win32.h | 10 ++ test/test-runner.c | 178 ++++++++++++++++++++++++++++ test/test-runner.h | 83 +++++++++++++ test/test.h | 12 ++ 14 files changed, 655 insertions(+), 32 deletions(-) create mode 100644 libol-tests.vcxproj rename test/{echo.c => echo-server.c} (91%) delete mode 100644 test/echo.h create mode 100644 test/test-fail-always.c create mode 100644 test/test-list.h create mode 100644 test/test-pass-always.c create mode 100644 test/test-runner-win32.c create mode 100644 test/test-runner-win32.h create mode 100644 test/test-runner.c create mode 100644 test/test-runner.h create mode 100644 test/test.h diff --git a/libol-tests.vcxproj b/libol-tests.vcxproj new file mode 100644 index 00000000..911a266f --- /dev/null +++ b/libol-tests.vcxproj @@ -0,0 +1,95 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + + {301fe650-cd34-14e5-6b63-42e383fa02bc} + false + true + false + true + false + + + + + + + + + + + + + + + + + + test + {B30D70EF-2678-4393-B322-74E1476757DC} + ManagedCProj + liboltests + + + + Application + Unicode + + + Application + Unicode + + + + + + + + + + + + if exist app.config copy app.config "$(OutDir)app.config" + true + false + .exe + .exe + + + + Level3 + Disabled + WIN32;_DEBUG;%(PreprocessorDefinitions) + NotUsing + + + true + ws2_32.lib + Default + + + + + Level3 + WIN32;NDEBUG;%(PreprocessorDefinitions) + NotUsing + + + true + ws2_32.lib + + + + + + \ No newline at end of file diff --git a/libol.sln b/libol.sln index b9144123..45cef918 100644 --- a/libol.sln +++ b/libol.sln @@ -3,6 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 11.00 # Visual Studio 2010 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libol", "libol.vcxproj", "{301FE650-CD34-14E5-6B63-42E383FA02BC}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libol-tests", "libol-tests.vcxproj", "{B30D70EF-2678-4393-B322-74E1476757DC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 @@ -13,6 +15,10 @@ Global {301FE650-CD34-14E5-6B63-42E383FA02BC}.Debug|Win32.Build.0 = Debug|Win32 {301FE650-CD34-14E5-6B63-42E383FA02BC}.Release|Win32.ActiveCfg = Release|Win32 {301FE650-CD34-14E5-6B63-42E383FA02BC}.Release|Win32.Build.0 = Release|Win32 + {B30D70EF-2678-4393-B322-74E1476757DC}.Debug|Win32.ActiveCfg = Debug|Win32 + {B30D70EF-2678-4393-B322-74E1476757DC}.Debug|Win32.Build.0 = Debug|Win32 + {B30D70EF-2678-4393-B322-74E1476757DC}.Release|Win32.ActiveCfg = Release|Win32 + {B30D70EF-2678-4393-B322-74E1476757DC}.Release|Win32.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/libol.vcxproj b/libol.vcxproj index 7a68a3df..30861fdb 100644 --- a/libol.vcxproj +++ b/libol.vcxproj @@ -15,11 +15,11 @@ - Application + StaticLibrary true - Application + StaticLibrary false @@ -40,7 +40,7 @@ - WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + WIN32;_DEBUG;%(PreprocessorDefinitions) MultiThreadedDebugDLL Level3 ProgramDatabase @@ -55,7 +55,7 @@ - WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + WIN32;NDEBUG;%(PreprocessorDefinitions) MultiThreadedDLL Level3 ProgramDatabase @@ -70,20 +70,11 @@ - - - - true - - - - - diff --git a/test/echo.c b/test/echo-server.c similarity index 91% rename from test/echo.c rename to test/echo-server.c index 5d8d406a..6e72ad82 100644 --- a/test/echo.c +++ b/test/echo-server.c @@ -1,7 +1,7 @@ #include "../ol.h" +#include "test.h" #include #include -#include #define BUFSIZE 1024 @@ -111,7 +111,19 @@ int echo_start(int port) { return 0; } + int echo_stop() { assert(server != NULL); - ol_close(server); + return ol_close(server); +} + + +TEST_IMPL(echo_server) { + ol_init(); + if (echo_start(TEST_PORT)) + return 1; + + fprintf(stderr, "Listening!\n"); + ol_run(); + return 0; } \ No newline at end of file diff --git a/test/echo.h b/test/echo.h deleted file mode 100644 index 52246a2c..00000000 --- a/test/echo.h +++ /dev/null @@ -1,4 +0,0 @@ -#include "../ol.h" - -int echo_start(int port); -int echo_stop(); \ No newline at end of file diff --git a/test/test-fail-always.c b/test/test-fail-always.c new file mode 100644 index 00000000..5780a67c --- /dev/null +++ b/test/test-fail-always.c @@ -0,0 +1,7 @@ +#include "test.h" + +TEST_IMPL(fail_always) { + /* This test always fails. It is used to test the test runner. */ + assert("Yes, it always fails" && 0); + return 1; +} \ No newline at end of file diff --git a/test/test-list.h b/test/test-list.h new file mode 100644 index 00000000..28714fd3 --- /dev/null +++ b/test/test-list.h @@ -0,0 +1,13 @@ +TEST_DECLARE (echo_server) +TEST_DECLARE (ping_pong) +TEST_DECLARE (pass_always) +TEST_DECLARE (fail_always) + +TEST_LIST_START + TEST_ENTRY (ping_pong) + TEST_HELPER (ping_pong, echo_server) + + TEST_ENTRY (fail_always) + + TEST_ENTRY (pass_always) +TEST_LIST_END \ No newline at end of file diff --git a/test/test-pass-always.c b/test/test-pass-always.c new file mode 100644 index 00000000..975913a7 --- /dev/null +++ b/test/test-pass-always.c @@ -0,0 +1,6 @@ +#include "test.h" + +TEST_IMPL(pass_always) { + /* This test always passes. It is used to test the test runner. */ + return 0; +} \ No newline at end of file diff --git a/test/test-ping-pong.c b/test/test-ping-pong.c index 7b6f49ee..9f25d555 100644 --- a/test/test-ping-pong.c +++ b/test/test-ping-pong.c @@ -1,11 +1,14 @@ #include "../ol.h" -#include "echo.h" +#include "test.h" #include #include +#include static int completed_pingers = 0; static ol_req connect_req; +#define NUM_PINGS 50 + /* 64 bytes is enough for a pinger */ #define BUFSIZE 64 @@ -27,11 +30,10 @@ void pinger_on_close(ol_handle* handle, ol_err err) { assert(!err); p = (pinger*)handle->data; - assert(1000 == p->pongs); + assert(NUM_PINGS == p->pongs); free(p); ol_free(handle); completed_pingers++; - echo_stop(); } void pinger_after_read(ol_req* req, size_t nread) { @@ -53,7 +55,7 @@ void pinger_after_read(ol_req* req, size_t nread) { p->state = (p->state + 1) % 5; if (p->state == 0) { p->pongs++; - if (p->pongs < 1000) { + if (p->pongs < NUM_PINGS) { r = ol_write2(p->handle, PING); assert(!r); } else { @@ -75,10 +77,16 @@ void pinger_try_read(pinger* pinger) { void pinger_on_connect(ol_req *req, ol_err err) { - int r; ol_handle *handle = req->handle; + pinger *p; + int r; - pinger *p = calloc(sizeof(pinger), 1); + if (err) { + /* error */ + assert(0); + } + + p = calloc(sizeof(pinger), 1); p->handle = handle; p->buf.base = p->read_buffer; p->buf.len = BUFSIZE; @@ -97,10 +105,10 @@ void pinger_on_connect(ol_req *req, ol_err err) { int pinger_connect(int port) { - /* Try to connec to the server and do 1000 ping-pongs. */ + /* Try to connec to the server and do NUM_PINGS ping-pongs. */ ol_handle* handle = ol_tcp_handle_new(pinger_on_close, NULL); struct sockaddr_in client_addr = ol_ip4_addr("0.0.0.0", 0); - struct sockaddr_in server_addr = ol_ip4_addr("127.0.0.1", port); + struct sockaddr_in server_addr = ol_ip4_addr("127.0.0.1", TEST_PORT); ol_bind(handle, (struct sockaddr*)&client_addr); ol_req_init(&connect_req, &pinger_on_connect); @@ -108,13 +116,9 @@ int pinger_connect(int port) { } -int main(int argc, char** argv) { +TEST_IMPL(ping_pong) { ol_init(); - if (echo_start(8000)) { - return 1; - } - if (pinger_connect(8000)) { return 2; } diff --git a/test/test-runner-win32.c b/test/test-runner-win32.c new file mode 100644 index 00000000..8236bff1 --- /dev/null +++ b/test/test-runner-win32.c @@ -0,0 +1,210 @@ + +#include +#include +#include +#include + +#include "test-runner.h" + + +int process_start(char *name, process_info_t *p) { + HANDLE file = INVALID_HANDLE_VALUE; + HANDLE nul = INVALID_HANDLE_VALUE; + WCHAR path[MAX_PATH], filename[MAX_PATH]; + WCHAR image[MAX_PATH + 1]; + WCHAR args[MAX_PATH * 2]; + STARTUPINFOW si; + PROCESS_INFORMATION pi; + DWORD result; + + if (GetTempPathW(sizeof(path), (WCHAR*)&path) == 0) + goto error; + if (GetTempFileNameW((WCHAR*)&path, L"ol_", 0, (WCHAR*)&filename) == 0) + goto error; + + file = CreateFileW((WCHAR*)filename, + GENERIC_READ | GENERIC_WRITE, + 0, + NULL, + CREATE_ALWAYS, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, + NULL); + if (file == INVALID_HANDLE_VALUE) + goto error; + + if (!SetHandleInformation(file, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)) + goto error; + + nul = CreateFileA("nul", + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL); + if (nul == INVALID_HANDLE_VALUE) + goto error; + + if (!SetHandleInformation(nul, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)) + goto error; + + result = GetModuleFileName(NULL, (WCHAR*)&image, sizeof(image)); + if (result == 0 || result == sizeof(image)) + goto error; + + if (_snwprintf_s((wchar_t*)&args, + sizeof(args) / sizeof(wchar_t), + _TRUNCATE, + L"\"%s\" %S meh", + image, + name) < 0) + goto error; + + memset((void*)&si, 0, sizeof(si)); + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = nul; + si.hStdOutput = file; + si.hStdError = file; + + if (!CreateProcessW(image, args, NULL, NULL, TRUE, + 0, NULL, NULL, &si, &pi)) + goto error; + + CloseHandle(pi.hThread); + + SetHandleInformation(nul, HANDLE_FLAG_INHERIT, 0); + SetHandleInformation(file, HANDLE_FLAG_INHERIT, 0); + + p->stdio_in = nul; + p->stdio_out = file; + p->process = pi.hProcess; + p->name = name; + + return 0; + + +error: + if (file != INVALID_HANDLE_VALUE) + CloseHandle(file); + if (nul != INVALID_HANDLE_VALUE) + CloseHandle(file); + + return -1; +} + + +/* Timeout is is msecs. Set timeout < 0 to never time out. */ +/* Returns 0 when all processes are terminated, -1 on timeout. */ +int process_wait(process_info_t *vec, int n, int timeout) { + int i; + HANDLE handles[MAXIMUM_WAIT_OBJECTS]; + DWORD timeout_api, result; + + /* If there's nothing to wait for, return immedately. */ + if (n == 0) + return 0; + + assert(n <= MAXIMUM_WAIT_OBJECTS); + + for (i = 0; i < n; i++) + handles[i] = vec[i].process; + + if (timeout >= 0) { + timeout_api = (DWORD)timeout; + } else { + timeout_api = INFINITE; + } + + result = WaitForMultipleObjects(n, handles, TRUE, timeout_api); + + if (result >= WAIT_OBJECT_0 && result < WAIT_OBJECT_0 + n) { + /* All processes are terminated. */ + return 0; + } + if (result == WAIT_TIMEOUT) { + return -2; + } + return -1; +} + + +long int process_output_size(process_info_t *p) { + LARGE_INTEGER size; + if (!GetFileSizeEx(p->stdio_out, &size)) + return -1; + return (long int)size.QuadPart; +} + + +int process_copy_output(process_info_t *p, int fd) { + /* Any errors in this function are ignored */ + DWORD read; + char buf[1024]; + + if (SetFilePointer(p->stdio_out, 0, 0, FILE_BEGIN) == INVALID_SET_FILE_POINTER) + return -1; + + while (ReadFile(p->stdio_out, (void*)&buf, sizeof(buf), &read, NULL) && + read > 0) + write(fd, buf, read); + + if (GetLastError() != ERROR_HANDLE_EOF) + return -1; + + return 0; +} + + +char* process_get_name(process_info_t *p) { + return p->name; +} + + +int process_terminate(process_info_t *p) { + /* If it fails the process is probably already closed. */ + if (!TerminateProcess(p->process, 1)) + return -1; + return 0; +} + + +int process_reap(process_info_t *p) { + DWORD exitCode; + if (!GetExitCodeProcess(p->process, &exitCode)) + return -1; + return (int)exitCode; +} + + +void process_cleanup(process_info_t *p) { + CloseHandle(p->process); + CloseHandle(p->stdio_in); + CloseHandle(p->stdio_out); +} + + +int rewind_cursor() { + HANDLE handle; + CONSOLE_SCREEN_BUFFER_INFO info; + COORD coord; + + handle = (HANDLE)_get_osfhandle(fileno(stdout)); + if (handle == INVALID_HANDLE_VALUE) + return -1; + + if (!GetConsoleScreenBufferInfo(handle, &info)) + return -1; + + coord = info.dwCursorPosition; + if (coord.Y <= 0) + return -1; + + coord.Y--; + coord.X = 0; + + if (!SetConsoleCursorPosition(handle, coord)) + return -1; + + return 0; +} \ No newline at end of file diff --git a/test/test-runner-win32.h b/test/test-runner-win32.h new file mode 100644 index 00000000..620468ca --- /dev/null +++ b/test/test-runner-win32.h @@ -0,0 +1,10 @@ + +#include + + +typedef struct { + HANDLE process; + HANDLE stdio_in; + HANDLE stdio_out; + char *name; +} process_info_t; diff --git a/test/test-runner.c b/test/test-runner.c new file mode 100644 index 00000000..6d73a4b8 --- /dev/null +++ b/test/test-runner.c @@ -0,0 +1,178 @@ + +#include "test-runner.h" + +#include +#include +#include +#include + +/* Actual tests and helpers are defined in test-list.h */ +#include "test-list.h" + +/* The maximum number of processes (main + helpers) that a test can have. */ +#define TEST_MAX_PROCESSES 8 + +/* The time in milliseconds after which a single test times out, */ +#define TEST_TIMEOUT 20000 + +/* Die with fatal error. */ +#define FATAL(msg) assert(msg && 0); + +/* Log to stderr. */ +#define LOG(...) fprintf(stderr, "%s", __VA_ARGS__) +#define LOGF(...) fprintf(stderr, __VA_ARGS__) + + +/* + * Runs an individual test; returns 1 if the test succeeded, 0 if it failed. + * If the test fails it prints diagnostic information. + */ +int run_test(test_entry_t *test) { + int i, result, success; + char errmsg[256]; + test_entry_t *helper; + int process_count; + process_info_t processes[TEST_MAX_PROCESSES]; + process_info_t *main_process; + + success = 0; + + process_count = 0; + + /* Start all helpers for this test first */ + for (helper = (test_entry_t*)&TESTS; helper->main; helper++) { + if (helper->is_helper && + strcmp(test->test_name, helper->test_name) == 0) { + if (process_start(helper->process_name, &processes[process_count]) == -1) { + sprintf_s((char*)&errmsg, sizeof(errmsg), "process `%s` failed to start.", helper->process_name); + goto finalize; + } + process_count++; + } + } + + /* Start the main test process. */ + if (process_start(test->process_name, &processes[process_count]) == -1) { + sprintf_s((char*)&errmsg, sizeof(errmsg), "process `%s` failed to start.", test->process_name); + goto finalize; + } + main_process = &processes[process_count]; + process_count++; + + /* Wait for the main process to terminate. */ + result = process_wait(main_process, 1, TEST_TIMEOUT); + if (result == -1) { + FATAL("process_wait failed\n"); + } else if (result == -2) { + sprintf_s((char*)&errmsg, sizeof(errmsg), "timeout."); + goto finalize; + } + + /* Reap main process */ + result = process_reap(main_process); + if (result != 0) { + sprintf_s((char*)&errmsg, sizeof(errmsg), "exit code %d.", result); + goto finalize; + } + + /* Yes! did it. */ + success = 1; + +finalize: + /* Kill all (helper) processes that are still running. */ + for (i = 0; i < process_count; i++) + process_terminate(&processes[i]); + + /* Wait until all processes have really terminated. */ + if (process_wait((process_info_t*)&processes, process_count, -1) < 0) + FATAL("process_wait failed\n"); + + /* Show error and output from processes if the test failed. */ + if (!success) { + LOG("===============================================================================\n"); + LOGF("Test `%s` failed: %s\n", test->test_name, errmsg); + for (i = 0; i < process_count; i++) { + switch (process_output_size(&processes[i])) { + case -1: + LOGF("Output from process `%s`: << unavailable >>\n", process_get_name(&processes[i])); + break; + + case 0: + LOGF("Output from process `%s`: << no output >>\n", process_get_name(&processes[i])); + break; + + default: + LOGF("Output from process `%s`:\n", process_get_name(&processes[i])); + process_copy_output(&processes[i], fileno(stderr)); + break; + } + } + LOG("\n"); + } + + /* Clean up all process handles. */ + for (i = 0; i < process_count; i++) + process_cleanup(&processes[i]); + + return success; +} + + +void log_progress(int total, int passed, int failed, char *name) { + LOGF("[%% %3d|+ %3d|- %3d]: %-50s\n", (passed + failed) / total * 100, passed, failed, name); +} + + +int main(int argc, char **argv) { + int total, passed, failed; + test_entry_t *test; + +#ifdef _WIN32 + /* On windows disable the "application crashed" popup */ + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); +#endif + + /* Disable output buffering */ + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + if (argc > 1) { + /* A specific test process is being started. */ + for (test = (test_entry_t*)&TESTS; test->main; test++) { + if (strcmp(argv[1], test->process_name) == 0) + return test->main(); + } + LOGF("Test process %s not found!\n", argv[1]); + return 255; + + } else { + /* Count the number of tests */ + total = 0; + test = (test_entry_t*)&TESTS; + for (test = (test_entry_t*)&TESTS; test->main; test++) { + if (!test->is_helper) + total++; + } + + /* Run all tests */ + passed = 0; + failed = 0; + test = (test_entry_t*)&TESTS; + for (test = (test_entry_t*)&TESTS; test->main; test++) { + if (test->is_helper) + continue; + + log_progress(total, passed, failed, test->test_name); + rewind_cursor(); + + if (run_test(test)) { + passed++; + } else { + failed++; + } + } + log_progress(total, passed, failed, "Done."); + + return 0; + } +} \ No newline at end of file diff --git a/test/test-runner.h b/test/test-runner.h new file mode 100644 index 00000000..be76b7ba --- /dev/null +++ b/test/test-runner.h @@ -0,0 +1,83 @@ + +#ifndef TEST_RUNNER_H_ +#define TEST_RUNNER_H_ + +/* + * Struct to store both tests and to define helper processes for tests. + */ +typedef struct { + char *test_name; + char *process_name; + int (*main)(); + int is_helper; +} test_entry_t; + + +/* + * Macros used by test-list.h + */ +#define TEST_DECLARE(name) \ + int run_##name(); + +#define TEST_LIST_START \ + test_entry_t TESTS[] = { + +#define TEST_LIST_END \ + { 0, 0, 0, 0 } \ + }; + +#define TEST_ENTRY(name) \ + { #name, #name, &run_##name, 0 }, + +#define TEST_HELPER(name, proc) \ + { #name, #proc, &run_##proc, 1 }, + + +/* + * Include platform-dependent definitions + */ +#ifdef _WIN32 +# include "test-runner-win32.h" +#else +# include "test-runner-unix.h" +#endif + + +/* + * Stuff that should be implemented by test-runner-.h + * All functions return 0 on success, -1 on failure, unless specified + * otherwise. + */ + +/* Invoke "arv[0] test-name". Store process info in *p. */ +/* Make sure that all stdio output of the processes is buffered up. */ +int process_start(char *name, process_info_t *p); + +/* Wait for all `n` processes in `vec` to terminate. */ +/* Time out after `timeout` msec, or never if timeout == -1 */ +/* Return 0 if all processes are terminated, -1 on error, -2 on timeout. */ +int process_wait(process_info_t *vec, int n, int timeout); + +/* Returns the number of bytes in the stdio output buffer for process `p`. */ +long int process_output_size(process_info_t *p); + +/* Copy the contents of the stdio output buffer to `fd`. */ +int process_copy_output(process_info_t *p, int fd); + +/* Return the name that was specified when `p` was started by process_start */ +char* process_get_name(process_info_t *p); + +/* Terminate process `p`. */ +int process_terminate(process_info_t *p); + +/* Return the return value of process p. */ +/* On error, return -1. */ +int process_reap(process_info_t *p); + +/* Clean up after terminating process `p` (e.g. free the output buffer etc.) */ +void process_cleanup(process_info_t *p); + +/* Move the console cursor one line up and back to the first column. */ +int rewind_cursor(); + +#endif /* TEST_RUNNER_H_ */ \ No newline at end of file diff --git a/test/test.h b/test/test.h new file mode 100644 index 00000000..f65b0bb7 --- /dev/null +++ b/test/test.h @@ -0,0 +1,12 @@ +#ifndef TEST_H_ +#define TEST_H_ + +#include + + +#define TEST_IMPL(name) \ + int run_##name() + +#define TEST_PORT 8123 + +#endif /* TEST_H_ */ \ No newline at end of file