Skip to main content

runner.cpp File

C++ source file with implementations for the µTest++ test runner member functions. More...

Included Headers​

Namespaces Index​

namespacemicro_os_plus

The primary namespace for the µOS++ framework. More...

namespacemicro_test_plus

Primary namespace for the µTest++ testing framework. More...

namespaceanonymous{runner.cpp}

Local helpers of the runner. More...

Variables Index​

constexpr const char *help_options = ...

The text displayed by --help, after the usage line. More...

Description​

C++ source file with implementations for the µTest++ test runner member functions.

This source file contains the core implementations for the test runner facilities of the µTest++ framework. It provides the logic for initialising the test environment, registering and managing test suites, handling command-line arguments, orchestrating test execution, and determining the overall test result. The registered suites and the suites declared at namespace scope are executed together, in a single order, sorted by name. A mechanism for aborting test execution in critical scenarios is also provided.

All definitions reside within the micro_os_plus::micro_test_plus namespace.

This file must be included when building the µTest++ library.

Variables​

help_options​

const char* micro_os_plus::micro_test_plus::anonymous{runner.cpp}::help_options
constexpr

The text displayed by --help, after the usage line.

Initialiser
= "Options:\n"   " --help, -h\n"   " --verbose | --quiet | --silent\n"   " --list-tests, --list\n"   " --reporter=human|tap\n"   " --output-file=<path>\n"   " --colour[=always|never|auto], --color[=...]\n"   " --use-colour=yes|no|auto, --use-color=...\n"   " --filter=<pattern>\n"   " Run only the matching names; repeat the option for each\n"   " nesting level: the first pattern applies to the suite\n"   " names, the second to the test case names, and each\n"   " further one to the nested test cases. The patterns use\n"   " '*' and '?', and '|' separates alternatives.\n"

Lists the options recognised by runner_options, one per line, with the description of the --filter levels.

Definition at line 95 of file runner.cpp.

96 = "Options:\n"

File Listing​

The file content with the documentation metadata removed is:

1/*
2 * This file is part of the µOS++ project (https://micro-os-plus.github.io/).
3 * Copyright (c) 2021-2026 Liviu Ionescu. All rights reserved.
4 *
5 * Permission to use, copy, modify, and/or distribute this software for any
6 * purpose is hereby granted, under the terms of the MIT license.
7 *
8 * If a copy of the license was not distributed with this file, it can be
9 * obtained from https://opensource.org/licenses/mit.
10 */
11
12// ----------------------------------------------------------------------------
13
34
35// ----------------------------------------------------------------------------
36
41
42#if __has_include("micro-os-plus/diag/trace.h")
43#include "micro-os-plus/diag/trace.h"
44#endif // __has_include("micro-os-plus/diag/trace.h")
45
46#include <cstdio>
47#include <cstring>
48#include <string>
49
50// ----------------------------------------------------------------------------
51
52#if defined(__GNUC__)
53#pragma GCC diagnostic ignored "-Waggregate-return"
54#if defined(__clang__)
55#pragma clang diagnostic ignored "-Wunknown-warning-option"
56#pragma clang diagnostic ignored "-Wc++98-compat"
57#pragma clang diagnostic ignored "-Wc++98-c++11-c++14-compat"
58#pragma clang diagnostic ignored "-Wpre-c++17-compat"
59#pragma clang diagnostic ignored "-Wc++20-compat"
60#pragma clang diagnostic ignored "-Wunsafe-buffer-usage-in-libc-call"
61#endif // defined(__clang__)
62#endif // defined(__GNUC__)
63
64// ============================================================================
65
66#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_ENABLED)
67
68// ----------------------------------------------------------------------------
69
71{
72 // --------------------------------------------------------------------------
73
86 namespace
87 {
95 constexpr const char* help_options
96 = "Options:\n"
97 " --help, -h\n"
98 " --verbose | --quiet | --silent\n"
99 " --list-tests, --list\n"
100 " --reporter=human|tap\n"
101 " --output-file=<path>\n"
102 " --colour[=always|never|auto], --color[=...]\n"
103 " --use-colour=yes|no|auto, --use-color=...\n"
104 " --filter=<pattern>\n"
105 " Run only the matching names; repeat the option for each\n"
106 " nesting level: the first pattern applies to the suite\n"
107 " names, the second to the test case names, and each\n"
108 " further one to the nested test cases. The patterns use\n"
109 " '*' and '?', and '|' separates alternatives.\n";
110 } // namespace
111
112 // --------------------------------------------------------------------------
113
120 runner::runner (void) : top_suite_{ "", *this }
121 {
122#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_CONSTRUCTORS_ENABLED)
123 trace::printf ("%s '%s'\n", __PRETTY_FUNCTION__, top_suite_.name ());
124#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_CONSTRUCTORS_ENABLED
125 }
126
127#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_DEPRECATED_ENABLED)
134 // Deprecated since 4.1.0; use runner() and add_top_suite().
135 runner::runner (const char* top_suite_name)
136 : top_suite_{ top_suite_name, *this }
137 {
138#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_CONSTRUCTORS_ENABLED)
139 trace::printf ("%s '%s'\n", __PRETTY_FUNCTION__, top_suite_.name ());
140#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_CONSTRUCTORS_ENABLED
141 }
142#endif // defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_DEPRECATED_ENABLED)
143
150 {
151#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_CONSTRUCTORS_ENABLED)
152 trace::printf ("%s\n", __PRETTY_FUNCTION__);
153#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_CONSTRUCTORS_ENABLED
154
155 for (runner** slot = &started_first_; *slot != nullptr;
156 slot = &(*slot)->started_next_)
157 {
158 if (*slot == this)
159 {
160 *slot = started_next_;
161 break;
162 }
163 }
164
165 // reporter_ is a unique_ptr; destroyed automatically.
166 }
167
168#if defined(__GNUC__)
169#pragma GCC diagnostic push
170
171#if defined(__clang__)
172#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
173#endif // defined(__clang__)
174#endif // defined(__GNUC__)
175
176#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_DEPRECATED_ENABLED)
186 // Deprecated since 6.0.0; use initialise (argc, argv) and
187 // add_top_suite().
188 suite&
189 runner::initialise (int argc, char* argv[], const char* top_suite_name)
190 {
192 {
193 return top_suite_;
194 }
195
196 initialise (argc, argv);
197
198 return add_top_suite (top_suite_name != nullptr ? top_suite_name : "");
199 }
200#endif // defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_DEPRECATED_ENABLED)
201
215 void
216 runner::initialise (int argc, char* argv[])
217 {
218#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED)
219 trace::printf ("%s\n", __PRETTY_FUNCTION__);
220
221#if !(defined(MICRO_OS_PLUS_STARTUP_ENABLED) \
222 && defined(MICRO_OS_PLUS_DIAG_TRACE_ENABLED))
223#if defined(MICRO_OS_PLUS_DEBUG_ENABLED)
224 trace::printf ("argv[");
225 for (int i = 0; i < argc; ++i)
226 {
227 if (i > 0)
228 {
229 trace::printf (", ");
230 }
231 trace::printf ("'%s'", argv[i]);
232 }
233 trace::puts ("]");
234#endif // defined(MICRO_OS_PLUS_DEBUG_ENABLED)
235#endif // !defined(MICRO_OS_PLUS_STARTUP_ENABLED)
236#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED
237
239 {
241 return;
242 }
243
244 initialise (runner_options{ argc, argv });
245 }
246
272 void
274 {
275#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED)
276 trace::printf ("%s\n", __PRETTY_FUNCTION__);
277#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED
278
279 if (aborted_)
280 {
281 fprintf (stderr, "error: test execution aborted\n");
282 return;
283 }
284
286 {
287 reject_call ("test runner already initialised");
288 return;
289 }
290
291 // Move the vectors, since their copy assignment uses std::copy, which
292 // triggers -Waggregate-return in the standard library headers on some
293 // targets.
294 options_ = std::move (options);
295
296 // With errors in the options, do not proceed: the errors are written on
297 // the console and in the output file, if any, which is truncated, so
298 // that it never keeps the results of an earlier run; no test is run, and
299 // `run_suites()` returns 2. The same for `--help`, which writes the
300 // usage instead of the errors, only on the console, and `run_suites()`
301 // returns 0. A reporter is still created, since the suites and the test
302 // cases refer to it.
304 {
305 runner_options console_options{ options_ };
306 if (!options_.has_errors ())
307 {
308 console_options.output_file_path.clear ();
309 }
310 if (console_options.selected_reporter == reporter_kind::human)
311 {
313 = std::make_unique<detail::reporter_human> (console_options);
314 }
315 else
316 {
318 = std::make_unique<detail::reporter_tap> (console_options);
319 }
320
321 for (const std::string& error : options_.errors ())
322 {
323 fprintf (stderr, "error: %s\n", error.c_str ());
324 reporter_->write_line (
326 reporter_->error_prefix (), error.c_str ());
327 }
329 {
332 "%sUse --help to list the options.\n",
333 reporter_->comment_prefix ());
334 }
336 {
337 // Requested, so written also with the silent verbosity.
338 const char* program = options_.arguments ().empty ()
339 ? "<program>"
341 options_.arguments ()[0].c_str ());
342 reporter_->write_line (true,
343 "Usage: %s [options] [--] "
344 "[arguments]\n%s",
345 program, help_options);
346 }
347 reporter_->flush_streams ();
348
349 exit_code_ = options_.has_errors () ? 2 : 0;
351 return;
352 }
353
354 // Initialise and configure the reporter; it keeps its own copy of the
355 // verbosity, taken from the same options.
357 {
358 reporter_ = std::make_unique<detail::reporter_human> (options_);
359 }
360 else
361 {
362 reporter_ = std::make_unique<detail::reporter_tap> (options_);
363 }
364
365 // If the output file cannot be opened, do not run the tests either.
366 if (reporter_->has_output_file_error ())
367 {
369 "%sfailed to open output file '%s'\n",
370 reporter_->error_prefix (),
371 options_.output_file_path.c_str ());
372 reporter_->flush_streams ();
373
374 exit_code_ = 2;
376 return;
377 }
378
379 // ------------------------------------------------------------------------
380
381 // When listing, the session is not reported; only the header of the
382 // list is written, and the test cases are listed as they are created.
384 {
385 reporter_->write_line (true, "%s\n",
387 ? "All available test cases:"
388 : "Matching test cases:");
390 return;
391 }
392
394 reporter_->begin_session (*this);
395
397 }
398
419 suite&
420 runner::add_top_suite (const located_name& top_suite_name)
421 {
422 const std::string_view requested_name{ top_suite_name.name () };
423
424#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED)
425 trace::printf ("%s\n", __PRETTY_FUNCTION__);
426#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED
427
428 if (aborted_)
429 {
430 fprintf (stderr, "error: test execution aborted\n");
431 return top_suite_;
432 }
433
435 {
437 }
438
440 {
441 return top_suite_;
442 }
443
445 {
446 reject_call ("top suite already added", &top_suite_name.location ());
447 return top_suite_;
448 }
449
450 if (!requested_name.empty ())
451 {
452 // If provided by this call, use it, possibly override the
453 // deprecated constructor.
454 top_suite_.located_name_ = top_suite_name;
456 }
457 else if (strlen (top_suite_.name ()) == 0)
458 {
459 // If not provided by the constructor or by this call, try to extract a
460 // name from argv[0], which is commonly the executable name. If that
461 // fails, use a default name.
462 if (!options_.arguments ().empty ())
463 {
464 std::string_view top_suite_name_view{ utility::extract_file_name (
465 options_.arguments ()[0].c_str ()) };
466
467 const auto dot_pos = top_suite_name_view.rfind ('.');
468 if (dot_pos != std::string_view::npos)
469 {
470 top_suite_name_view = top_suite_name_view.substr (0, dot_pos);
471 }
472
474 = located_name{ top_suite_name_view,
475 top_suite_name.location () };
476 }
477 else
478 {
480 = located_name{ "default suite", top_suite_name.location () };
481 }
483 }
484
485 // Named first, so that the rejection names it.
487 {
488 reject_suite (top_suite_.name (), top_suite_name.location ());
489 return top_suite_;
490 }
491
492 has_top_suite_ = true;
493
494 // The top suite runs until run_suites() is called; its test cases
495 // are accepted only meanwhile.
497
498 // When listing, the top suite is not reported; its test cases are
499 // listed as they are created.
501 {
503 reporter_->begin_suite (top_suite_);
504 }
505
506 return top_suite_;
507 }
508
513 suite&
515 {
516 return add_top_suite (located_name{ "" });
517 }
518
519#if defined(__GNUC__)
520#pragma GCC diagnostic pop
521#endif // defined(__GNUC__)
522
529 bool
530 runner::is_selected (const char* name, size_t level) const
531 {
532 if (level >= options_.filter_patterns.size ())
533 {
534 return true;
535 }
536
537 std::string_view alternatives{ options_.filter_patterns[level] };
538 while (true)
539 {
540 const size_t separator = alternatives.find ('|');
541 if (utility::is_match (name, alternatives.substr (0, separator)))
542 {
543 return true;
544 }
545 if (separator == std::string_view::npos)
546 {
547 return false;
548 }
549 alternatives.remove_prefix (separator + 1);
550 }
551 }
552
553 // --------------------------------------------------------------------------
554
562 void
564 const test_case& test_case)
565 {
566 if (&suite != listed_suite_)
567 {
570 reporter_->write_line (true, " %s\n", suite.name ());
571 }
572
574 reporter_->write_line (true, " %s\n", test_case.name ());
575 }
576
577 // --------------------------------------------------------------------------
578
598 void
599 runner::register_suite (std::unique_ptr<micro_test_plus::suite> suite)
600 {
601#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED)
602 trace::printf ("%s '%s'\n", __PRETTY_FUNCTION__, suite->name ());
603#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED
604
606 {
607 // Nothing runs in a declined session; the suite is discarded.
608 return;
609 }
610
613 {
614 reject_suite (suite->name (), suite->located_name_.location ());
615 return;
616 }
617
618 children_suites_.push_back (std::move (suite));
619 children_suites_.back ()->link ();
620 }
621
627 void
629 const reflection::source_location& location)
630 {
631 std::string message{ "suite '" };
632 message.append (name);
633 message.append ("' added after run_suites() started, rejected");
634
635 reject_call (message, &location);
636 }
637
659 void
660 runner::reject_call (const std::string& message,
661 const reflection::source_location* location)
662 {
663 fprintf (stderr, "error: %s\n", message.c_str ());
665
667 || aborted_)
668 {
669 return;
670 }
671
673 {
675 "%serror: %s\n", reporter_->comment_prefix (),
676 message.c_str ());
677 reporter_->flush_streams ();
678 return;
679 }
680
681 if (running_test_case_ != nullptr)
682 {
684 message, location != nullptr
685 ? *location
687 return;
688 }
689
690 if (running_suite_ != nullptr)
691 {
693 reporter_->fail_suite (message, *running_suite_);
695 return;
696 }
697
699 "%serror: %s\n", reporter_->comment_prefix (),
700 message.c_str ());
701 reporter_->flush_streams ();
702 }
703
710 void
711 runner::mark_rejected (void) noexcept
712 {
715 {
716 exit_code_ = 1;
717 }
718 }
719
727 void
730 {
731 for (const runner* started = started_first_; started != nullptr;
732 started = started->started_next_)
733 {
734 if (started == &test_runner)
735 {
736 test_runner.reject_suite (suite.name (),
737 suite.located_name_.location ());
738 return;
739 }
740 }
741
742 suite.link ();
743 }
744
750 constinit runner* runner::started_first_ = nullptr;
751
777 void
779 {
780 // The top suite, if added, has index 1.
781 size_t own_index = has_top_suite_ ? 1 : 0;
782 for (micro_test_plus::suite* suite_ptr = suite::first_;
783 suite_ptr != nullptr; suite_ptr = suite_ptr->next_)
784 {
785 // Skip the suites run by other runners.
786 if (&suite_ptr->test_runner () != this)
787 {
788 continue;
789 }
790
791 suite_ptr->own_index_ = ++own_index;
792
793 if (!is_selected (suite_ptr->name (), 0))
794 {
797 {
798 reporter_->skip_suite (*suite_ptr);
799 }
800 continue;
801 }
802
803 suite_ptr->run ();
804
805 // Accumulate the totals from the child suite into the runner
806 // totals.
807 // DO NOT increment executed_test_cases here.
808 totals_ += suite_ptr->totals ();
809 }
810 }
811
839 int
841 {
842#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED)
843 trace::printf ("%s\n", __PRETTY_FUNCTION__);
844#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED
845
846 if (aborted_)
847 {
848 fprintf (stderr, "error: test execution aborted\n");
849 return 1;
850 }
851
854 {
855 // The session has ended; do not run the suites again.
856 return exit_code_;
857 }
858
860 {
861 fprintf (stderr, "error: test runner already running\n");
862 return 1;
863 }
864
866 {
868 }
869
871 running_suite_ = nullptr;
873 started_first_ = this;
874
875 // When listing, the suites run only to create their test cases, which
876 // are listed, and the number of listed test cases ends the list.
878 {
880
881 const size_t test_cases = listed_test_cases_count_;
882 const size_t suites = listed_suites_count_;
883 reporter_->write_line (
884 true,
887 " test suite%s\n",
888 test_cases, (test_cases == 1) ? "" : "s", suites,
889 (suites == 1) ? "" : "s");
890
891 // As for a run, filters that select no test case are an error.
892 const bool listed_nothing = total_suites_count () != 0
893 && !options_.filter_patterns.empty ()
895 if (listed_nothing)
896 {
897 fprintf (stderr, "error: the filters selected no test case\n");
898 reporter_->write_line (
900 "%serror: the filters selected no test case\n",
901 reporter_->comment_prefix ());
902 }
903 reporter_->flush_streams ();
904
905 if (listed_nothing || has_option_errors ())
906 {
907 exit_code_ = 2;
908 }
909 else
910 {
912 }
914 return exit_code_;
915 }
916
918 {
920 reporter_->end_suite (top_suite_);
922 }
923
925
926 // A session without suites runs nothing; report it.
927 const bool has_no_suites = (total_suites_count () == 0);
928 if (has_no_suites)
929 {
930 fprintf (stderr, "error: no test suites\n");
932 "%serror: no test suites\n",
933 reporter_->comment_prefix ());
934 }
935
936 // A filter that selects no test case disables the tests; report it.
937 const bool selected_nothing = !has_no_suites
938 && !options_.filter_patterns.empty ()
940 if (selected_nothing)
941 {
942 fprintf (stderr, "error: the filters selected no test case\n");
944 "%serror: the filters selected no test case\n",
945 reporter_->comment_prefix ());
946 }
947
949 reporter_->end_session (*this);
950
951 if (selected_nothing || has_option_errors ())
952 {
953 exit_code_ = 2;
954 }
955 else
956 {
958 = (totals_.passed () && !has_rejected_calls_ && !has_no_suites)
959 ? 0
960 : 1;
961 }
963
964#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED)
965 trace::printf ("%s -> %d\n", __PRETTY_FUNCTION__, exit_code_);
966#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED
967
968 return exit_code_;
969 }
970
971#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_DEPRECATED_ENABLED)
977 // Deprecated since 6.0.0; use run_suites().
978 int
980 {
981 return run_suites ();
982 }
983#endif // defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_DEPRECATED_ENABLED)
984
992 {
994 abort_handler_ = handler;
995 return previous;
996 }
997
1003 void
1005 {
1006 abort (sl, false);
1007 }
1008
1028 void
1029 runner::abort (const reflection::source_location& sl, bool is_requirement)
1030 {
1031#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED)
1032 trace::printf ("%s\n", __PRETTY_FUNCTION__);
1033#endif // MICRO_OS_PLUS_MICRO_TEST_PLUS_TRACE_ENABLED
1034
1035 if (!aborted_)
1036 {
1037 aborted_ = true;
1038
1041 {
1042 reporter_->abort_session (sl, is_requirement);
1043 }
1044 else
1045 {
1046 // The type of `line()` depends on the toolchain
1047 // (`uint_least32_t` for `std::source_location`); widen it
1048 // implicitly, without a cast that would be useless on some
1049 // platforms.
1050 const unsigned long line = sl.line ();
1051 fprintf (stderr, "error: test execution aborted at %s:%lu\n",
1053 }
1054 }
1055
1056 fflush (stdout);
1057 fflush (stderr);
1058
1059 if (abort_handler_ != nullptr)
1060 {
1061 abort_handler_ (sl);
1062 }
1063
1064 ::abort ();
1065 }
1066
1072 size_t
1073 runner::added_suites_count (void) const noexcept
1074 {
1075 return children_suites_.size ();
1076 }
1077
1078#if defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_DEPRECATED_ENABLED)
1084 // Deprecated since 6.0.0; the name is that of the top suite.
1085 const char*
1086 runner::name (void) const noexcept
1087 {
1088 return "runner";
1089 }
1090
1096 // Deprecated since 6.0.0; use added_suites_count() or total_suites_count().
1097 size_t
1098 runner::suites_count (void) const noexcept
1099 {
1100 return added_suites_count () + 1;
1101 }
1102#endif // defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_DEPRECATED_ENABLED)
1103
1110 size_t
1111 runner::static_suites_count (void) const noexcept
1112 {
1113 size_t count = 0;
1114 for (const micro_test_plus::suite* suite_ptr = suite::first_;
1115 suite_ptr != nullptr; suite_ptr = suite_ptr->next_)
1116 {
1117 if (&suite_ptr->test_runner () == this)
1118 {
1119 ++count;
1120 }
1121 }
1122 return count - children_suites_.size ();
1123 }
1124
1130 size_t
1131 runner::total_suites_count (void) const noexcept
1132 {
1133 return (has_top_suite_ ? 1u : 0u) + added_suites_count ()
1135 }
1136
1141 size_t
1142 runner::skipped_suites_count (void) const noexcept
1143 {
1145 }
1146
1147 // --------------------------------------------------------------------------
1148} // namespace micro_os_plus::micro_test_plus
1149
1150// ----------------------------------------------------------------------------
1151
1152#endif // defined(MICRO_OS_PLUS_MICRO_TEST_PLUS_ENABLED)
1153
1154// ----------------------------------------------------------------------------

Generated via doxygen2docusaurus 2.2.8 by Doxygen 1.18.0.