//+private //+build windows package testing import win32 "core:sys/windows" import "core:runtime" import "core:intrinsics" import "core:time" Sema :: struct { count: i32, } sema_reset :: proc "contextless" (s: ^Sema) { intrinsics.atomic_store(&s.count, 0) } sema_wait :: proc "contextless" (s: ^Sema) { for { original_count := s.count for original_count == 0 { win32.WaitOnAddress(&s.count, &original_count, size_of(original_count), win32.INFINITE) original_count = s.count } if original_count == intrinsics.atomic_compare_exchange_strong(&s.count, original_count-1, original_count) { return } } } sema_wait_with_timeout :: proc "contextless" (s: ^Sema, duration: time.Duration) -> bool { if duration <= 0 { return false } for { original_count := intrinsics.atomic_load(&s.count) for start := time.tick_now(); original_count == 0; /**/ { if intrinsics.atomic_load(&s.count) != original_count { remaining := duration - time.tick_since(start) if remaining < 0 { return false } ms := u32(remaining/time.Millisecond) if !win32.WaitOnAddress(&s.count, &original_count, size_of(original_count), ms) { return false } } original_count = s.count } if original_count == intrinsics.atomic_compare_exchange_strong(&s.count, original_count-1, original_count) { return true } } } sema_post :: proc "contextless" (s: ^Sema, count := 1) { intrinsics.atomic_add(&s.count, i32(count)) if count == 1 { win32.WakeByAddressSingle(&s.count) } else { win32.WakeByAddressAll(&s.count) } } Thread_Proc :: #type proc(^Thread) MAX_USER_ARGUMENTS :: 8 Thread :: struct { using specific: Thread_Os_Specific, procedure: Thread_Proc, t: ^T, it: Internal_Test, success: bool, init_context: Maybe(runtime.Context), creation_allocator: runtime.Allocator, internal_fail_timeout: time.Duration, internal_fail_timeout_loc: runtime.Source_Code_Location, } Thread_Os_Specific :: struct { win32_thread: win32.HANDLE, win32_thread_id: win32.DWORD, done: bool, // see note in `is_done` } thread_create :: proc(procedure: Thread_Proc) -> ^Thread { __windows_thread_entry_proc :: proc "stdcall" (t_: rawptr) -> win32.DWORD { t := (^Thread)(t_) context = t.init_context.? or_else runtime.default_context() t.procedure(t) if t.init_context == nil { if context.temp_allocator.data == &runtime.global_default_temp_allocator_data { runtime.default_temp_allocator_destroy(auto_cast context.temp_allocator.data) } } intrinsics.atomic_store(&t.done, true) return 0 } thread := new(Thread) if thread == nil { return nil } thread.creation_allocator = context.allocator win32_thread_id: win32.DWORD win32_thread := win32.CreateThread(nil, 0, __windows_thread_entry_proc, thread, win32.CREATE_SUSPENDED, &win32_thread_id) if win32_thread == nil { free(thread, thread.creation_allocator) return nil } thread.procedure = procedure thread.win32_thread = win32_thread thread.win32_thread_id = win32_thread_id thread.init_context = context return thread } thread_start :: proc "contextless" (thread: ^Thread) { win32.ResumeThread(thread.win32_thread) } thread_join_and_destroy :: proc(thread: ^Thread) { if thread.win32_thread != win32.INVALID_HANDLE { win32.WaitForSingleObject(thread.win32_thread, win32.INFINITE) win32.CloseHandle(thread.win32_thread) thread.win32_thread = win32.INVALID_HANDLE } free(thread, thread.creation_allocator) } thread_terminate :: proc "contextless" (thread: ^Thread, exit_code: int) { win32.TerminateThread(thread.win32_thread, u32(exit_code)) } _fail_timeout :: proc(t: ^T, duration: time.Duration, loc := #caller_location) { assert(global_fail_timeout_thread == nil, "set_fail_timeout previously called", loc) thread := thread_create(proc(thread: ^Thread) { t := thread.t timeout := thread.internal_fail_timeout if !sema_wait_with_timeout(&global_fail_timeout_semaphore, timeout) { fail_now(t, "TIMEOUT", thread.internal_fail_timeout_loc) } }) thread.internal_fail_timeout = duration thread.internal_fail_timeout_loc = loc thread.t = t global_fail_timeout_thread = thread thread_start(thread) } global_fail_timeout_thread: ^Thread global_fail_timeout_semaphore: Sema global_threaded_runner_semaphore: Sema global_exception_handler: rawptr global_current_thread: ^Thread global_current_t: ^T run_internal_test :: proc(t: ^T, it: Internal_Test) { thread := thread_create(proc(thread: ^Thread) { exception_handler_proc :: proc "stdcall" (ExceptionInfo: ^win32.EXCEPTION_POINTERS) -> win32.LONG { switch ExceptionInfo.ExceptionRecord.ExceptionCode { case win32.EXCEPTION_DATATYPE_MISALIGNMENT, win32.EXCEPTION_BREAKPOINT, win32.EXCEPTION_ACCESS_VIOLATION, win32.EXCEPTION_ILLEGAL_INSTRUCTION, win32.EXCEPTION_ARRAY_BOUNDS_EXCEEDED, win32.EXCEPTION_STACK_OVERFLOW: sema_post(&global_threaded_runner_semaphore) return win32.EXCEPTION_EXECUTE_HANDLER } return win32.EXCEPTION_CONTINUE_SEARCH } global_exception_handler = win32.AddVectoredExceptionHandler(0, exception_handler_proc) context.assertion_failure_proc = proc(prefix, message: string, loc: runtime.Source_Code_Location) -> ! { errorf(t=global_current_t, format="%s %s", args={prefix, message}, loc=loc) intrinsics.trap() } t := thread.t global_fail_timeout_thread = nil sema_reset(&global_fail_timeout_semaphore) thread.it.p(t) sema_post(&global_fail_timeout_semaphore) thread_join_and_destroy(global_fail_timeout_thread) thread.success = true sema_post(&global_threaded_runner_semaphore) }) sema_reset(&global_threaded_runner_semaphore) global_current_t = t t._fail_now = proc() -> ! { intrinsics.trap() } thread.t = t thread.it = it thread.success = false thread_start(thread) sema_wait(&global_threaded_runner_semaphore) thread_terminate(thread, int(!thread.success)) thread_join_and_destroy(thread) win32.RemoveVectoredExceptionHandler(global_exception_handler) if !thread.success && t.error_count == 0 { t.error_count += 1 } return }