From efb368383e87285d60c18697e9cf74c07cfeff04 Mon Sep 17 00:00:00 2001 From: PA Dev Bot <344001113+pa-devbot@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:36:05 +0000 Subject: [PATCH] Tests: dump a backtrace to stderr on a fatal signal in test mode When the command-line test runner crashes (e.g. a segfault in CI), the process's last stdio buffers are lost, so the log ends just before the crash with no indication of where it happened. Install handlers for SIGSEGV/SIGBUS/SIGABRT/SIGILL/SIGFPE (command-line test mode only, Apple/Linux; no-op on Windows) that write a native backtrace of the crashing thread straight to fd 2 - which also bypasses the app's std::cerr redirection into its logger - then restore the default disposition and re-raise, so the OS still writes its crash report and the exit code still reflects the crash. Also keep stdout line-buffered in test mode, so the test output reaches the log as it happens instead of being lost in the full buffer. An explicit 8192-byte buffer is used: with 0, MSVC's ucrt can end up with a zero-size buffer, which corrupts the next stdout write. --- .../Source/Tests/CommandLineTests.cpp | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/SerialPrograms/Source/Tests/CommandLineTests.cpp b/SerialPrograms/Source/Tests/CommandLineTests.cpp index a723fef213..79eddc61a9 100644 --- a/SerialPrograms/Source/Tests/CommandLineTests.cpp +++ b/SerialPrograms/Source/Tests/CommandLineTests.cpp @@ -11,14 +11,72 @@ #include "CommandLineTests.h" +#if defined(__APPLE__) || defined(__linux__) +#include +#include +#include +#endif + using std::cout; using std::endl; namespace PokemonAutomation{ +#if defined(__APPLE__) || defined(__linux__) +// On a fatal signal, dump a native backtrace of the crashing thread straight +// to the stderr file descriptor (fd 2, unbuffered). When stdout/stderr are +// piped (as in CI) the stdio buffers are lost when the process dies, and the +// app redirects std::cerr into its logger, so writing to fd 2 directly is the +// only way the backtrace reliably reaches the CI log. +// +// backtrace() and backtrace_symbols_fd() are not strictly async-signal-safe, +// so a re-entrancy guard stops a crash inside the handler from looping: if we +// re-enter, just die immediately. +static void crash_backtrace_handler(int sig){ + static volatile sig_atomic_t in_handler = 0; + if (in_handler){ + _exit(128 + sig); + } + in_handler = 1; + + const char msg[] = "\n[PA-CRASH] Fatal signal, backtrace of the crashing thread:\n"; + if (write(2, msg, sizeof(msg) - 1) < 0){ + // Nothing more we can do. + } + + void* frames[128]; + int n = backtrace(frames, 128); + // Writes one symbolized frame per line to fd 2. glibc declares the + // return type as int, Apple's as void; ignore it on both. + backtrace_symbols_fd(frames, n, 2); + + // Let the OS do its normal crash handling (crash report + exit code). + signal(sig, SIG_DFL); + raise(sig); +} + +static void install_crash_backtrace_handlers(){ + static const int sigs[] = { SIGSEGV, SIGBUS, SIGABRT, SIGILL, SIGFPE }; + for (int sig : sigs){ + signal(sig, crash_backtrace_handler); + } +} +#else +// No portable async-signal-safe backtrace API on Windows; the OS crash +// reports cover it there. +static void install_crash_backtrace_handlers(){} +#endif + int run_command_line_tests(bool augment_color_distortions){ + install_crash_backtrace_handlers(); + // Keep stdout line-buffered: when piped, stdio defaults to full + // buffering, and the last few KB of test output - including the lines + // right before a crash - would otherwise be lost in the buffer. An + // explicit buffer size is required: with 0, MSVC's ucrt can end up with + // a zero-size buffer, which corrupts the next stdout write. + setvbuf(stdout, nullptr, _IOLBF, 8192); cout << "Running parallel unit tests..." << endl; ComputerPrograms::CommandLineUnitTestRunner runner(global_logger_command_line()); if (runner.run(augment_color_distortions)){