Skip to content

Commit 94c7ec9

Browse files
committed
Improve: Print stack-traces in test singal-handlers
1 parent c666f16 commit 94c7ec9

5 files changed

Lines changed: 181 additions & 3 deletions

File tree

.github/workflows/prerelease.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
env:
1010
GH_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
1111
PYTHONUTF8: 1
12+
PYTHONFAULTHANDLER: 1
1213
PYTHON_VERSION: 3.11
1314
DOTNET_VERSION: 8.0.x
1415
NODE_VERSION: 20

c/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ if (USEARCH_BUILD_TEST_C)
66
include(CTest)
77
enable_testing()
88
add_test(NAME test_c COMMAND test_c)
9+
10+
# Export the dynamic symbol table so `backtrace_symbols_fd` can resolve
11+
# function names when the in-test crash handler fires.
12+
set_target_properties(test_c PROPERTIES ENABLE_EXPORTS ON)
913
endif ()
1014

1115
# This article discusses a better way to allow building either static or shared libraries:

c/test.c

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,91 @@
1+
/**
2+
* @file test.c
3+
* @author Ash Vardanian
4+
* @brief Unit tests for the pure-C ABI of USearch (`usearch.h`).
5+
* @date June 25, 2023
6+
*
7+
* Exercises the lifecycle of `usearch_index_t` through the public C surface:
8+
* index creation with every supported metric and scalar kind, `add` / `get` /
9+
* `find` / `remove`, on-disk `save` / `load` / `view`, and error propagation
10+
* via `usearch_error_t`. The harness is intentionally dependency-free so it
11+
* can run in the same matrix as the C++ tests and on cross-compilation
12+
* targets where only the C runtime is available.
13+
*
14+
* On startup we install a signal handler (see `install_crash_handlers`) that
15+
* prints a native back-trace before re-raising, so CI logs pinpoint the
16+
* faulting frame instead of stopping at a bare exit code.
17+
*/
118
#include <errno.h>
2-
#include <stdio.h> // `remove`
19+
#include <signal.h> // `signal`, `raise`, `SIGSEGV`
20+
#include <stdio.h> // `remove`
321
#include <stdlib.h>
22+
#include <string.h> // `memset`
423
#include <sys/stat.h>
524

25+
/* Back-trace support for the C test harness. The `signal` API is standard C;
26+
* the back-trace itself is taken via an OS-specific facility since C has no
27+
* standard stack-introspection API. On Windows, `dbghelp.h` references types
28+
* (e.g. `PSTR`) that are only defined after `windows.h`, so the two headers
29+
* are separated by a blank line to keep clang-format from re-sorting them
30+
* into a single alphabetized block. */
31+
#if defined(_WIN32)
32+
#include <windows.h>
33+
34+
#include <dbghelp.h>
35+
#pragma comment(lib, "Dbghelp.lib")
36+
#elif defined(__unix__) || defined(__APPLE__)
37+
#include <execinfo.h>
38+
#include <unistd.h>
39+
#endif
40+
641
#include "usearch.h"
742

43+
static void usearch_write_backtrace(int signal_number) {
44+
fprintf(stderr, "\n[usearch] Fatal signal %d. Back-trace:\n", signal_number);
45+
#if defined(_WIN32)
46+
enum { backtrace_depth_limit = 64 };
47+
void* backtrace_frames[backtrace_depth_limit];
48+
USHORT backtrace_depth = CaptureStackBackTrace(0, backtrace_depth_limit, backtrace_frames, NULL);
49+
HANDLE current_process = GetCurrentProcess();
50+
SymInitialize(current_process, NULL, TRUE);
51+
52+
unsigned char symbol_info_buffer[sizeof(SYMBOL_INFO) + 256 * sizeof(char)];
53+
SYMBOL_INFO* symbol_info = (SYMBOL_INFO*)symbol_info_buffer;
54+
symbol_info->MaxNameLen = 255;
55+
symbol_info->SizeOfStruct = sizeof(SYMBOL_INFO);
56+
57+
for (USHORT frame_index = 0; frame_index < backtrace_depth; ++frame_index) {
58+
if (SymFromAddr(current_process, (DWORD64)backtrace_frames[frame_index], 0, symbol_info))
59+
fprintf(stderr, " #%2u %s + 0x%llx\n", (unsigned)frame_index, symbol_info->Name,
60+
(unsigned long long)((DWORD64)backtrace_frames[frame_index] - symbol_info->Address));
61+
else
62+
fprintf(stderr, " #%2u %p\n", (unsigned)frame_index, backtrace_frames[frame_index]);
63+
}
64+
#elif defined(__unix__) || defined(__APPLE__)
65+
enum { backtrace_depth_limit = 64 };
66+
void* backtrace_frames[backtrace_depth_limit];
67+
int backtrace_depth = backtrace(backtrace_frames, backtrace_depth_limit);
68+
backtrace_symbols_fd(backtrace_frames, backtrace_depth, STDERR_FILENO);
69+
#else
70+
(void)signal_number;
71+
fprintf(stderr, " <back-trace unavailable on this platform>\n");
72+
#endif
73+
fflush(stderr);
74+
}
75+
76+
static void usearch_crash_handler(int signal_number) {
77+
usearch_write_backtrace(signal_number);
78+
/* Restore the default disposition and re-raise so the shell / CI sees the true exit status. */
79+
signal(signal_number, SIG_DFL);
80+
raise(signal_number);
81+
}
82+
83+
static void install_crash_handlers(void) {
84+
int const fatal_signals[] = {SIGSEGV, SIGABRT, SIGILL, SIGFPE};
85+
for (unsigned signal_index = 0; signal_index < sizeof(fatal_signals) / sizeof(fatal_signals[0]); ++signal_index)
86+
signal(fatal_signals[signal_index], &usearch_crash_handler);
87+
}
88+
889
void expect(bool must_be_true, char const* message) {
990
if (must_be_true)
1091
return;
@@ -368,6 +449,7 @@ void test_view(size_t const collection_size, size_t const dimensions) {
368449
}
369450

370451
int main(int argc, char const* argv[]) {
452+
install_crash_handlers();
371453
printf("Running tests...\n");
372454
printf("USearch version: %s\n", usearch_version());
373455

cpp/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ if (USEARCH_BUILD_TEST_CPP)
77

88
target_include_directories(test_cpp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../stringzilla/include)
99

10+
# Export the dynamic symbol table so `backtrace_symbols` / `std::stacktrace`
11+
# can resolve function names when the in-test crash handler fires.
12+
set_target_properties(test_cpp PROPERTIES ENABLE_EXPORTS ON)
13+
1014
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
1115
target_compile_options(test_cpp PRIVATE -Wno-vla -Wno-unused-function -Wno-cast-function-type)
1216
endif ()

cpp/test.cpp

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,45 @@
1212
* - 128-bit `uuid_t` keys and `enum slot64_t : std::uint64_t` make most sense for
1313
* for database users, implementing portable, concurrent systems.
1414
*/
15+
#include <cassert> // `assert`
16+
#include <cmath> // `std::abs`
17+
#include <csignal> // `std::signal`, `SIGSEGV`, ...
18+
#include <cstdio> // `std::fprintf`
19+
#include <cstdlib> // `std::_Exit`
20+
1521
#include <algorithm> // `std::shuffle`
16-
#include <cassert> // `assert`
17-
#include <cmath> // `std::abs`
1822
#include <random> // `std::default_random_engine`
1923
#include <stdexcept> // `std::terminate`
2024
#include <unordered_map> // `std::unordered_map`
2125
#include <vector> // `std::vector`
2226

27+
// Back-trace support. Prefer the C++23 `<stacktrace>` library when the
28+
// toolchain + stdlib expose it (`__cpp_lib_stacktrace`); otherwise fall back
29+
// to the OS-native facility so that unit-test crashes in CI log something
30+
// useful beyond a bare exit code.
31+
#if defined(__has_include)
32+
#if __has_include(<stacktrace>)
33+
#include <stacktrace>
34+
#endif
35+
#endif
36+
#if defined(__cpp_lib_stacktrace) && __cpp_lib_stacktrace >= 202011L
37+
#define USEARCH_HAS_STD_STACKTRACE 1
38+
#else
39+
#define USEARCH_HAS_STD_STACKTRACE 0
40+
#if defined(_WIN32)
41+
// `windows.h` must precede `dbghelp.h` — the latter uses `PSTR` and friends
42+
// that are only defined after `windows.h`. The blank line keeps clang-format
43+
// from re-sorting the two headers into a single alphabetized block.
44+
#include <windows.h>
45+
46+
#include <dbghelp.h>
47+
#pragma comment(lib, "Dbghelp.lib")
48+
#else
49+
#include <execinfo.h>
50+
#include <unistd.h>
51+
#endif
52+
#endif
53+
2354
#define SZ_USE_X86_AVX512 0 // Sanitizers hate AVX512
2455
#include <stringzilla/stringzilla.hpp> // Levenshtein distance implementation
2556

@@ -1179,7 +1210,63 @@ void test_isolate() {
11791210
}
11801211
}
11811212

1213+
static void usearch_write_backtrace(int signal_number) {
1214+
std::fprintf(stderr, "\n[usearch] Fatal signal %d. Back-trace:\n", signal_number);
1215+
#if USEARCH_HAS_STD_STACKTRACE
1216+
// C++23 `std::stacktrace` covers every platform the library can reach.
1217+
auto const current_trace = std::stacktrace::current();
1218+
std::size_t frame_index = 0;
1219+
for (auto const& frame : current_trace) {
1220+
std::fprintf(stderr, " #%2zu %s\n", frame_index, std::to_string(frame).c_str());
1221+
++frame_index;
1222+
}
1223+
#elif defined(_WIN32)
1224+
// Fallback for MSVC stdlibs without `<stacktrace>`: DbgHelp API.
1225+
constexpr USHORT backtrace_depth_limit = 64;
1226+
void* backtrace_frames[backtrace_depth_limit];
1227+
USHORT backtrace_depth = CaptureStackBackTrace(0, backtrace_depth_limit, backtrace_frames, nullptr);
1228+
HANDLE current_process = GetCurrentProcess();
1229+
SymInitialize(current_process, nullptr, TRUE);
1230+
1231+
unsigned char symbol_info_buffer[sizeof(SYMBOL_INFO) + 256 * sizeof(char)];
1232+
SYMBOL_INFO* symbol_info = reinterpret_cast<SYMBOL_INFO*>(symbol_info_buffer);
1233+
symbol_info->MaxNameLen = 255;
1234+
symbol_info->SizeOfStruct = sizeof(SYMBOL_INFO);
1235+
1236+
for (USHORT frame_index = 0; frame_index < backtrace_depth; ++frame_index) {
1237+
if (SymFromAddr(current_process, reinterpret_cast<DWORD64>(backtrace_frames[frame_index]), 0, symbol_info))
1238+
std::fprintf(stderr, " #%2u %s + 0x%llx\n", static_cast<unsigned>(frame_index), symbol_info->Name,
1239+
static_cast<unsigned long long>(reinterpret_cast<DWORD64>(backtrace_frames[frame_index]) -
1240+
symbol_info->Address));
1241+
else
1242+
std::fprintf(stderr, " #%2u %p\n", static_cast<unsigned>(frame_index), backtrace_frames[frame_index]);
1243+
}
1244+
#else
1245+
// Fallback for POSIX stdlibs without `<stacktrace>`: `<execinfo.h>`.
1246+
constexpr int backtrace_depth_limit = 64;
1247+
void* backtrace_frames[backtrace_depth_limit];
1248+
int const backtrace_depth = backtrace(backtrace_frames, backtrace_depth_limit);
1249+
backtrace_symbols_fd(backtrace_frames, backtrace_depth, STDERR_FILENO);
1250+
#endif
1251+
std::fflush(stderr);
1252+
}
1253+
1254+
static void usearch_crash_handler(int signal_number) {
1255+
usearch_write_backtrace(signal_number);
1256+
// Restore the default disposition and re-raise so the shell / CI sees the true exit status.
1257+
std::signal(signal_number, SIG_DFL);
1258+
std::raise(signal_number);
1259+
}
1260+
1261+
static void install_crash_handlers() {
1262+
int const fatal_signals[] = {SIGSEGV, SIGABRT, SIGILL, SIGFPE};
1263+
for (int signal_number : fatal_signals)
1264+
std::signal(signal_number, &usearch_crash_handler);
1265+
}
1266+
11821267
int main(int, char**) {
1268+
install_crash_handlers();
1269+
11831270
std::printf("Hardware acceleration compiled: %s\n", hardware_acceleration_compiled());
11841271
std::printf("Hardware acceleration available: %s\n", hardware_acceleration_available());
11851272

0 commit comments

Comments
 (0)