About the reporter architecture¶
utest uses two separate reporter objects: a worker-side emitter that serialises
events as JSON, and a coordinator-side renderer that turns those events into
human- or machine-readable output. The coordinator-side renderer is built
around a shared base class (ReporterBase) that accumulates statistics and
calls optional render_* hooks on the concrete reporter for each event type.
Understanding why the two sides are split, and how the hook model works, makes
the reporter interface easier to reason about when adding a new output format.
Why two reporters?¶
Workers run in separate subprocesses. A worker has no direct access to the coordinator's output stream, and the coordinator has no direct access to a worker's in-process state. The only communication channel between them is stdout.
The worker-side reporter (src/utest/runner/worker/reporter.uc) therefore has a
single job: serialise every significant event — SUITE_START, TEST_RESULT,
SUITE_END, FATAL — as a JSON line to stdout. It knows nothing about formatting,
colour, or output destination. It is not extensible and not intended to be.
The coordinator-side reporter (src/utest/runner/reporter/) has the opposite
job: receive those events, accumulate statistics, and render them. This is the
part that users can extend.
The split means that all output formatting is centralised in one process and one module hierarchy, regardless of how many workers ran in parallel or how their output was interleaved.
graph LR
W1["Worker 1\nreporter.uc"] -->|"JSON lines\nstdout"| EX["Executor\n(coordinator)"]
W2["Worker 2\nreporter.uc"] -->|"JSON lines\nstdout"| EX
WN["Worker N\n…"] -->|"JSON lines\nstdout"| EX
EX --> BASE["ReporterBase\nstats · failures · timing"]
BASE --> DET["detailed.uc\nrender_* hooks"]
BASE --> COM["compact.uc\nrender_* hooks"]
BASE --> JSON["json.uc\nrender_* hooks"]
BASE --> CUST["custom reporter\nrender_* hooks"]
The coordinator-side hook model¶
The coordinator reporter is built around ReporterBase
(src/utest/runner/reporter/base.uc). ReporterBase handles everything that is
the same for every reporter: counting passes, failures, errors, and skips;
recording which tests failed; tracking wall-clock time; and holding the random
seed for the summary line.
A concrete reporter extends ReporterBase via prototype and overrides only the
render_* hooks it cares about. ReporterBase calls a hook only when the
concrete reporter defines it, so an unimplemented hook is simply skipped — there
is no need to provide a no-op stub.
This design has two consequences:
- A minimal reporter that only shows the summary can implement a single
render_summaryhook and ignore everything else. - Adding a new event type to the coordinator in the future only requires adding a new optional hook; existing reporters need no changes.
The available hooks, and the data each receives, are documented in Reporter API reference.
Why ReporterBase accumulates stats rather than the concrete reporter¶
Stats accumulation involves edge cases — double-counting suites, handling FATAL
events that produce no TEST_RESULT lines, distinguishing errors from assertion
failures — that every reporter would have to solve identically. Putting that
logic in ReporterBase means a concrete reporter can read ctx.stats at summary
time and trust the numbers without reimplementing the accounting.
It also means that the render_* hooks receive pre-processed arguments (e.g., a
stats object) rather than raw JSON payloads. Concrete reporters work at the
level of what to display, not how to parse events.
See also¶
- Hook signatures, status values, and stats fields: Reporter API reference
- How to add a new output format: How-to: Add a reporter
- How workers and the coordinator communicate: About the worker/coordinator architecture