#ifndef AKERR_TEST_THREADS_H #define AKERR_TEST_THREADS_H /* * Shared helpers for the thread-safety tests. * * These tests are checkable on their own -- they assert exclusive ownership of * pool slots and of reserved ranges, which is a property, not a symptom -- but * the run that proves the absence of a data race is the one under * ThreadSanitizer: * * cmake -S . -B build/tsan -DAKERR_SANITIZE=thread * cmake --build build/tsan * ctest --test-dir build/tsan --output-on-failure * * Everything shared between the threads here is either read-only after the * threads start, or touched through __atomic builtins. Anything else would be a * race in the *test*, and TSan cannot tell whose bug it is reporting. * * A test body runs on AKERR_TEST_THREADS threads that meet at a barrier first, * so they arrive at the library together instead of in start-up order. Failed * checks are counted per thread rather than returned early: a thread that * abandoned its work would leave the others holding pool slots and turn one * failure into a cascade of unrelated ones. */ #include "akerror.h" #include #include #define AKERR_TEST_THREADS 8 typedef struct { int id; /* 1-based: 0 means "no thread" below */ int failures; pthread_barrier_t *barrier; } akerr_ThreadArg; #define AKERR_TCHECK(__arg, __cond) \ do { \ if ( !(__cond) ) { \ fprintf(stderr, "CHECK FAILED (thread %d): %s at %s:%d\n", \ (__arg)->id, #__cond, __FILE__, __LINE__); \ (__arg)->failures += 1; \ } \ } while ( 0 ) /* * A logger that counts instead of printing. The capturing logger in * err_capture.h appends to a shared buffer with a shared length, which is a * data race the moment two threads log at once; these tests need a logger that * is safe to install before spawning and still shows that something was * reported. */ static int akerr_thread_log_count; static void __attribute__((unused)) akerr_thread_logger(const char *fmt, ...) { (void)fmt; __atomic_fetch_add(&akerr_thread_log_count, 1, __ATOMIC_RELAXED); } static int __attribute__((unused)) akerr_thread_logs(void) { return __atomic_load_n(&akerr_thread_log_count, __ATOMIC_RELAXED); } /* * Independent bookkeeping of who holds which pool slot. The library's own * refcount says a slot is checked out; this says which thread it was checked * out to, which is the part a racing akerr_next_error() would get wrong by * handing one slot to two threads at once. */ static int akerr_slot_owner[AKERR_MAX_ARRAY_ERROR]; /* Returns 0 on success, or the id of the thread that already holds the slot. */ static int __attribute__((unused)) akerr_slot_claim(int slot, int id) { int unowned = 0; if ( __atomic_compare_exchange_n(&akerr_slot_owner[slot], &unowned, id, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE) ) { return 0; } return unowned; } static int __attribute__((unused)) akerr_slot_holder(int slot) { return __atomic_load_n(&akerr_slot_owner[slot], __ATOMIC_ACQUIRE); } /* * Give the slot up *before* releasing the error context. The other order hands * the slot back to the pool while this thread still claims it, and the next * thread to be given it reports a violation that is the test's fault. */ static void __attribute__((unused)) akerr_slot_drop(int slot) { __atomic_store_n(&akerr_slot_owner[slot], 0, __ATOMIC_RELEASE); } /* * Run `body` on AKERR_TEST_THREADS threads and return the total number of * failed checks. A thread that cannot be created is itself a failure, but the * ones already running still get joined. */ static int __attribute__((unused)) akerr_run_threads(void *(*body)(void *)) { pthread_t threads[AKERR_TEST_THREADS]; akerr_ThreadArg args[AKERR_TEST_THREADS]; pthread_barrier_t barrier; int started = 0; int failures = 0; if ( pthread_barrier_init(&barrier, NULL, AKERR_TEST_THREADS) != 0 ) { fprintf(stderr, "CHECK FAILED: pthread_barrier_init at %s:%d\n", __FILE__, __LINE__); return 1; } for ( int i = 0; i < AKERR_TEST_THREADS; i++ ) { args[i].id = i + 1; args[i].failures = 0; args[i].barrier = &barrier; if ( pthread_create(&threads[i], NULL, body, &args[i]) != 0 ) { fprintf(stderr, "CHECK FAILED: pthread_create for thread %d" " at %s:%d\n", i + 1, __FILE__, __LINE__); failures++; break; } started++; } /* An unstarted thread never reaches the barrier, so the started ones would * wait for it forever. Nothing to do but say so before hanging is diagnosed * as a deadlock in the library. */ if ( started != AKERR_TEST_THREADS ) { fprintf(stderr, "only %d of %d threads started; the barrier will not" " release\n", started, AKERR_TEST_THREADS); } for ( int i = 0; i < started; i++ ) { pthread_join(threads[i], NULL); failures += args[i].failures; } pthread_barrier_destroy(&barrier); return failures; } #endif // AKERR_TEST_THREADS_H