TTS has an extensible tts::output_sink customization point (see Output Utilities and the tts::gathering_sink example) - everything a test suite prints goes through whichever sink is currently installed. Beyond writing your own, TTS ships a small set of ready-to-use sinks under include/tts/sinks/, already available through #include <tts/tts.hpp> with no extra include needed.
All five draw from tts::output_sink's structured hooks (test_started(), assertion_failed(), test_finished(), suite_finished(), suite_metric(), suite_aborted()) rather than by parsing the text tts::stdout_sink prints, so they stay correct regardless of -v/-q. None of them currently reflect TTS_CASE_TPL's per-type breakdown or TTS_WHEN / TTS_AND_THEN sub-scenarios, since those don't have their own hook.
Every "Example" below runs this same sample suite - one passing case, one invalid (empty) case, and one failing case:
Each of these produces plain, line-oriented text - as opposed to a structured-format sink like tts::json_sink or tts::junit_sink below, which each assemble a single well-formed document with its own schema instead of just lines of text.
Wraps a target sink (tts::output_handler::default_sink() by default), coloring pass/fail/ invalid lines and the Results: ... summary with ANSI escapes - forwarding everything else unchanged. Opt-in: not every terminal or CI log renders ANSI escapes usefully, and on Windows it additionally depends on the host console having Virtual Terminal Processing enabled (most modern terminals already do).
./my_test --sink=colored
Running the sample suite above then looks like this:
TEST: 'Check that expectation can be met' TEST: 'Check that expectation can be met' - [PASSED] TEST: 'Check invalid detection' [!!]: EMPTY TEST CASE TEST: 'Check that expectation fails' [X] [expect.cpp:15] : ** FAILURE ** : Expression: 1 == 2 evaluates to false. -------------------------------------------------------------------------------- Results: 3 tests - 1/3 (33.33%) success - 1/3 (33.33%) failure - 1/3 (33.33%) invalid
TAP (Test Anything Protocol) is a simple, language-agnostic text format for reporting test results, understood by many CI dashboards and test harnesses. tts::tap_sink listens to test_finished() and accumulates one ok N - name / not ok N - name line per TTS_CASE, then dump() streams them out preceded by a leading 1..N plan line.
./my_test --sink=tap - dump() then happens automatically once the run finishes.
Running the sample suite above, tap.dump() prints - note that TAP has no third state, so the invalid case comes out indistinguishable from a genuine failure:
Wraps a target sink, forwarding every message unchanged, and additionally prints one path:line: error: message / path:line: fatal error: message line per failing/fatal assertion, so editors/IDEs with a GCC/Clang-style problem matcher (VS Code's C/C++ extension provides $gcc, vim has quickfix, ...) can jump straight to it. assertion_failed() fires before its corresponding text, so the diagnostic line prints first; it still fires under -q, when that raw line is itself suppressed.
./my_test --sink=diagnostics
Running the sample suite above, the failing case prints the diagnostic line, immediately followed by its usual one - the passing and invalid cases are unaffected, since neither triggers assertion_failed():
Unlike the simple sinks above, each of these assembles its output into a single well-formed document with its own schema instead of printing independent lines - so they only ever produce output once the whole run has finished, via dump() or finish(), never incrementally.
Accumulates one JSON object per TTS_CASE from test_finished() and assertion_failed(), plus a summary counting passed/failed/invalid cases - counted directly as cases finish, not from the suite's raw assertion totals, so the number of entries in tests always matches summary.total. No JSON library involved: escaping is hand-rolled in tts::_::json_escape(), in keeping with Compile-Time Discipline.
The document is a single object with two fields:
| Field | Type | Description |
|---|---|---|
| tests | array of object | One entry per TTS_CASE that finished running, in run order. |
| summary | object | Aggregate counts over every entry in tests. |
Each entry in tests:
| Field | Type | Description |
|---|---|---|
| name | string | The case's ID, exactly as given to TTS_CASE. |
| status | string | One of "passed", "failed", "invalid" (registered no assertion at all). |
| duration_ns | integer | Wall-clock time the case took to run, in nanoseconds. |
| failures | array of object | One entry per failing/fatal assertion. Empty unless status is "failed". |
Each entry in a failures array:
| Field | Type | Description |
|---|---|---|
| location | object | Where the assertion is, split into file/line (see below). |
| message | string | The assertion's failure message. |
| fatal | boolean | true if this came from TTS_FATAL (which then aborts the whole suite - see summary note below). |
location:
| Field | Type | Description |
|---|---|---|
| file | string | Source file name, as reported in the human-readable output (basename only). |
| line | integer | Line number within file. |
summary:
| Field | Type | Description |
|---|---|---|
| total | integer | Number of entries in tests (passed + failed + invalid). |
| passed | integer | Number of cases with status: "passed". |
| failed | integer | Number of cases with status: "failed". |
| invalid | integer | Number of cases with status: "invalid". |
| duration_ns | integer | Sum of every case's duration_ns. |
./my_test --sink=json
Running the sample suite above via --sink=json produces (pretty-printed here for readability):
Renders the run as JUnit XML, the de facto standard test-report format consumed by Jenkins, GitLab CI, CircleCI, Azure DevOps, Bitbucket Pipelines and most CI dashboards - including GitHub Actions itself via a separate report-parsing action (e.g. dorny/test-reporter, mikepenz/action-junit-report), which is the standard way JUnit XML turns into inline PR annotations. Every TTS_CASE becomes one <testcase> inside a single <testsuite>, counted from test_finished() directly rather than the suite's raw assertion totals, exactly like tts::json_sink.
<testsuites> wraps a single <testsuite>:
| Attribute | Description |
|---|---|
| name | Fixed at "TTS" - TTS only ever reports one flat suite. |
| tests | Total number of <testcase> entries (passed + failed + skipped). |
| failures | Number of <testcase> entries with a <failure> child. |
| errors | Always 0 - see the TTS_FATAL note below. |
| skipped | Number of <testcase> entries with a <skipped/> child (invalid cases). |
| time | Sum of every case's elapsed time, in seconds. |
Each <testcase>:
| Attribute/child | Description |
|---|---|
| name | The case's ID, exactly as given to TTS_CASE. |
| classname | Duplicates name - TTS has no class-like grouping to report instead. |
| time | Elapsed time for this case, in seconds. |
| <skipped/> | Present, empty, if the case registered no assertion at all. |
| <failure> | Present if at least one assertion failed - message attribute is the first failure's message, text content is every failing assertion's path:line: message, one per line. |
Neither <skipped/> nor <failure> is present for a passed case - just a self-closing <testcase .../>.
./my_test --sink=junit
Running the sample suite above via --sink=junit produces (pretty-printed here for readability):