TestContext and checks¶
The API a test body receives, and the macros that register one. All in
namespace cpptb. A test is a coroutine of shape
Task<void>(Dut dut, TestContext& test);
Test lifecycle covers the model, this page the
surface.
TestContext is a copyable, reference-counted handle over the run’s
shared state. Pass it by value into spawned processes that may outlive
the spawning scope, by reference into ordinary sequential helpers.
Registration¶
CPPTB_REGISTER_TEST¶
CPPTB_REGISTER_TEST_WITH_OPTIONS¶
CPPTB_REGISTER_TEST_CASE¶
CPPTB_REGISTER_TEST_CASE_WITH_OPTIONS¶
CPPTB_REGISTER_TEST(counter_sequence);
CPPTB_REGISTER_TEST_WITH_OPTIONS(
known_issue,
(::cpptb::TestOptions{
.tags = {"nightly"},
.expected_failure = true,
.expected_failure_reason = "tracked as RTL-241",
.simulation_timeout = 20_us,
}));
CPPTB_REGISTER_TEST_CASE(counter_case, empty, (CounterCase{0, 0}));
Place at file scope in the testbench translation unit. The registered name
is the function identifier; a case registers as function[case] and its
body takes the case value as a third parameter. Wrap braced arguments in
parentheses so their commas survive the macro. Plain functions only — the
registration slot is a function pointer, so a capturing lambda cannot be
registered directly.
TestOptions¶
struct TestOptions {
std::vector<std::string_view> tags;
bool expected_failure = false;
std::string_view expected_failure_reason;
std::string_view skip_reason;
std::optional<coro::SimTime> simulation_timeout;
};
skip_reason skips the test at selection without spawning it.
expected_failure inverts the verdict: a failing run reports
ExpectedFailure (which counts as successful) and a clean run reports
UnexpectedPass. simulation_timeout is the in-simulation deadline for
this test; there is no macro or environment variable for it.
Checks¶
expect¶
expect_eq¶
require¶
require_eq¶
void expect(std::string_view label, bool condition) const;
void expect_eq(std::string_view label, const Actual& actual,
const Expected& expected) const;
void require(std::string_view label, bool condition) const;
void require_eq(std::string_view label, const Actual& actual,
const Expected& expected) const;
The complete check family — four calls, label first and mandatory.
expect* records a failure and continues, so one run reports every
mismatch. require* records and ends the test at once by unwinding to
the framework — which is why a catch (...) must never wrap a check.
Every check, passing or failing, counts toward the result’s checks.
Each failure records the label, the call site, both formatted values (for
_eq), the simulation time, and the spawning process’s identity — the
same fields the structured JSON carries. Value formatting runs only on
failure, and custom types render by specializing
cpptb::DiagnosticFormatter<T>.
warn¶
void warn(std::string_view message) const;
Records a warning with the same location detail as a failure; never changes the verdict.
skip¶
[[noreturn]] void skip(std::string_view reason) const;
Ends the test immediately with status Skipped, which counts as
successful. For skipping at registration instead, use
TestOptions::skip_reason.
Processes and clocks¶
spawn¶
spawn_detached¶
coro::Process spawn(coro::Task<void> task) const;
void spawn_detached(coro::Task<void> task) const;
Start a coroutine as a concurrent, test-owned process. Ownership means
cleanup is automatic: on any terminal outcome — return, fatal check,
timeout, escaping exception — remaining owned processes are cancelled, and
a failure inside one is attributed to its spawn site. spawn_detached is
the same ownership without a handle. Spawning after the test has already
finished is an error that cannot leak a process.
start_clock¶
void start_clock(Signal signal, coro::SimTime period,
coro::SimTime phase = {}) const;
Register a periodic clock on a writable one-bit port; returns without
suspending. Call before the test’s first co_await, once per clock; the
first registered clock supplies the result’s cycle count. The period must
be even and, like the phase, a whole multiple of the simulator precision.
Initialize the pin with set_now(0) first — a plain set() queues under
the write model. DUT-produced clocks are never started, only awaited.
See Clocking.
now¶
coro::SimTime now() const;
Current simulation time.
Randomization¶
random¶
Random& random() const;
The calling process’s deterministic stream, derived from the master seed and the process’s stable ID — so concurrent processes never steal each other’s draws and any process replays independently.
uint64_t next_u64();
Value randint(Value minimum, Value maximum); // inclusive; throws if min > max
Bits<W> randbits<W>();
choice(range); weighted_choice(range); shuffle(range);
uint64_t seed() const; void reseed(uint64_t seed);
The master seed defaults to 1 and is set per run with CPPTB_RANDOM_SEED
(decimal or 0x-hex). See
Reproducibility.
randomize¶
randomize_with¶
void randomize(Randomized& item) const;
void randomize_with(Randomized& item, Constraint constraint) const;
Solve a constrained transaction in place; a failed solve is fatal to the test. See Constrained transactions.
Logging¶
logger¶
Logger logger(std::string_view scope = {}) const;
A scoped structured logger: per-level calls (trace through error),
each with an eager string overload and a lazy factory overload that skips
message construction when the level is disabled. Run-time level comes from
CPPTB_LOG_LEVEL. See Logging.
Environment variables¶
The complete framework-level set read by a built simulator process:
Variable |
Effect |
|---|---|
|
Exact test to run; empty auto-selects only when a single test is registered |
|
List the catalog (one |
|
Master 64-bit seed, decimal or |
|
|
|
Path for the structured JSON result |
|
Waveform dump path in a |
An unparsable seed or level does not abort — it surfaces as a selection error in the structured result.
See also¶
Tasks and coordination — the
Processhandle’s methods and the primitives spawned processes coordinate with.Embedding and results — running registered tests from your own harness, and the JSON these checks feed.