Skip to content

Commit c4fc39a

Browse files
committed
test: refine per-test framework context
Keep per-test mutable runner state in TestCaseContext while preserving the sequential runner model. Use atomic check counters so joined child-thread CHECK calls are recorded, restrict aborting REQUIRE-style assertions to the owning test thread, and preserve check accounting when a test exits through an unexpected exception. Update stale runner documentation for suite/case filters and --list_content.
1 parent a665539 commit c4fc39a

3 files changed

Lines changed: 49 additions & 48 deletions

File tree

src/test/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ and tests weren't explicitly disabled.
2323
The unit tests can be run with `ctest --test-dir build`, which includes unit
2424
tests from subtrees.
2525

26-
Run `build/bin/test_bitcoin --list` for the full list of tests.
26+
Run `build/bin/test_bitcoin --list_content` for the full list of tests.
2727

2828
To run the unit tests manually, launch `build/bin/test_bitcoin`. To recompile
2929
after a test file was modified, run `cmake --build build` and then run the test again. If you
@@ -57,7 +57,7 @@ build/bin/test_bitcoin -l all -t getarg_tests
5757
or to run only the `doubledash` test in `getarg_tests`
5858

5959
```bash
60-
build/bin/test_bitcoin --run_test=getarg_tests::doubledash
60+
build/bin/test_bitcoin --run_test=getarg_tests/doubledash
6161
```
6262

6363
The `--log_level=` (or `-l`) argument controls the verbosity of the test output.

src/test/main.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
/**
1515
* Retrieve the user-supplied command line arguments — everything after the
1616
* `--` separator on the command line. Allows usage like:
17-
* `test_bitcoin --run_test="net_tests::cnode_listen_port" -- -checkaddrman=1 -printtoconsole=1`
17+
* `test_bitcoin --run_test="net_tests/cnode_listen_port" -- -checkaddrman=1 -printtoconsole=1`
1818
* which would return `["-checkaddrman=1", "-printtoconsole=1"]`.
1919
*/
2020
const std::function<std::vector<const char*>()> G_TEST_COMMAND_LINE_ARGUMENTS = []() {
@@ -23,7 +23,7 @@ const std::function<std::vector<const char*>()> G_TEST_COMMAND_LINE_ARGUMENTS =
2323
};
2424

2525
/**
26-
* Retrieve the full name (`suite::case`) of the currently-running test.
26+
* Retrieve the full name (`suite/case`) of the currently-running test.
2727
*/
2828
const std::function<std::string()> G_TEST_GET_FULL_NAME = []() {
2929
return framework::current_test_full_name();

src/test/util/framework.hpp

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
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. */
5758
struct 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+
*/
6469
struct 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. */
7678
inline 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-
8884
inline 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()
122115
inline 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)
287280
inline 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+
316307
template <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>>
318309
Result 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

Comments
 (0)