Making changes
This page collects the rules that apply to every change of the framework: where the code goes, the constraints that follow from the embedded targets, the documentation, and the tests. The commands that build and test the project, and the formatting tools, are described on the first page of the Contributor's Guide.
Where the code goes
Each part of the framework is split across three files with the same name:
| File | Contents |
|---|---|
include/micro-os-plus/micro-test-plus/<name>.h | the declarations, with their Doxygen comments |
include/micro-os-plus/micro-test-plus/inlines/<name>-inlines.h | the definitions of the templates, of the inline functions, and of the constexpr functions |
src/<name>.cpp | the non-inline definitions |
A new member of an existing class follows the same split. A new header is
included by micro-test-plus.h, in the order of the dependencies (see
The files); the applications never
include it directly.
The top-level CMakeLists.txt and meson.build are generated, and must not
be edited by hand. A new source file, a new include folder, or a new
dependency is added to xcdl-package.jsonc, and the build files are
regenerated with:
xpm run xcdl-export
Only the publicIncludeFolders, sourceFiles, publicDefines,
compilerOptions, and dependencies properties affect the generated files;
the other properties of xcdl-package.jsonc are informative.
Keep the templates small
The templates are instantiated for each type of operand, in every
translation unit of the tests; their code is multiplied accordingly. A
template should do only the work that depends on the type, and delegate the
rest to a non-template function, compiled once, in src/. The check result
is the model (see Initialisation):
init() evaluates and formats the expression, and passes a bool and the
location to start(), while the destructor, which does the reporting, is not
a template at all.
The constraints of the embedded targets
Every change must build and run on the bare-metal targets, under QEMU and on the boards. In practice:
- No streams: do not include
<iostream>,<ostream>, or<sstream>; write text through the formatter or the reporter, which use the C standard I/O functions. - No allocations on the paths that run often: a check, a test case, and
its body should not allocate; reuse the existing buffers. The current
exception is the name of a test case longer than the small-string capacity,
which
located_namecopies into astd::string, freed whentest()returns, so that the heap does not grow; a change that adds an allocation should keep this property. The objects created once per session may allocate. - Builds without exceptions: guard the code that throws or catches with
#if defined(__cpp_exceptions), and provide the behaviour without them; the reference outputs have-noexceptvariants for these builds. The embedded platforms are built without exceptions; on the native platform (synthetic-posix), theunit-test-noexceptprogram compiles the framework and the unit tests with-fno-exceptions, with the strict warnings of the tests, so a warning that appears only without exceptions (such as a private member that only the code under#if defined(__cpp_exceptions)uses) fails the native build. The framework and the programs that use it must be compiled with the same exception option, since some classes depend on it; the other native programs link the static library, compiled with exceptions, so this one also links the interface library, which compiles the sources of the framework in the program, and the linker does not use the members of the archive. - Small stacks: the nested test cases run at the deepest point of the
stack, and the embedded targets have small stacks. Avoid large local
arrays (
reporter::write_line()formats into a local array of 128 bytes), avoid recursion where an iteration will do (as inutility::is_match()), and call the callables directly, rather than throughstd::invoke(), which adds frames without optimisations. For the same reason, keep the test cases of theunit-miscellaneousprogram small. - Warnings: the tests are built with many warnings enabled, with several
compilers. When a warning must be silenced, do it locally, with a separate
group of pragmas for each compiler, guarded by
__GNUC__and, if needed,__clang__, as in the existing sources. The Cortex-M GCC release builds treat-Wsuggest-final-typesand-Wsuggest-final-methodsas errors, so a new virtual function ofdetail::reporteris pure virtual, with an override in both reporters and in thebuffer_reporterofunit-miscellaneous; the native builds do not show these errors, so runtest-selectedafter such a change. - Portability of the standard library: some features are missing or
incomplete on the embedded toolchains (for example
std::to_chars()forlong double); check the existing code for the workarounds before using a new standard facility.
Style
- The C++ names are in snake_case; the descriptive template parameters end
in
_T(Expr_T,Callable_T); the private data members of the classes end in_. - The nested namespaces are written on one line
(
namespace micro_os_plus::micro_test_plus::detail). - The includes are grouped (project, µOS++, third-party, standard library), in alphabetical order within each group, separated by blank lines.
- The code is formatted with
clang-format, using the configuration inmaintenance/config/; the formatting is applied withxpm run clang-format, from the top folder of the project. - The comments and the documentation use British English.
Documentation
- Doxygen comments: every entity (class, member, function, parameter,
return value), and every file, including those in
inlines/andsrc/, has a Doxygen comment, with a@briefand, where useful, a@details. The comments are updated in the same change as the code, so that they always describe what the code does. The rules are in.github/skills/doxygen-review/SKILL.md. - The User's Guide: a change visible to the applications (a new function, a new option, a different report) is described in the relevant page of the User's Guide, and, if it breaks existing code, in the compatibility notices.
- The API reference: the pages under
/docs/api/are generated from the Doxygen comments, and are regenerated separately; they are not edited by hand. - These pages: a change to a mechanism described in the Contributor's Guide is reflected in the page that describes it, including the code excerpts.
Tests
The tests are separate programs, in tests/sources/, built and run for each
platform and toolchain:
unitlinks the bodies of theunit-checks,unit-combinations,unit-exceptions, andunit-miscellaneousfolders, which share the header inunit-common;unit-deprecatedandunit-static-assertsare separate programs;empty,minimal,sample, andsuiteexercise the main uses of the framework, as an application would.
The output of each program is compared with the reference files in
tests/expected/tap and tests/expected/txt: the -noexcept variants are
for the builds without exceptions, and the -console, -console-quiet, and
-colour variants hold the console outputs of the unit programs. A change
that alters a report, or adds or removes a check, must update these files in
the same commit.
The TAP outputs are also checked with tests/scripts/validate-tap.mjs, which
parses them with the node-tap parser and rejects the plans that do not match
their test points, among other errors. The console captures are checked with
its --lenient option, which accepts the lines printed by the test programs,
but not the malformed TAP lines.
Run the tests with the system compiler after a minor change:
xpm run test -C tests
and a minimal selection of platforms and toolchains after a major one:
xpm run test-selected -C tests
The other actions, for each platform and toolchain, are described in the Tests section of the first page of this guide.
Reviews
The project has agent skills, in .github/skills/, which describe the
checks of the reviews: api-review and code-review for the code,
doxygen-review for the comments, website-review and website-api-review
for the website, and consistency-check for the names and the
configurations. They are useful checklists also for a review done by hand.