Skip to main content

Extending the framework

The applications can make their own types readable in the reports, and can write their own comparators. These are advanced features, which are not needed for the usual checks described in Writing checks.

Utility functions​

namespace utility {
bool is_match (std::string_view input, std::string_view pattern);
}

namespace utility {
template <class T, class Delim_T> // string-like T only
auto split (T input, Delim_T delim) -> std::vector<T>;

// Selected for string literals and `const char*`.
std::vector<std::string_view>
split (std::string_view input, std::string_view delimiters);
}

is_match() checks whether the whole input matches a pattern, in which * matches any sequence of characters, including an empty one, and ? matches any single character; the comparison is case-sensitive. The --filter option uses it, for each of the alternatives that it separates with |.

split() splits a string at any of the delimiter characters, and returns the sub-strings in a std::vector, omitting the empty ones. For a std::string_view, a string literal, or a const char*, the elements are views into the input, which must outlive them; for a std::string they are copies. The vector allocates memory from the heap, which may matter on embedded targets.

Refer to the Utility Functions reference page.

Custom types​

The function comparators also accept user-defined types, provided that the type has the operator used by the comparator: operator== for eq() and ne() (in C++20, operator== also provides !=), and operator<=> (or the individual relational operators) for lt(), le(), gt(), and ge().

Since the comparators report the values of their operands when a check fails, they also need to know how to print the type. For this, the application must define an operator<< that writes the value to an mt::expression_formatter, in the namespace of the type, so that it is found by argument-dependent lookup. Without it, the comparators do not compile for that type: the call is rejected with a message that says that an operand cannot be written in the report, and what to define; only the scoped enumerations are shown without it, by their underlying value.

#include "micro-os-plus/micro-test-plus.h"

namespace mt = micro_os_plus::micro_test_plus;

namespace app
{
struct point
{
int x;
int y;
};

constexpr bool
operator== (const point& lhs, const point& rhs)
{
return lhs.x == rhs.x && lhs.y == rhs.y;
}

// Used to report the values of failed checks.
inline mt::expression_formatter&
operator<< (mt::expression_formatter& formatter, const point& p)
{
return formatter << "{ " << p.x << ", " << p.y << " }";
}
} // namespace app

mt::runner tr;

int
main (int argc, char* argv[])
{
tr.initialise (argc, argv);
auto& ts = tr.add_top_suite ();

ts.test ("Points", [] (auto& t) {
app::point p{ 1, 2 };
t.expect (mt::eq (p, app::point{ 1, 3 })) << "point is { 1, 3 }";
});

return tr.run_suites ();
}

The failed check reports both values:

not ok 1 - point is { 1, 3 }
---
condition: "{ 1, 2 } == { 1, 3 }"
at:
fileName: main.cpp
lineNumber: 37
...

The formatter accepts the standard arithmetic types, characters, strings, pointers, containers, scoped enumerations, pairs, tuples, and optionals, so the members can usually be written directly, as above. The char pointers and arrays, also when not const, and those of the wchar_t, char8_t, char16_t, and char32_t types, converted to UTF-8, are written as strings (a null pointer as nullptr); the other pointers, including the signed char and unsigned char (uint8_t) pointers, are written as addresses. A plain boolean check, such as t.expect (p == q), needs no formatter, but on failure it reports only the source location. The same operator<< also writes the values appended to the messages (t.expect (p == q) << "p is " << p).

To test such an operator<< on its own, construct a formatter, write the value, and compare str() with the expected text:

mt::expression_formatter formatter;
formatter << app::point{ 1, 2 };
// formatter.str () is "{ 1, 2 }".

A formatter constructed in this way writes the values as the reports of the failed comparisons of the built-in types do, with the type suffixes of the numbers (1l, 2u) and the strings quoted. In the reports of the comparisons of the application types, the values written by the operator<< of the application are shown as it writes them, without the suffixes and the quotes, as in the messages.

Custom comparators​

When the relation to check is not one of the built-in comparisons, a function that returns bool can be used, but on failure it reports only the source location. To report the operands, as the built-in comparators do, the function must return a framework expression, built with the public templates of the mt::extension namespace:

  • extension::compare_op{ lhs, rhs, tag }, for a comparison of two operands;
  • extension::predicate_op{ operand, tag }, for a property of one operand.

The tag is an empty structure that defines the operation, with a static apply() member function template, which receives the operands and returns the result, and a static symbol, written in the failure reports between the two operands, or before the single one. A tag that has no symbol that can be written, or no apply() that can be called with the operands and returns a value convertible to bool, is rejected with a message that states what the tag needs, not with the trace of the concepts:

#include "micro-os-plus/micro-test-plus.h"

namespace mt = micro_os_plus::micro_test_plus;

namespace app
{
struct divides_tag
{
static constexpr const char* symbol = " divides ";

template <class Lhs_T, class Rhs_T>
static constexpr bool
apply (const Lhs_T& lhs, const Rhs_T& rhs)
{
return rhs % lhs == 0;
}
};

struct even_tag
{
static constexpr const char* symbol = "even ";

template <class T>
static constexpr bool
apply (const T& t)
{
return t % 2 == 0;
}
};

// A custom comparator.
template <class Lhs_T, class Rhs_T>
constexpr auto
divides (const Lhs_T& lhs, const Rhs_T& rhs)
{
return mt::extension::compare_op{ lhs, rhs, divides_tag{} };
}

// A custom predicate.
template <class T>
constexpr auto
even (const T& t)
{
return mt::extension::predicate_op{ t, even_tag{} };
}

} // namespace app

mt::runner tr;

int
main (int argc, char* argv[])
{
tr.initialise (argc, argv);

auto& ts = tr.add_top_suite ();

ts.test ("Custom", [] (auto& t) {
t.expect (app::divides (5, 12));
t.expect (app::even (7));
t.expect (mt::all_of (app::divides (3, 12), app::even (7)));
});

return tr.run_suites ();
}

The failed checks report the operands, with the symbols of the tags:

✗ FAILED (main.cpp:59, 5 divides 12)
✗ FAILED (main.cpp:60, even 7)
✗ FAILED (main.cpp:61, (3 divides 12 and even 7))

The tag is stateless, so it cannot carry a parameter, such as a tolerance, to be reported with the operands. A parameter is passed as an operand, usually the right one, as the divisor of divides above, and is then written in the report like any other operand.

The expressions can be combined with the logical functions, and, being constexpr, also checked at compile time (static_assert (app::divides (3, 12))). The operands are written with the operator<< of the formatter, so the types of the application need one, as for the custom types. The C strings and the arrays among the operands are reported as the built-in comparators report them (quoted, and as ranges), while the tag receives the operands as they were passed.

For brevity, the example above takes its operands as const&.

A temporary operand is moved into the expression, and a named operand is copied, so an expression can be returned from the function that builds it, or kept in a variable, after the operands are destroyed; the C strings and the other arrays are the exception, since the expression refers to their characters or elements. An operand whose class cannot be copied (for example an application type with a deleted copy constructor) is referred to instead, and must outlive the check. To move the temporaries, and not copy them, take the operands as forwarding references (Lhs_T&& lhs) and pass them on with std::forward<Lhs_T> (lhs), as eq() does.

A helper that passes its argument on to expect() or require() can be constrained with extension::checkable, as these functions are, and one that passes it on to the message operator with extension::printable.