Embedding and results

The contract for running registered tests from your own harness instead of the cpptb launcher, and the result structures every run produces. All in namespace cpptb. Architecture explains where this boundary sits; nothing here is needed when the cpptb command runs your tests.

Catalog and launch

registered_tests

template <typename Dut>
std::span<const detail::TestDescriptor<Dut>> registered_tests();

The compiled catalog. Each descriptor carries the registered name, the invoker, a TestMetadata (base name, case name, tags, expected-failure and skip reasons, per-test simulation timeout), and the declaration location — enough to filter by tag or case without knowing parameter types.

run_registered_test

template <typename Dut>
bool run_registered_test(coro::Testbench& scheduler, Dut dut,
                         TestResult& result, RunRequest request,
                         coro::ClockRegistrar clocks = {},
                         ResultSink* sink = nullptr);

// Environment-driven form: builds the RunRequest from CPPTB_* variables.
template <typename Dut>
bool run_registered_test(coro::Testbench& scheduler, Dut dut,
                         TestResult& result,
                         coro::ClockRegistrar clocks = {});

Selects one test and spawns it as the scheduler’s root process. The return value is selection success, not test success: after true, the harness drives the scheduler to completion and then reads result.status. Selection failures (unknown name, ambiguous empty name, bad seed) return with a recorded Selection failure. A descriptor whose options carry a skip_reason returns true without spawning anything.

RunRequest

struct RunRequest {
    std::string_view test_name;      // empty auto-selects a single-test catalog
    bool list_only = false;          // print the catalog, run nothing
    std::optional<coro::SimTime> simulation_timeout;  // overrides the descriptor's
    std::optional<uint64_t> random_seed;              // default 1
    std::string_view configuration_error;  // surface an env-parse error as Selection
    LoggingOptions logging;
};

Observing a run

ResultSink

class ResultSink {
   public:
    virtual void test_started(const TestResult&) {}
    virtual void failure_recorded(const FailureRecord&) {}
    virtual void warning_recorded(const WarningRecord&) {}
    virtual void test_finished(const TestResult&) {}
};

All callbacks default to no-ops — override what you need. Two contract points: references are valid only for the duration of the call, and a selection error fires failure_recorded + test_finished without a preceding test_started, so do not assume pairing.

Result structures

TestResult

struct TestResult {
    TestStatus status;               // NotRun … TimedOut
    uint64_t checks;  uint32_t failures;  uint32_t warnings;
    std::string test_name, case_name, status_reason;
    std::vector<std::string> tags;
    std::vector<FailureRecord> failure_records;
    std::vector<WarningRecord> warning_records;
    std::optional<WaitGraphSnapshot> wait_graph;   // captured on timeout
    std::optional<uint64_t> random_seed;
    std::string random_algorithm;
    std::string constraint_backend, constraint_backend_version;
    uint64_t random_sampling_solves, random_solver_solves;
    uint64_t simulation_time_fs;
    uint64_t wall_time_ns;           // the HARNESS fills this in
    bool finished;
};

wall_time_ns is deliberately left to the embedding harness — the framework cannot know what wall interval the harness considers the run.

TestStatus

enum class TestStatus : uint8_t { NotRun, Running, Passed, Failed, Error,
                                  Skipped, ExpectedFailure, UnexpectedPass,
                                  TimedOut };

constexpr bool test_status_successful(TestStatus status);
// true for Passed, Skipped, and ExpectedFailure — the exit-code predicate

FailureRecord

struct FailureRecord {
    FailureKind kind;      // Expectation, Requirement, Exception,
                           // Selection, Timeout, UnexpectedPass
    std::string label, actual, expected;
    std::string source_file;  uint32_t source_line;
    uint64_t simulation_time_fs;
    std::string process;  uint64_t process_id;
    std::string process_source_file;  uint32_t process_source_line;
    bool has_comparison;   // actual/expected are meaningful only when true
};

WarningRecord has the same shape without kind and the comparison fields.

The JSON contract

write_test_result_json

bool write_test_result_json(const char* path, const TestResult& result);

Writes the versioned structured result — the file a CI system consumes, schema_version 5, with earlier versions still readable by the reference runner. A null or empty path returns true and writes nothing: an unset result path is not an error. The top-level keys mirror TestResult field for field; each failure record carries its kind, label, values, both source locations, and simulation time; a timed-out run’s wait_graph serializes every parked process with what it waits on — WaitGraphSnapshot::deadlocked() and format_wait_graph(...) interpret the same data in process.

The minimal harness

coro::Testbench scheduler;
TestResult result;
RunRequest request{.test_name = "counter_sequence"};

if (run_registered_test(scheduler, dut, result, request)) {
    // drive the scheduler to completion (simulator loop)
    result.wall_time_ns = measured_wall_ns;
    write_test_result_json(result_path, result);
}
return test_status_successful(result.status) ? 0 : 1;

See also

  • Running tests — the cpptb-run executable protocol built on this API.

  • Architecture — the ownership and threading rules a harness must respect.