aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorblob1807 <12388588+blob1807@users.noreply.github.com>2025-12-23 02:44:36 +1000
committerblob1807 <12388588+blob1807@users.noreply.github.com>2025-12-23 02:44:36 +1000
commitc9fff456cd19bf92c72a96bdbc98062c3530dc57 (patch)
tree2bde6ab5142a2d44a475f88f6e9b872dd1c7a868
parentb53ee3dba3f21bf5a22b9cf211ae49ff92b3e08b (diff)
[`core:testing`] Use Windows API for SIG handling
-rw-r--r--core/testing/signal_handler_libc.odin21
-rw-r--r--core/testing/signal_handler_windows.odin240
2 files changed, 246 insertions, 15 deletions
diff --git a/core/testing/signal_handler_libc.odin b/core/testing/signal_handler_libc.odin
index a1441f29d..74608bb48 100644
--- a/core/testing/signal_handler_libc.odin
+++ b/core/testing/signal_handler_libc.odin
@@ -1,5 +1,5 @@
#+private
-#+build windows, linux, darwin, freebsd, openbsd, netbsd, haiku
+#+build linux, darwin, freebsd, openbsd, netbsd, haiku
package testing
/*
@@ -8,6 +8,7 @@ package testing
List of contributors:
Feoramund: Total rewrite.
+ blob1807: Windows Win32 API rewrite.
*/
import "base:intrinsics"
@@ -24,18 +25,12 @@ import "core:terminal/ansi"
@(private="file") stop_test_passed: libc.sig_atomic_t
@(private="file") stop_test_alert: libc.sig_atomic_t
-when ODIN_ARCH == .i386 && ODIN_OS == .Windows {
- // Thread-local storage is problematic on Windows i386
- @(private="file")
- local_test_index: libc.sig_atomic_t
- @(private="file")
- local_test_index_set: bool
-} else {
- @(private="file", thread_local)
- local_test_index: libc.sig_atomic_t
- @(private="file", thread_local)
- local_test_index_set: bool
-}
+
+@(private="file", thread_local)
+local_test_index: libc.sig_atomic_t
+@(private="file", thread_local)
+local_test_index_set: bool
+
// Windows does not appear to have a SIGTRAP, so this is defined here, instead
// of in the libc package, just so there's no confusion about it being
diff --git a/core/testing/signal_handler_windows.odin b/core/testing/signal_handler_windows.odin
index 74ebe2998..cde0cad9d 100644
--- a/core/testing/signal_handler_windows.odin
+++ b/core/testing/signal_handler_windows.odin
@@ -1,6 +1,242 @@
#+private
+#+build windows
package testing
-__setup_signal_handler :: proc() {}
+/*
+ (c) Copyright 2024 Feoramund <rune@swevencraft.org>.
+ Made available under Odin's license.
+
+ List of contributors:
+ Feoramund: Total rewrite.
+ blob1807: Windows Win32 API rewrite.
+
+*/
+
+import "base:runtime"
+import "base:intrinsics"
+
+import "core:os"
+import "core:sync"
+import "core:c/libc"
+import "core:terminal/ansi"
+
+import win32 "core:sys/windows"
+
+
+@(private="file") stop_runner_flag: int
+
+@(private="file") stop_test_gate: sync.Mutex
+@(private="file") stop_test_index: int
+@(private="file") stop_test_signal: Exception_Code
+@(private="file") stop_test_passed: bool
+@(private="file") stop_test_alert: int
+
+
+when ODIN_ARCH == .i386 {
+ // Thread-local storage is problematic on Windows i386
+ @(private="file")
+ local_test_index: int
+ @(private="file")
+ local_test_index_set: bool
+} else {
+ @(private="file", thread_local)
+ local_test_index: int
+ @(private="file", thread_local)
+ local_test_index_set: bool
+}
+
+
+@(private="file")
+Exception_Code :: enum win32.DWORD {
+ Datatype_Misalignment = win32.EXCEPTION_DATATYPE_MISALIGNMENT,
+ Breakpoint = win32.EXCEPTION_BREAKPOINT,
+ Single_Step = win32.EXCEPTION_SINGLE_STEP,
+ Access_Violation = win32.EXCEPTION_ACCESS_VIOLATION,
+ In_Page_Error = win32.EXCEPTION_IN_PAGE_ERROR,
+ Illegal_Instruction = win32.EXCEPTION_ILLEGAL_INSTRUCTION,
+ Noncontinuable_Exception = win32.EXCEPTION_NONCONTINUABLE_EXCEPTION,
+ Invaild_Disposition = win32.EXCEPTION_INVALID_DISPOSITION,
+ Array_Bounds_Exceeded = win32.EXCEPTION_ARRAY_BOUNDS_EXCEEDED,
+ FLT_Denormal_Operand = win32.EXCEPTION_FLT_DENORMAL_OPERAND,
+ FLT_Divide_By_Zero = win32.EXCEPTION_FLT_DIVIDE_BY_ZERO,
+ FLT_Inexact_Result = win32.EXCEPTION_FLT_INEXACT_RESULT,
+ FLT_Invalid_Operation = win32.EXCEPTION_FLT_INVALID_OPERATION,
+ FLT_Overflow = win32.EXCEPTION_FLT_OVERFLOW,
+ FLT_Stack_Check = win32.EXCEPTION_FLT_STACK_CHECK,
+ FLT_Underflow = win32.EXCEPTION_FLT_UNDERFLOW,
+ INT_Divide_By_Zero = win32.EXCEPTION_INT_DIVIDE_BY_ZERO,
+ INT_Overflow = win32.EXCEPTION_INT_OVERFLOW,
+ PRIV_Instruction = win32.EXCEPTION_PRIV_INSTRUCTION,
+ Stack_Overflow = win32.EXCEPTION_STACK_OVERFLOW,
+}
+
+
+@(private="file")
+stop_runner_callback :: proc "system" (ctrl_type: win32.DWORD) -> win32.BOOL {
+ if ctrl_type == win32.CTRL_C_EVENT {
+ prev := intrinsics.atomic_add(&stop_runner_flag, 1)
+
+ // If the flag was already set (if this is the second signal sent for example),
+ // consider this a forced (not graceful) exit.
+ if prev > 0 {
+ os.exit(1)
+ }
+ // Say we've hanndled the signal.
+ return true
+ }
+
+ // This will also get called for other events which we don't handle for.
+ // Instead we pass it on to the next handler.
+ return false
+}
+
+@(private)
+stop_test_callback :: proc "system" (info: ^win32.EXCEPTION_POINTERS) -> win32.LONG {
+ if !local_test_index_set {
+ // We're a thread created by a test thread.
+ //
+ // There's nothing we can do to inform the test runner about who
+ // signalled, so hopefully the test will handle their own sub-threads.
+ return win32.EXCEPTION_CONTINUE_SEARCH
+ }
+
+ context = runtime.default_context()
+ code := Exception_Code(info.ExceptionRecord.ExceptionCode)
+
+ if local_test_index == -1 {
+ // We're the test runner, and we ourselves have caught a signal from
+ // which there is no recovery.
+ //
+ // The most we can do now is make sure the user's cursor is visible,
+ // nuke the entire processs, and hope a useful core dump survives.
+ if !global_ansi_disabled {
+ show_cursor := ansi.CSI + ansi.DECTCEM_SHOW
+ os.write_string(os.stdout, show_cursor)
+ os.flush(os.stdout)
+ }
+
+ // This is an attempt at being compliant by avoiding printf.
+ expbuf: [8]byte
+ expstr: string
+ {
+ expnum := cast(int)code
+ i := len(expbuf) - 2
+ for expnum > 0 {
+ m := expnum % 10
+ expnum /= 10
+ expbuf[i] = cast(u8)('0' + m)
+ i -= 1
+ }
+ expstr = cast(string)expbuf[1 + i:len(expbuf) - 1]
+ }
+
+ advisory_a := `
+The test runner's main thread has caught an unrecoverable error (signal `
+ advisory_b := `) and will now forcibly terminate.
+This is a dire bug and should be reported to the Odin developers.
+`
+ os.write_string(os.stderr, advisory_a)
+ os.write_string(os.stderr, expstr)
+ os.write_string(os.stderr, advisory_b)
+ os.flush(os.stderr)
+
+ win32.TerminateProcess(win32.GetCurrentProcess(), 1)
+ }
+
+ if sync.mutex_guard(&stop_test_gate) {
+ intrinsics.atomic_store(&stop_test_index, local_test_index)
+ intrinsics.atomic_store(&stop_test_signal, code)
+ passed: bool
+ check_passing: {
+ if location := local_test_assertion_raised.location; location != {} {
+ for i in 0..<local_test_expected_failures.location_count {
+ if local_test_expected_failures.locations[i] == location {
+ passed = true
+ break check_passing
+ }
+ }
+ }
+ if message := local_test_assertion_raised.message; message != "" {
+ for i in 0..<local_test_expected_failures.message_count {
+ if local_test_expected_failures.messages[i] == message {
+ passed = true
+ break check_passing
+ }
+ }
+ }
+ signal := local_test_expected_failures.signal
+ switch signal {
+ case libc.SIGILL: passed = code == .Illegal_Instruction
+ case libc.SIGSEGV: passed = code == .Access_Violation
+ case libc.SIGFPE:
+ #partial switch code {
+ case .FLT_Denormal_Operand ..= .INT_Overflow:
+ passed = true
+ }
+ }
+ }
+ intrinsics.atomic_store(&stop_test_passed, passed)
+ intrinsics.atomic_store(&stop_test_alert, 1)
+
+ }
+
+ // Pass on the exeption to the next handler. As we don't wont to recover from it.
+ // This will allow debuggers handle it properly.
+ return win32.EXCEPTION_CONTINUE_SEARCH
+}
+
+_setup_signal_handler :: proc() {
+ local_test_index = -1
+ local_test_index_set = true
+
+ // Catch user interrupt / CTRL-C.
+ win32.SetConsoleCtrlHandler(stop_runner_callback, win32.TRUE)
+
+ // For tests:
+ // Catch the following:
+ // - Asserts and panics;
+ // - Arithmetic errors; and
+ // - Segmentation faults (illegal memory access).
+ win32.AddVectoredExceptionHandler(0, stop_test_callback)
+}
+
+_setup_task_signal_handler :: proc(test_index: int) {
+ local_test_index = test_index
+ local_test_index_set = true
+}
+
+_should_stop_runner :: proc() -> bool {
+ return intrinsics.atomic_load(&stop_runner_flag) == 1
+}
+
+@(private="file")
+unlock_stop_test_gate :: proc(_: int, _: Stop_Reason, ok: bool) {
+ if ok {
+ sync.mutex_unlock(&stop_test_gate)
+ }
+}
+
+@(deferred_out=unlock_stop_test_gate)
+_should_stop_test :: proc() -> (test_index: int, reason: Stop_Reason, ok: bool) {
+ if intrinsics.atomic_load(&stop_test_alert) == 1 {
+ intrinsics.atomic_store(&stop_test_alert, 0)
+
+ test_index = intrinsics.atomic_load(&stop_test_index)
+ if intrinsics.atomic_load(&stop_test_passed) {
+ reason = .Successful_Stop
+ } else {
+ #partial switch intrinsics.atomic_load(&stop_test_signal) {
+ case .Illegal_Instruction: reason = .Illegal_Instruction
+ case .Access_Violation: reason = .Segmentation_Fault
+ case .Breakpoint, .Single_Step: reason = .Unhandled_Trap
+
+ case .FLT_Denormal_Operand ..= .INT_Overflow:
+ reason = .Arithmetic_Error
+ }
+ }
+ ok = true
+ }
+
+ return
+}
-_test_thread_cancel :: proc "contextless" () {}