TTS v3.0.0
The Tiny Test System
Loading...
Searching...
No Matches
Built-in Output Sinks

Overview

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:

TTS_CASE("Check that expectation can be met")
{
TTS_EXPECT(1 == 1);
};
TTS_CASE("Check invalid detection")
{
};
TTS_CASE("Check that expectation fails")
{
TTS_EXPECT(1 == 2);
};
#define TTS_EXPECT(EXPR,...)
Check if a given expression evaluates to true.
Definition basic.hpp:43
#define TTS_CASE(ID)
Introduces a new test scenario and registers it into the current test driver.
Definition case.hpp:187
Note
On a plain TTS_MAIN binary (no custom driver), the --sink=name CLI flag installs any of the five below without writing any C++ at all - --sink=colored, --sink=tap, --sink=diagnostics, --sink=json, or --sink=junit. It composes with --capture=path: --sink=tap --capture=report.tap writes TAP-formatted output to the file instead of stdout. Like --shard, it's a no-op for binaries using a TTS_CUSTOM_DRIVER_FUNCTION, which already manages its own sink - see Command Line Interface for the full flag reference.

Simple Format Sinks

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.

tts::colorized_sink

Effect

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).

Manual Usage

tts::output().sink(colorized);
// ... run the test suite ...
void sink(output_sink &s)
Installs s as the output_sink every subsequent write goes to.
Definition output.hpp:379
output_handler & output()
Retrieves the current TTS output_handler.
Definition output.hpp:414
output_sink wrapping another sink, colorizing pass/fail/fatal lines with ANSI escapes.
Definition colorized.hpp:33

CLI Flag

./my_test --sink=colored

Example

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 

tts::tap_sink

Effect

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.

Manual Usage

// ... run the test suite ...
tap.dump(); // stream the TAP-formatted report to stdout
output_sink rendering the run as TAP (Test Anything Protocol).
Definition tap.hpp:42
void dump(output_sink &target)
Forwards the TAP-rendered report to target, then clears the gathered results.
Definition tap.hpp:70

CLI Flag

./my_test --sink=tap - dump() then happens automatically once the run finishes.

Example

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:

1..3
ok 1 - Check that expectation can be met
not ok 2 - Check invalid detection
not ok 3 - Check that expectation fails

tts::diagnostics_sink

Effect

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.

Manual Usage

tts::output().sink(diagnostics);
// ... run the test suite ...
output_sink adding compiler-style diagnostics for every failing/fatal assertion.
Definition diagnostics.hpp:37

CLI Flag

./my_test --sink=diagnostics

Example

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():

expect.cpp:15: error: Expression: 1 == 2 evaluates to false.
[X] [expect.cpp:15] : ** FAILURE ** : Expression: 1 == 2 evaluates to false.

Structured Format Sinks

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.

tts::json_sink

Effect

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.

Schema

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.
Note
A TTS_FATAL assertion aborts the whole suite immediately - the case it happened in never finishes running, so it never reaches test_finished() and does not appear in tests at all, not even as a "failed" entry. summary.total then undercounts the suite's actual registered case count. This mirrors how tts::tap_sink and the plain text output behave for the same reason: the driver loop's per-case bookkeeping only runs for a case that returns normally.

Manual Usage

tts::output().sink(json);
// ... run the test suite ...
json.dump(); // stream the JSON report to stdout
output_sink rendering the run as a single structured JSON document.
Definition json.hpp:73
void dump(output_sink &target)
Forwards the JSON-rendered report to target, then clears the gathered results.
Definition json.hpp:150

CLI Flag

./my_test --sink=json

Example

Running the sample suite above via --sink=json produces (pretty-printed here for readability):

{
"tests": [
{
"name": "Check that expectation can be met",
"status": "passed",
"duration_ns": 917,
"failures": []
},
{
"name": "Check invalid detection",
"status": "invalid",
"duration_ns": 68,
"failures": []
},
{
"name": "Check that expectation fails",
"status": "failed",
"duration_ns": 11030,
"failures": [
{
"location": {
"file": "expect.cpp",
"line": 15
},
"message": "Expression: 1 == 2 evaluates to false.",
"fatal": false
}
]
}
],
"summary": {
"total": 3,
"passed": 1,
"failed": 1,
"invalid": 1,
"duration_ns": 12015
}
}

tts::junit_sink

Effect

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.

Schema

<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 .../>.

Note
Exactly like tts::json_sink's tests array, a TTS_FATAL assertion aborts the whole suite before the case it happened in ever reaches test_finished(), so that case never becomes a <testcase> at all - not even one with a <failure> or an <error> child. This is also why errors is always 0: TTS has no way to represent "aborted mid-case" as a finished testcase, so there's nothing to ever count there.

Manual Usage

tts::output().sink(junit);
// ... run the test suite ...
junit.dump(); // stream the JUnit XML report to stdout
output_sink rendering the run as JUnit XML, the de facto standard consumed by Jenkins,...
Definition junit.hpp:70
void dump(output_sink &target)
Forwards the JUnit-rendered report to target, then clears the gathered results.
Definition junit.hpp:159

CLI Flag

./my_test --sink=junit

Example

Running the sample suite above via --sink=junit produces (pretty-printed here for readability):

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="TTS" tests="3" failures="1" errors="0" skipped="1" time="0.000012">
<testcase name="Check that expectation can be met"
classname="Check that expectation can be met" time="0.000001"/>
<testcase name="Check invalid detection" classname="Check invalid detection"
time="0.000000">
<skipped/>
</testcase>
<testcase name="Check that expectation fails" classname="Check that expectation fails"
time="0.000011">
<failure message="Expression: 1 == 2 evaluates to false.">
expect.cpp:15: Expression: 1 == 2 evaluates to false.
</failure>
</testcase>
</testsuite>
</testsuites>