GCC Code Coverage Report


Directory: include/tts/
File: include/tts/tools/output.hpp
Date: 2026-10-14 16:28:14
Exec Total Coverage
Lines: 116 116 100.0%
Functions: 54 57 94.7%
Branches: 11 14 78.6%

Line Branch Exec Source
1 //======================================================================================================================
2 //! @file
3 /*
4 TTS - Tiny Test System
5 Copyright : TTS Contributors & Maintainers
6 SPDX-License-Identifier: BSL-1.0
7 */
8 //======================================================================================================================
9 #pragma once
10
11 #include <tts/tools/text.hpp>
12 #include <tts/tools/units.hpp>
13
14 namespace tts::_
15 {
16 //====================================================================================================================
17 // Current test suite verbosity settings, set from the -v/--verbose and -q/--quiet CLI flags.
18 // This is the single source of truth for what should be reported and is kept independent from
19 // how a message actually gets emitted, which is the job of tts::output_handler below.
20 //====================================================================================================================
21 struct verbosity
22 {
23 bool verbose = false;
24 bool quiet = false;
25 };
26
27 inline verbosity current_verbosity = {}; // NOSONAR
28
29 //====================================================================================================================
30 // Sole writers for current_verbosity: every read goes through is_verbose()/is_quiet()/
31 // is_detailed(), so every write is funneled through these instead of poking the fields directly.
32 //====================================================================================================================
33 115 inline void set_verbose(bool verbose)
34 {
35 115 current_verbosity.verbose = verbose;
36 115 }
37
38 112 inline void set_quiet(bool quiet)
39 {
40 112 current_verbosity.quiet = quiet;
41 112 }
42
43 //====================================================================================================================
44 // RAII guard saving current_verbosity on construction and restoring it on destruction. Meant for
45 // tests that need to exercise a specific verbose/quiet combination without leaking that state
46 // into the rest of the running test suite, even if the test exits early.
47 //====================================================================================================================
48 struct scoped_verbosity
49 {
50 7 scoped_verbosity() = default;
51
52 7 ~scoped_verbosity()
53 {
54 7 current_verbosity = saved;
55 7 }
56
57 scoped_verbosity(scoped_verbosity const&) = delete;
58 scoped_verbosity& operator=(scoped_verbosity const&) = delete;
59 scoped_verbosity(scoped_verbosity&&) = delete;
60 scoped_verbosity& operator=(scoped_verbosity&&) = delete;
61
62 verbosity saved = current_verbosity;
63 };
64 }
65
66 namespace tts
67 {
68 //====================================================================================================================
69 /**
70 @defgroup tools-output Output Utilities
71 @{
72 **/
73 //====================================================================================================================
74
75 //====================================================================================================================
76 /**
77 @public
78 @brief The three ways a @ref TTS_CASE outcome is tallied in the `Results: ...` summary.
79 @see output_sink::suite_metric
80 **/
81 //====================================================================================================================
82 enum class outcome
83 {
84 success,
85 failure,
86 invalid
87 };
88
89 //====================================================================================================================
90 /**
91 @public
92 @brief Customization point for where TTS output actually goes.
93
94 All non-usage related output produced while running a test suite - tests progress, pass/fail/
95 fatal reports, additional logging information and the final results report - is written
96 through the tts::output_handler currently in use, which itself forwards every message to an
97 output_sink. Deriving from output_sink and overriding write() lets power users redirect that
98 output wherever they need to: a file, a GUI widget, a socket, another logging system...
99
100 @groupheader{Example}
101 @snippet doc/custom_output_sink.cpp snippet
102
103 @see tts::output_handler
104 @see tts::stdout_sink
105 @see tts::gathering_sink
106 **/
107 //====================================================================================================================
108 struct output_sink
109 {
110 /// Writes t to this sink's destination.
111 virtual void write(text const& t) = 0;
112
113 /// Flushes this sink's destination, if that means anything for it. No-op by default.
114 32 virtual void flush()
115 {
116 // Intentionally empty: most sinks (e.g. gathering_sink) have nothing meaningful to flush.
117 32 }
118
119 /// Called when a @ref TTS_CASE / @ref TTS_CASE_TPL / @ref TTS_CASE_WITH is about to run.
120 /// No-op by default - opt-in for sinks that want structured per-test events instead of (or
121 /// in addition to) parsing the formatted text stream.
122 277 virtual void test_started([[maybe_unused]] text const& name)
123 {
124 // Intentionally empty: most sinks only care about the formatted text stream.
125 277 }
126
127 /// Called once per failing/fatal assertion, always and before the corresponding text (if
128 /// any - it's suppressed under -q for a plain failure, though never for a fatal one). No-op
129 /// by default. Not called for passing assertions - test_finished()'s `passed` already covers
130 /// that case, and most consumers of this hook don't want one event per assertion.
131 54 virtual void assertion_failed([[maybe_unused]] text const& location,
132 [[maybe_unused]] text const& message,
133 [[maybe_unused]] bool fatal)
134 {
135 // Intentionally empty: most sinks only care about the formatted text stream.
136 54 }
137
138 /// Called once a @ref TTS_CASE / @ref TTS_CASE_TPL / @ref TTS_CASE_WITH has finished
139 /// running, always and before its corresponding text (if any - suppressed under -q), with
140 /// its outcome and how long it took to run, in nanoseconds. No-op by default.
141 269 virtual void test_finished([[maybe_unused]] text const& name,
142 [[maybe_unused]] bool passed,
143 [[maybe_unused]] bool invalid,
144 [[maybe_unused]] nanoseconds duration_ns)
145 {
146 // Intentionally empty: most sinks only care about the formatted text stream.
147 269 }
148
149 /// Called once the whole suite has finished running (from tts::report()), with its aggregate
150 /// outcome, always - even under -q. No-op by default.
151 97 virtual void suite_finished([[maybe_unused]] counter fail_count,
152 [[maybe_unused]] counter invalid_count)
153 {
154 // Intentionally empty: most sinks only care about the formatted text stream.
155 97 }
156
157 /// Called once per non-zero outcome category, right before its "N/M (P%) <label>" segment is
158 /// printed as part of the `Results: ...` summary. No-op by default.
159 111 virtual void suite_metric([[maybe_unused]] outcome kind,
160 [[maybe_unused]] counter count,
161 [[maybe_unused]] counter total)
162 {
163 // Intentionally empty: most sinks only care about the formatted text stream.
164 111 }
165
166 /// Called if the suite aborts early due to a @ref TTS_FATAL failure, before tts::report()
167 /// (and thus suite_finished()) runs. No-op by default.
168 1 virtual void suite_aborted()
169 {
170 // Intentionally empty: most sinks only care about the formatted text stream.
171 1 }
172
173 /// Called once, after tts::report() returns - every other hook has already fired by then - so
174 /// an accumulate-style sink (e.g. tts::tap_sink) can render and flush its buffered content to
175 /// its own destination. No-op by default: pass-through sinks that already forward write() as
176 /// it happens (e.g. tts::colorized_sink) have nothing left to do here. Like `--shard`, this is
177 /// only called by the default driver (@ref TTS_MAIN) - a @ref TTS_CUSTOM_DRIVER_FUNCTION
178 /// manages whichever sink it installed itself, including dumping it, so its driver never calls
179 /// finish() on its own.
180 71 virtual void finish()
181 {
182 // Intentionally empty: most sinks stream their output as it happens, nothing to flush.
183 71 }
184
185 1448 virtual ~output_sink() = default;
186 };
187
188 //====================================================================================================================
189 /**
190 @public
191 @brief Default output_sink, streaming every message straight to `stdout`.
192
193 This is the sink TTS uses unless a power user installs another one, matching its historical
194 behavior of streaming output as it is produced.
195 **/
196 //====================================================================================================================
197 struct stdout_sink : output_sink
198 {
199 2436 void write(text const& t) override
200 {
201 2436 fputs(t.data(), stdout);
202 2436 }
203
204 503 void flush() override
205 {
206 503 fflush(stdout);
207 503 }
208 };
209
210 //====================================================================================================================
211 /**
212 @public
213 @brief output_sink gathering every message into a single retrievable buffer.
214
215 Instead of streaming messages as they are written, gathering_sink accumulates them into a
216 single tts::text. That buffer can be inspected at any time via content(), streamed out at once
217 via dump(), or discarded via clear().
218
219 @groupheader{Example}
220 @code
221 tts::gathering_sink report;
222 tts::output().sink(report);
223
224 // ... run the test suite ...
225
226 report.dump(); // stream everything gathered so far to stdout
227 // or
228 report.dump(some_other_sink); // forward it to another output_sink instead
229 // or
230 do_something_with(report.content()); // retrieve it to do something else entirely
231 @endcode
232 **/
233 //====================================================================================================================
234 struct gathering_sink : output_sink
235 {
236 135 void write(text const& t) override
237 {
238 135 buffer_ += t;
239 135 }
240
241 /// Retrieves everything gathered so far.
242 28 text const& content() const
243 {
244 28 return buffer_;
245 }
246
247 /// Forwards everything gathered so far to target, then clears the buffer.
248 2 void dump(output_sink& target)
249 {
250 2 target.write(buffer_);
251 2 clear();
252 2 }
253
254 /// Streams everything gathered so far to `stdout`, then clears the buffer.
255 1 void dump()
256 {
257 1 stdout_sink target;
258
1/1
✓ Branch 1 taken 1 times.
1 dump(target);
259 1 }
260
261 /// Discards everything gathered so far.
262 6 void clear()
263 {
264 6 buffer_ = text {};
265 6 }
266
267 private:
268 text buffer_;
269 };
270
271 //====================================================================================================================
272 /**
273 @public
274 @brief Centralizes all TTS runtime output and dispatches it to the current output_sink.
275
276 tts::output_handler is the class every piece of non-usage related TTS output goes through. It
277 exposes formatted writing at the granularity actually needed by the engine - a fragment, a
278 full line, raw text or a printf-style message - and forwards the result to whichever
279 output_sink is currently installed (tts::stdout_sink by default).
280
281 @see tts::output
282 @see tts::output_sink
283 **/
284 //====================================================================================================================
285 class output_handler
286 {
287 public:
288 /// Constructs an output_handler using s as its initial output_sink.
289 96 explicit output_handler(output_sink& s = default_sink())
290 96 : sink_(&s)
291 {
292 96 }
293
294 /// Writes a pre-built tts::text as-is.
295 9 void write(text const& t)
296 {
297 9 sink_->write(t);
298 9 }
299
300 /// Writes a raw C-string as-is.
301 5 void write(char const* s)
302 {
303
1/1
✓ Branch 2 taken 5 times.
5 sink_->write(text(s));
304 5 }
305
306 /// Writes a printf-style formatted message.
307 645 template<typename... Args> void write(char const* format, Args const&... args)
308 {
309
1/1
✓ Branch 2 taken 323 times.
645 sink_->write(text(format, args...));
310 645 }
311
312 /// Writes a pre-built tts::text followed by a newline.
313 1140 void writeln(text const& t = text {})
314 {
315 1140 sink_->write(t);
316
1/1
✓ Branch 2 taken 1140 times.
1140 sink_->write(text("\n"));
317 1140 }
318
319 /// Writes a raw C-string followed by a newline.
320 141 void writeln(char const* s)
321 {
322
1/1
✓ Branch 2 taken 141 times.
141 writeln(text(s));
323 141 }
324
325 /// Writes a printf-style formatted message followed by a newline.
326 1106 template<typename... Args> void writeln(char const* format, Args const&... args)
327 {
328
1/1
✓ Branch 2 taken 553 times.
1106 writeln(text(format, args...));
329 1106 }
330
331 /// Flushes the current output_sink, if that means anything for it.
332 535 void flush()
333 {
334 535 sink_->flush();
335 535 }
336
337 /// Notifies the current output_sink that a test is about to run.
338 282 void test_started(text const& name)
339 {
340 282 sink_->test_started(name);
341 282 }
342
343 /// Notifies the current output_sink that an assertion failed.
344 59 void assertion_failed(text const& location, text const& message, bool fatal)
345 {
346 59 sink_->assertion_failed(location, message, fatal);
347 59 }
348
349 /// Notifies the current output_sink that a test has finished running.
350 280 void test_finished(text const& name, bool passed, bool invalid, nanoseconds duration_ns)
351 {
352 280 sink_->test_finished(name, passed, invalid, duration_ns);
353 280 }
354
355 /// Notifies the current output_sink that the whole suite has finished running.
356 99 void suite_finished(counter fail_count, counter invalid_count)
357 {
358 99 sink_->suite_finished(fail_count, invalid_count);
359 99 }
360
361 /// Notifies the current output_sink that a Results: outcome category is about to print.
362 113 void suite_metric(outcome kind, counter count, counter total)
363 {
364 113 sink_->suite_metric(kind, count, total);
365 113 }
366
367 /// Notifies the current output_sink that the suite aborted early on a fatal failure.
368 2 void suite_aborted()
369 {
370 2 sink_->suite_aborted();
371 2 }
372
373 /// Notifies the current output_sink that tts::report() has returned.
374 71 void finish()
375 {
376 71 sink_->finish();
377 71 }
378
379 /// Installs s as the output_sink every subsequent write goes to.
380 48 void sink(output_sink& s)
381 {
382 48 sink_ = &s;
383 48 }
384
385 /// Retrieves the output_sink currently in use.
386 30 output_sink& sink() const
387 {
388 30 return *sink_;
389 }
390
391 /// The output_sink TTS uses unless a power user installs another one.
392 199 static stdout_sink& default_sink()
393 {
394
3/6
✓ Branch 1 taken 96 times.
✓ Branch 2 taken 103 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
295 static stdout_sink that = {};
395 199 return that;
396 }
397
398 private:
399 output_sink* sink_;
400 };
401
402 namespace _
403 {
404 inline output_handler current_output {};
405 }
406
407 //====================================================================================================================
408 /**
409 @public
410 @brief Retrieves the current TTS output_handler.
411
412 @return A reference to the tts::output_handler instance used by the current test suite.
413 **/
414 //====================================================================================================================
415 2461 inline output_handler& output()
416 {
417 2461 return _::current_output;
418 }
419
420 //====================================================================================================================
421 /**
422 @public
423 @brief RAII guard installing an output_sink for its scope, restoring the previous one on exit.
424
425 Saves whichever output_sink is currently installed on tts::output(), installs `s`, then restores
426 the saved one once the guard goes out of scope - even through an early return or an exception.
427
428 @groupheader{Example}
429 @snippet doc/custom_output_sink.cpp snippet
430
431 @see tts::output_sink
432 @see tts::output_handler
433 **/
434 //====================================================================================================================
435 class scoped_sink
436 {
437 public:
438 /// Installs s as the current output_sink, saving whichever one was previously installed.
439 22 explicit scoped_sink(output_sink& s)
440 22 {
441 22 output().sink(s);
442 22 }
443
444 22 ~scoped_sink()
445 {
446 22 output().sink(saved_);
447 22 }
448
449 scoped_sink(scoped_sink const&) = delete;
450 scoped_sink& operator=(scoped_sink const&) = delete;
451 scoped_sink(scoped_sink&&) = delete;
452 scoped_sink& operator=(scoped_sink&&) = delete;
453
454 private:
455 output_sink& saved_ = output().sink();
456 };
457
458 //====================================================================================================================
459 //! @}
460 //====================================================================================================================
461 }
462
463 namespace tts::_
464 {
465 //====================================================================================================================
466 // Shared separator line for display helpers to visually delimit sections of output (see
467 // tts::_::header/results in ranges.hpp). printable lets a caller fold its own display condition
468 // (e.g. !tts::is_quiet()) into the call instead of guarding it at every call site.
469 //====================================================================================================================
470 136 inline void separator(bool printable = true)
471 {
472
2/2
✓ Branch 0 taken 135 times.
✓ Branch 1 taken 1 times.
136 if(printable)
473 135 ::tts::output().writeln(
474 "--------------------------------------------------------------------------------");
475 136 }
476 }
477