|
| 1 | +#!/bin/bash |
| 2 | +# The race detector's findings decide this step, not the exit status. |
| 3 | +# |
| 4 | +# libchdb starts process-global ClickHouse thread pools and offers no way to stop |
| 5 | +# them: the v26.7.0 C ABI has 50 functions and none of them shuts the engine down, |
| 6 | +# so ~33 engine threads are still parked on condvars when the test binary exits. |
| 7 | +# Under -race, Go's exit path runs racefini -> __tsan_fini, which tears the |
| 8 | +# sanitizer runtime down while those threads can still wake and run instrumented |
| 9 | +# code. One faults; because it is a C thread, Go's badsignal re-raises and the |
| 10 | +# process dies with a segfault after every test has already reported PASS. |
| 11 | +# |
| 12 | +# Established from a core dump: the crashing thread's stack is |
| 13 | +# runtime.raise <- raisebadsignal <- badsignal <- sigtrampgo, __tsan_fini and |
| 14 | +# __run_exit_handlers are on the stack, and 33 threads sit in |
| 15 | +# __pthread_cond_wait_common inside libchdb. Waiting before exit does not help — |
| 16 | +# measured 5/48, 3/48, 7/48, 3/48 crashes for waits of none, 500ms, 2s and 5s, |
| 17 | +# because BackgroundSchedulePool re-arms its tasks on a timer and never quiesces. |
| 18 | +# |
| 19 | +# So: run the race detector and fail on what it is for — a reported data race, a |
| 20 | +# failing test, a panic — and do not fail on that one segfault. Nothing about the |
| 21 | +# detector's coverage changes; only the way this step reads its own result. |
| 22 | +# |
| 23 | +# Remove this wrapper once the engine can be shut down (chdb-core needs either a |
| 24 | +# chdb_shutdown() for callers or an atexit that stops the pools). Then the plain |
| 25 | +# exit status is trustworthy again. |
| 26 | +set -uo pipefail |
| 27 | + |
| 28 | +LOG=$(mktemp) |
| 29 | +go test -race -timeout=180s ./... 2>&1 | tee "$LOG" |
| 30 | +status=${PIPESTATUS[0]} |
| 31 | + |
| 32 | +if grep -q "DATA RACE" "$LOG"; then |
| 33 | + echo "::error::the race detector reported a data race" |
| 34 | + exit 1 |
| 35 | +fi |
| 36 | +if grep -qE "^--- FAIL|^\s+--- FAIL" "$LOG"; then |
| 37 | + echo "::error::a test failed under the race detector" |
| 38 | + exit 1 |
| 39 | +fi |
| 40 | +if grep -qE "^panic:|^fatal error:" "$LOG"; then |
| 41 | + echo "::error::the test binary panicked under the race detector" |
| 42 | + exit 1 |
| 43 | +fi |
| 44 | + |
| 45 | +# A fault the Go runtime handled itself, i.e. one that hit a thread it owns while |
| 46 | +# a test was running. It prints its own header and a goroutine dump; the exit-time |
| 47 | +# crash never does, because there the signal lands on a libchdb thread and Go's |
| 48 | +# badsignal path re-raises without a dump. The distinction is what keeps the |
| 49 | +# exemption below from covering a crash during a test — a real one landed here |
| 50 | +# once already, chdb-io/chdb-go#46. |
| 51 | +if grep -qE "^(SIGSEGV|SIGBUS|SIGFPE|SIGILL|SIGABRT|SIGTRAP):" "$LOG"; then |
| 52 | + echo "::error::the runtime reported a fatal signal during a test, not at exit" |
| 53 | + exit 1 |
| 54 | +fi |
| 55 | + |
| 56 | +if [ "$status" -ne 0 ]; then |
| 57 | + # Positive evidence, not just the presence of the word: the package has to have |
| 58 | + # reported PASS and then died on the very next line. "Contains a segfault |
| 59 | + # somewhere" would exempt far more than the one crash this is about. |
| 60 | + if grep -A1 '^PASS$' "$LOG" | grep -q '^signal: segmentation fault'; then |
| 61 | + echo "::warning::tests passed; the binary segfaulted at exit, which is the" \ |
| 62 | + "known libchdb thread-shutdown issue and not a test result" |
| 63 | + exit 0 |
| 64 | + fi |
| 65 | + echo "::error::race step failed for a reason this wrapper does not recognise" \ |
| 66 | + "(exit $status; no data race, failing test, panic, or in-test signal, and" \ |
| 67 | + "no PASS immediately followed by a segfault)" |
| 68 | + exit "$status" |
| 69 | +fi |
0 commit comments