1919#include < stdexcept>
2020#include < string>
2121#include < string_view>
22+ #include < thread>
2223#include < unordered_map>
2324#include < utility>
2425#include < vector>
@@ -53,41 +54,35 @@ struct Registrar {
5354 }
5455};
5556
56- /* * assertion counts for one test case. */
57+ /* * Assertion counts for one test case. */
5758struct TestStats {
5859 int checks = 0 ;
5960 int failed_checks = 0 ;
6061};
6162
62- /* * state owned by one running test case.
63- * joined worker threads may use CHECK while the parent test is active. */
63+ /* * State owned by one running test case.
64+ *
65+ * The runner is sequential: only one test case is active in a process.
66+ * Non-aborting checks may be used from child threads that are joined before the
67+ * test returns.
68+ */
6469struct TestCaseContext {
6570 explicit TestCaseContext (std::string name) : full_name{std::move (name)} {}
6671
6772 const std::string full_name;
68- /* * keep the test-thread path plain, worker threads pay the atomic cost. */
69- TestStats stats;
70- std::atomic<int > worker_checks{0 };
71- std::atomic<int > worker_failed_checks{0 };
73+ const std::thread::id owner_thread{std::this_thread::get_id ()};
74+ std::atomic<int > checks{0 };
75+ std::atomic<int > failed_checks{0 };
7276};
7377
74- /* * active context for the sequential runner.
75- * parallel in-process tests need explicit context propagation. until then, fail closed. */
7678inline std::atomic<TestCaseContext*>& active_test_context_storage ()
7779{
7880 static std::atomic<TestCaseContext*> context{nullptr };
7981 return context;
8082}
8183
82- inline TestCaseContext*& local_test_context ()
83- {
84- thread_local TestCaseContext* context{nullptr };
85- return context;
86- }
87-
8884inline TestCaseContext& active_test_context ()
8985{
90- if (auto * context{local_test_context ()}) return *context;
9186 if (auto * context{active_test_context_storage ().load (std::memory_order_acquire)}) return *context;
9287 throw std::logic_error{" test framework assertion used outside an active test case" };
9388}
@@ -100,12 +95,10 @@ class ScopedTestCaseContext {
10095 if (!active_test_context_storage ().compare_exchange_strong (expected, &context, std::memory_order_release, std::memory_order_relaxed)) {
10196 throw std::logic_error{" test framework supports only one active test case per process" };
10297 }
103- local_test_context () = &context;
10498 }
10599
106- ~ScopedTestCaseContext ()
100+ ~ScopedTestCaseContext () noexcept
107101 {
108- local_test_context () = nullptr ;
109102 active_test_context_storage ().store (nullptr , std::memory_order_release);
110103 }
111104
@@ -122,8 +115,8 @@ inline std::string current_test_full_name()
122115inline TestStats snapshot_stats (const TestCaseContext& context)
123116{
124117 return TestStats{
125- context.stats . checks + context. worker_checks .load (std::memory_order_relaxed),
126- context.stats . failed_checks + context. worker_failed_checks .load (std::memory_order_relaxed),
118+ context.checks .load (std::memory_order_relaxed),
119+ context.failed_checks .load (std::memory_order_relaxed),
127120 };
128121}
129122
@@ -287,21 +280,10 @@ inline std::optional<LogLevel> to_log_level(std::string_view s)
287280inline void record_check (const Result& result, const char * kind, const char * expr,
288281 const char * file, int line, const std::string& message = {})
289282{
290- TestCaseContext* context{local_test_context ()};
291- const bool worker_thread{context == nullptr };
292- if (worker_thread) context = &active_test_context ();
293-
294- if (worker_thread) {
295- context->worker_checks .fetch_add (1 , std::memory_order_relaxed);
296- } else {
297- ++context->stats .checks ;
298- }
283+ auto & context = active_test_context ();
284+ context.checks .fetch_add (1 , std::memory_order_relaxed);
299285 if (!result.is_ok ()) {
300- if (worker_thread) {
301- context->worker_failed_checks .fetch_add (1 , std::memory_order_relaxed);
302- } else {
303- ++context->stats .failed_checks ;
304- }
286+ context.failed_checks .fetch_add (1 , std::memory_order_relaxed);
305287 if (current_log_level () >= LogLevel::Error) {
306288 std::scoped_lock lock{log_mutex ()};
307289 log_unlocked (" [FAIL]: %s:%d: %s(%s)\n " , file, line, kind, expr);
@@ -313,6 +295,15 @@ inline void record_check(const Result& result, const char* kind, const char* exp
313295 log (LogLevel::All, " [PASS]: %s:%d: %s(%s)\n " , file, line, kind, expr);
314296}
315297
298+ inline bool aborting_check_allowed (const char * kind, const char * file, int line)
299+ {
300+ const auto & context = active_test_context ();
301+ if (context.owner_thread == std::this_thread::get_id ()) return true ;
302+ record_check (Result::failed (std::string{kind} + " used from a child thread; use non-aborting checks in child threads" ),
303+ kind, " test-thread-only assertion" , file, line);
304+ return false ;
305+ }
306+
316307template <std::ranges::range R1 , std::ranges::range R2 >
317308 requires std::same_as<std::ranges::range_value_t <R1 >, std::ranges::range_value_t <R2 >>
318309Result check_equal_ranges (const R1 & r1, const R2 & r2)
@@ -506,22 +497,25 @@ inline int run(int argc, char** argv)
506497 continue ;
507498 }
508499 TestCaseContext context{test_name (test_case)};
500+ bool failed_by_exception{false };
509501 try {
510502 ScopedTestCaseContext active_context{context};
511503 test_case.fn ();
512504 } catch (const RequireFailed&) {
513505 } catch (const std::exception& e) {
514- ++summary. failed ;
506+ failed_by_exception = true ;
515507 log (LogLevel::Error, " EXCEPTION in %s: %s\n " , test_case.name , e.what ());
516- continue ;
517508 } catch (...) {
518- ++summary. failed ;
509+ failed_by_exception = true ;
519510 log (LogLevel::Error, " EXCEPTION in %s: %s\n " , test_case.name , " unknown exception" );
520- continue ;
521511 }
522512 const auto stats = snapshot_stats (context);
523513 summary.total_checks += stats.checks ;
524- if (stats.failed_checks == 0 ) {
514+ if (failed_by_exception) {
515+ ++summary.failed ;
516+ log (LogLevel::Error, " [FAIL] %s (exception after %d checks, %d checks failed)\n " ,
517+ test_case.name , stats.checks , stats.failed_checks );
518+ } else if (stats.failed_checks == 0 ) {
525519 ++summary.passed ;
526520 log (LogLevel::Info, " [ OK ] %s (%d checks)\n " , test_case.name , stats.checks );
527521 } else {
@@ -634,13 +628,16 @@ inline int run(int argc, char** argv)
634628 btc_test_res_.is_ok() ? std::string{} : btc_test_os_.str ()); \
635629 } while (false )
636630
637- /* * Like CHECK, but aborts the current test on failure by throwing.
638- * Subsequent checks in the test are skipped.
639- * keep this on the test thread, use CHECK from worker threads .
631+ /* * Like CHECK, but aborts the current test's control flow on failure by throwing.
632+ * Subsequent checks in that control flow are skipped. Use CHECK from child
633+ * threads, because throwing in a child thread cannot abort the parent test .
640634 *
641635 * Accepts the same optional stream-style failure message as CHECK. */
642636#define REQUIRE (expr, ...) \
643637 do { \
638+ if (!::framework::aborting_check_allowed (" REQUIRE" , __FILE__, __LINE__)) { \
639+ break ; \
640+ } \
644641 BITCOIN_TEST_DIAG_PUSH \
645642 ::framework::Result btc_test_res_ = ::framework::Decomposer{} <= expr; \
646643 BITCOIN_TEST_DIAG_POP \
@@ -678,9 +675,13 @@ inline int run(int argc, char** argv)
678675 #expr, __FILE__, __LINE__); \
679676 } while (false )
680677
681- /* * Like CHECK_NOTHROW, but aborts the current test on failure. */
678+ /* * Like CHECK_NOTHROW, but aborts the current test's control flow on failure. */
682679#define REQUIRE_NOTHROW (expr ) \
683680 do { \
681+ if (!::framework::aborting_check_allowed (" REQUIRE_NOTHROW" , __FILE__, \
682+ __LINE__)) { \
683+ break ; \
684+ } \
684685 ::framework::Result btc_test_res_ = ::framework::Result::ok (); \
685686 try { \
686687 expr; \
0 commit comments