Rexo
Rexo is a neat single-file cross-platform unit testing framework for C/C++.
It offers the same xUnit-like structure than most other unit testing frameworks but aims at providing a truly polished API.
Features
- sleek: polished API with great attention to details.
- easy: no learning curve, it's yet another framework based on xUnit with test suites, test cases, and fixtures.
- convenient: automatic registration of tests.
- granular: high level or low level API? You choose.
- portable: compatible with C89 (ANSI C) and C++.
- cross-platform: tested on Linux, macOS, and Windows.
- simple: straightforward implementation—KISS all the things!
- cascading configuration: configure a whole test suite at once and/or tweak specific options for each test case.
- painless: deployment couldn't be easier—it all fits into a single header file and has no external dependencies.
But also...
- fully standard compliant minus the optional automatic registration of tests that relies on a widespread compiler-specific feature.
- designated initializer-like syntax to all C and C++ versions.
Roadmap
- implement a command-line option parser (e.g.: for filtering test cases).
- allow choosing the output format of the summary (e.g.: jUnit XML).
- support more assertion macros (e.g.: array comparison, signal handling).
- improve failure messages to be more visual (e.g.: an arrow pointing where strings differ).
Usage
Minimal
#include <rexo.h>
RX_TEST_CASE(foo, bar)
{
RX_INT_REQUIRE_EQUAL(2 * 3 * 7, 42);
}
int
main(int argc, const char **argv)
{
return rx_main(0, NULL, argc, argv) == RX_SUCCESS ? 0 : 1;
}
Fixture
#include <rexo.h>
struct foo_data {
const char *value;
};
RX_SET_UP(foo_set_up)
{
struct foo_data *data;
data = (struct foo_data *)RX_DATA;
data->value = "world!";
return RX_SUCCESS;
}
RX_FIXTURE(foo_fixture, struct foo_data, .set_up = foo_set_up);
RX_TEST_CASE(foo, bar, .fixture = foo_fixture)
{
struct foo_data *data;
data = (struct foo_data *)RX_DATA;
RX_STR_REQUIRE_EQUAL("Hello", data->value);
}
int
main(int argc, const char **argv)
{
return rx_main(0, NULL, argc, argv) == RX_SUCCESS ? 0 : 1;
}
Documentation
https://christophercrouzet.github.io/rexo
Repository
https://github.com/christophercrouzet/rexo
License
Getting Started
Prerequisites
- a C89 (ANSI C) or a C++ compiler.
Note: The automatic registration of tests requires a widespread compiler-specific feature that places data in a given memory section. The compilers currently supported are the GNU compilers (clang, gcc, icc) and MSVC.
Installation
Just the One File
The simplest option to be good to go is to copy the file rexo.h
into
your source directory.
The folder containing the file rexo.h
needs to be recognized as an include
directory by your compiler, which will make Rexo available by using
#include <rexo.h>
in your code.
The Whole Repository
Instead of copying solely the rexo.h
file, you could also grab the whole
code repository, which brings in some additional features such as being able
to link against the library using CMake.
Additionally, Git can be used to add Rexo's repository as a submodule of
your project, thus allowing to conveniently pull updates at any time.
This can be done by running the git submodule
command from within
the directory where Rexo should be added to:
git submodule add git@github.com:christophercrouzet/rexo.git
Note: Linking against the Rexo library using CMake is really simple. If your project Foo has a structure on disk similar to this one:
foo/ ├── deps/ │ └── rexo/ │ └── ... ├── src/ ├── tests/ └── CMakeLists.txt
Then you'd only have to add two lines to your own
CMakeFiles.txt
file:add_subdirectory(deps/rexo) target_link_libraries(foo PRIVATE rexo)
Writing a First Test
To verify that everything is correctly set-up, create a new file containing a simple test like the minimal one from the guides section, compile it, and run it!
Guides
These guides take you over some concrete examples and how-tos covering most of Rexo's features.
Minimal
The easiest way to run a single test is to rely on the framework
by defining a test case with the RX_TEST_CASE
macro,
writing a test in there using one of the assertion macros
available, and finally running that test case by calling
the rx_main
function:
#include <rexo.h>
/*
Define a new test case 'bar' that is part of an implicit test suite 'foo'.
The main purpose of the `RX_TEST_CASE` macro is to declare the test case's
function.
*/
RX_TEST_CASE(foo, bar)
{
/* Run a single test that compares two integer values for equality. */
RX_INT_REQUIRE_EQUAL(2 * 3 * 7, 42);
}
int
main(int argc, const char **argv)
{
/* Execute the main function that runs the test cases found. */
return rx_main(0, NULL, argc, argv) == RX_SUCCESS ? 0 : 1;
}
Note: Setting the
test_cases
argument from therx_main
function toNULL
means that the function is responsible for finding all the test cases that were defined using the framework's automatic registration feature.
Note: In the example above, the
RX_TEST_CASE
macro implicitly creates a test suite named foo.
Runtime Configuration
When using the framework and its automatic registration feature,
configuring test cases is done by passing optional arguments to the
RX_TEST_CASE
and/or
RX_TEST_SUITE
macros.
#include <stdio.h>
#include <rexo.h>
RX_TEST_SUITE(foo, .skip = 1);
/* Inherit the skip option from the test suite 'foo'. */
RX_TEST_CASE(foo, bar)
{
printf("this does NOT print\n");
}
/* Override the skip option to run this specific test case. */
RX_TEST_CASE(foo, baz, .skip = 0)
{
printf("this does print!\n");
}
int
main(int argc, const char **argv)
{
return rx_main(0, NULL, argc, argv) == RX_SUCCESS ? 0 : 1;
}
This example explicitly creates a test suite named foo with the skip
option
enabled.
Any option defined on a test suite is to be inherited by all the test cases of that test suite, unless overidden by a specific test case like here with baz.
Note: For a list of all the runtime options available, see the
rx_test_case_config
struct.
Fixtures
Often times a dataset is required to be prepared in advance, before test cases should be run.
Like with every other xUnit-like frameworks, Rexo allows this do be done using fixtures.
You can define a new fixture through the RX_FIXTURE
macro
with optional set up and tear down functions, then reference this fixture by
setting the fixture
option on the RX_TEST_CASE
macro.
#include <rexo.h>
/* Data structure to use at the core of our fixture. */
struct foo_data {
const char *value;
};
/* Initialize the data structure. Its allocation is handled by Rexo. */
RX_SET_UP(foo_set_up)
{
struct foo_data *data;
/*
The macro `RX_DATA` references our data as a pointer to `void` that
needs to be cast to the correct type before being used.
*/
data = (struct foo_data *)RX_DATA;
/* Initialize it! */
data->value = "world!";
/* Let Rexo know that everything went well. */
return RX_SUCCESS;
}
/* Define the fixture. */
RX_FIXTURE(foo_fixture, struct foo_data, .set_up = foo_set_up);
RX_TEST_CASE(foo, bar, .fixture = foo_fixture)
{
struct foo_data *data;
/* Here again, casting needs to be node before operating on the data. */
data = (struct foo_data *)RX_DATA;
/*
Run a string equality test that will fail since 'Hello' isn't equal
to the value 'world!' that we initialized earlier.
*/
RX_STR_REQUIRE_EQUAL("Hello", data->value);
}
int
main(int argc, const char **argv)
{
return rx_main(0, NULL, argc, argv) == RX_SUCCESS ? 0 : 1;
}
Fixture data can be accessed at any time using the RX_DATA
macro.
Compile-Time Configuration
Configurations that applies globally are sometimes best done once and for all during the compilation step.
Rexo offers a set of macros that, for example, allows redefining the malloc
and assert
functions to use, or to set global flags:
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
/* Define a custom `malloc` function. */
void *
my_malloc(size_t size)
{
printf("something worth %ld bytes is being allocated\n", size);
return malloc(size);
}
/* Override the default `malloc` function used by Rexo with ours. */
#define RX_MALLOC my_malloc
/* Override the default `assert` macro used by Rexo. */
#define RX_ASSERT(x) \
(void)( \
(x) \
|| (printf(__FILE__ ":%d: assertion `" #x "` failed\n", __LINE__), 0) \
|| (abort(), 0))
/* Force Rexo's compatibility with C89. */
#define RX_ENABLE_C89_COMPAT
/* Proceed with including Rexo and writing tests as usual. */
#include <rexo.h>
RX_TEST_CASE(foo, bar)
{
RX_INT_REQUIRE_EQUAL(2 * 3 * 7, 42);
}
int
main(int argc, const char **argv)
{
return rx_main(0, NULL, argc, argv) == RX_SUCCESS ? 0 : 1;
}
Note: The use of
#include <rexo.h>
needs to come after the definition of any compile-time option.
Note: For a list of all the compile-time options available, see the Compile-Time Configuration page.
Explicit Registration
If you would like to not make use of the framework's automatic registration feature, it is possible to register everything explicitly in the same way as most C unit testing frameworks offer:
#include <rexo.h>
struct foo_data {
int value;
};
enum rx_status
foo_set_up(struct rx_context *RX_PARAM_CONTEXT, void *RX_PARAM_DATA)
{
struct foo_data *data;
/* Suppress the ‘defined but not used’ compiler warning. */
(void)RX_PARAM_CONTEXT;
data = (struct foo_data *)RX_DATA;
data->value = 123;
return RX_SUCCESS;
}
void
foo_bar(struct rx_context *RX_PARAM_CONTEXT, void *RX_PARAM_DATA)
{
struct foo_data *data;
data = (struct foo_data *)RX_DATA;
RX_INT_REQUIRE_EQUAL(data->value, 123);
}
static const struct rx_test_case test_cases[] = {
{ /* First test case's definition. */
"foo", /* Name of the test suite. */
"bar", /* Name of the test case. */
foo_bar, /* Function to run. */
{ /* Test case's configuration. */
0, /* Option 'skip'. */
{ /* Option 'fixture'. */
sizeof(struct foo_data), /* Size of the data in bytes. */
{ /* Fixture's configuration. */
foo_set_up, /* Function to initialize the fixture. */
NULL /* Function to clean up the fixture. */
}
}
}
}
};
/* Retrieve the number of test cases to run. */
static const size_t test_case_count
= sizeof(test_cases) / sizeof(test_cases[0]);
int
main(int argc, const char **argv)
{
/* Explicitly pass the test cases to be run. */
return rx_main(test_case_count, test_cases, argc, argv) == RX_SUCCESS
? 0 : 1;
}
Note: See the rx_test_case struct.
Note: The function parameters
struct rx_context *
andvoid *
are required to be respectively named using theRX_PARAM_CONTEXT
andRX_PARAM_DATA
macros.
Note: Alternatively, the data parameter can be defined with its actual type instead of
void *
, thus removing the need to cast the data within the functions. Although this approach requires casting function pointers, which is not standard compliant and hence not used in the implementation of Rexo, in practice the behaviour is well-defined for the common platforms.
Design Rationale
With dozens of well-established unit testing frameworks out there, the question why yet another one? is unavoidable.
The answer is fairly simple—the goal has been to try to further polish the user experience from existing frameworks while maintaining compatibility with both C89 and C++.
Also, it sounded like a great exercise! 😊
It is important to note that nothing is truly novel in Rexo—the main concepts are inspired by existing frameworks like Criterion, novaprova, and others, although their form and implementation have been entirely revised.
Automatic Registration
These two frameworks in particular proved that it was possible to make automatic registration of tests also possible in C, where most C frameworks require a lot of boilerplate to register these.
The advantages that Rexo has in this regard are that:
- it only relies on a widespread compiler-specific feature to get the automatic registration going.
- it works on both Unix and Windows platforms.
- it is implemented in a fairly simple fashion.
Automatic features are always nice to have unless they cannot be overridden. Rexo is designed in such a way that it is possible for the users to skip either parts or the whole automatic registration framework by allowing explicit registration of tests, like more conventional C frameworks offer.
See the automatic registration design page for more details.
Options Definition
Criterion also cleverly demonstrated that it was possible to abuse the languages to make a named argument syntax available for configuring objects.
Rexo took this a step further by allowing test cases not only to inherit options from their parent test suites, but also to tweak only specific fields for each test case.
Since consistency is king in design, the approach was also extended to work with fixtures, thus providing a unified interface to configure all the things.
See the options definition design page for more details.
Automatic Registration
Rexo aims at closing the gap between the C and C++ unit testing frameworks by implementing one feature that is almost a given in C++ frameworks but that is rarely seen in their C counterparts: automatic registration of test suites, test cases, and fixtures.
Common Implementation
The automatic registration of such C++ frameworks is commonly achieved by defining a function to run for each test case and by having these functions added to a global array that can then be iterated over at runtime, as demonstrated in this snippet:
/* C++ only! */
#include <stdio.h>
struct test_case {
const char *name;
void (*run_fn)();
};
struct test_case *test_cases[128];
static int next_test_case_idx = 0;
static int
register_test_case(struct test_case *test_case)
{
test_cases[next_test_case_idx++] = test_case;
return 0;
}
#define TEST_CASE(name) \
static void name(); \
struct test_case test_case_##name = {#name, name}; \
int dummy_##name = register_test_case(&test_case_##name); \
static void name()
TEST_CASE(foo)
{
printf("Hello, world, I'm foo!\n");
}
TEST_CASE(bar)
{
printf("Hello, world, I'm bar!\n");
}
int
main(void)
{
int i;
for (i = 0; i < next_test_case_idx; ++i) {
printf("running `%s`\n", test_cases[i]->name);
test_cases[i]->run_fn();
}
return 0;
}
By the time that this program iterates over the test cases in main
,
the two test cases foo and bar have already been added to the array through
a call to the register_test_case
function.
Enter C Land
Running the previous snippet is possible in C++ because it is able to call any
function before executing main
.
Alas, this doesn't work in C due to additional constraints to the language:
#include <stdio.h>
static int
get_foo()
{
return 123;
}
static int foo = get_foo(); /* only allowed in C++ :( */
int
main(void)
{
printf("%d\n", foo);
return 0;
}
Without any standard way of providing an automatic registration framework in C, most minimalist samples from conventional frameworks look like something along these lines:
#include <stdlib.h>
#include <unittestframework.h>
struct my_string {
int length;
const char *value;
};
static int
set_up(void **data)
{
struct my_string *s;
s = (struct my_string *)malloc(sizeof *s);
if (s == NULL) {
return 1;
}
s->length = 13;
s->value = "hello, world!";
*data = (void *)s;
return 0;
}
static void
tear_down(void *data)
{
free(data);
}
static void
test_foo(void *data)
{
struct my_string *s;
s = (struct my_string *)data;
ASSERT_INT_EQUAL(s->length, 13);
}
static void
test_bar(void *data)
{
struct my_string *s;
s = (struct my_string *)data;
ASSERT_STR_EQUAL(s, "hello, world!");
}
static const struct test_cases cases[] = {
{"foo", test_foo},
{"bar", test_bar},
};
static const struct test_suite suites[]= {
{"suite", sizeof cases / sizeof cases[0], cases, set_up, tear_down},
};
int
main(void)
{
return run(sizeof suites / sizeof suites[0], suites);
}
Not only is this a lot of boilerplate but it is also fairly error-prone since it is easy to add new tests while forgetting to register them further down the line.
Making It Work
One solution for C is to use a compiler extension that is expected to be available for all compilers in a form or another and that provides custom memory sections.
Custom memory sections allow grouping related objects in the same memory space and to then iterate over them at runtime.
As an example, this is the gist of how such an implementation might look like for the GNU-compliant compilers:
#include <stdio.h>
struct test_case {
const char *name;
};
const struct test_case test_case_foo = {"foo"};
const struct test_case test_case_bar = {"bar"};
const struct test_case *__start_rxcases;
const struct test_case *__stop_rxcases;
__attribute__((section("rxcases")))
const struct test_case *test_case_foo_ptr = &test_case_foo;
__attribute__((section("rxcases")))
const struct test_case *test_case_bar_ptr = &test_case_bar;
int
main(void)
{
const struct test_case **it;
for (it = &__start_rxcases; it < &__stop_rxcases; ++it) {
if (*it != NULL) {
printf("found `%s`\n", (*it)->name);
}
}
return 0;
}
And here's the equivalent implementation, adapted to MSVC:
#include <stdio.h>
struct test_case {
const char *name;
};
const struct test_case test_case_foo = {"foo"};
const struct test_case test_case_bar = {"bar"};
__pragma(section("rxcases$a", read))
__pragma(section("rxcases$b", read))
__pragma(section("rxcases$c", read))
__declspec(allocate("rxcases$a"))
const struct test_case *section_begin = NULL;
__declspec(allocate("rxcases$c"))
const struct test_case *section_end = NULL;
__declspec(allocate("rxcases$b"))
const struct test_case *test_case_foo_ptr = &test_case_foo;
__declspec(allocate("rxcases$b"))
const struct test_case *test_case_bar_ptr = &test_case_bar;
int
main(void)
{
const struct test_case **it;
for (it = §ion_begin + 1; it < §ion_end; ++it) {
if (*it != NULL) {
printf("found `%s`\n", (*it)->name);
}
}
return 0;
}
Options Definition
To build an expressive and intuitive framework, it was important to put a system in place that makes the experience of configuring the different objects as frictionless as possible.
Named Arguments Syntax
When it comes to setting a bunch of options at once, the most user-friendly
approach is usually to define a struct where 0
(or NULL
) corresponds to
the default value for each field, and to let the users initialize instances
of that struct by only setting the fields that need to have their value changed
from the defaults through.
This is exactly what the designated initializer syntax introduced with C99 offers:
struct config {
const char *foo;
int bar;
double baz;
};
/* Only setting the bar field leaves foo and baz to 0. */
struct config my_config = {.bar = 123};
Despite Rexo's intention of supporting C89 and C++ when both languages are not compatible with the designated initializer syntax, it was still a strong design goal to provide a similar approach for setting options.
This is done by using variadic macros where the optional arguments start with a dot character:
MY_MACRO(arg_1, arg_2, .option_1 = 123, .option_2 = "abc");
Options Sharing
This designated initialized syntax can be used to set-up individual test cases but, to avoid redundancy, it is possible to group test cases with a similar configuration into a same test suite, configure only that test suite, and have all of its test cases automatically inherit the configuration.
Not only that but it is still possible to override only specific options for individual test cases:
RX_TEST_SUITE(my_test_suite, .foo = 123, .bar = "abc");
RX_TEST_CASE(my_test_suite, my_test_case, .bar = "def", .baz = 1.23)
{
/*
foo is 123.
bar is "def".
baz is 1.23
*/
}
Variadic Macros Fallback
Since C89 and C++98 don't support variadic macros, an alternative set of macros is provided, each suffixed with a digit representing the number of options to pass.
For example, the equivalent of the previous examples become:
MY_MACRO(arg_1, arg_2)
MY_MACRO_1(arg_1, arg_2, .option_2 = "abc");
MY_MACRO_2(arg_1, arg_2, .option_1 = 123, .option_2 = "abc");
RX_TEST_SUITE_2(my_test_suite, .foo = 123, .bar = "abc");
RX_TEST_CASE_2(my_test_suite, my_test_case, .bar = "def", .baz = 1.23)
{
/* ... */
}
API Reference
As a single-header file library, all of Rexo's public API and implementation is available from within a single include:
#include <rexo.h>
Framework
The framework defines the go-to approach when wanting to use the mechanism that automatically registers test suites, test cases, and fixtures.
RX_SET_UP
Defines the initialization function of a fixture.
#define RX_SET_UP(id)
The name for this function needs to be passed to the id
parameter and can then
referenced through the set_up
option available as part of
the RX_FIXTURE
macro.
RX_TEAR_DOWN
Defines the clean-up function of a fixture.
#define RX_TEAR_DOWN(id)
The name for this function needs to be passed to the id
parameter and can then
be referenced through the tear_down
option available as part of
the RX_FIXTURE
macro.
RX_FIXTURE
Defines a fixture.
#define RX_FIXTURE(id, type, ...)
The name for this fixture needs to be passed to the id
parameter and can then
be referenced through the configuration of
the RX_TEST_SUITE
and
the RX_TEST_CASE
macros.
For a list of all the options available through the variadic parameter, see
the rx_fixture_config
struct.
If the fixture defines some data to be used by its test cases, its type needs
to be passed to the type
parameter. Otherwise, the alternative
macro RX_VOID_FIXTURE
should be used instead.
RX_VOID_FIXTURE
Defines a fixture without any user data.
#define RX_VOID_FIXTURE(id, ...)
The name for this fixture needs to be passed to the id
parameter and can then
be referenced through the configuration of
the RX_TEST_SUITE
and
the RX_TEST_CASE
macros.
RX_TEST_SUITE
Defines a test suite.
#define RX_TEST_SUITE(id, ...)
The name for this suite needs to be passed to the id
parameter and can then be
referenced by the RX_TEST_CASE
macro.
For a list of all the options available through the variadic parameter, see
the rx_test_case_config
struct.
RX_TEST_CASE
Defines a test case function.
#define RX_TEST_CASE(suite_id, id, ...)
For a list of all the options available through the variadic parameter, see
the rx_test_case_config
struct.
Assertion Macros
Each assertion macro comes in two variants that define a different behaviour when an assertion fails:
REQUIRE
: reports a fatal failure and aborts the current test case.CHECK
: reports a nonfatal failure and keeps running other tests found within the same test case.
Note: When in C89 compatibility mode, variadic macro arguments are not available as part of the language. See the associated gotcha.
Generic Assertions
#define RX_REQUIRE(condition)
#define RX_REQUIRE_MSG(condition, msg, ...)
#define RX_CHECK(condition)
#define RX_CHECK_MSG(condition, msg, ...)
Ultimately, these two assertion macros could be enough to express all possible tests but it is recommended to use the more specialized assertions available.
Boolean Assertions
#define RX_BOOL_REQUIRE_TRUE(condition)
#define RX_BOOL_REQUIRE_TRUE_MSG(condition, msg, ...)
#define RX_BOOL_CHECK_TRUE(condition)
#define RX_BOOL_CHECK_TRUE_MSG(condition, msg, ...)
#define RX_BOOL_REQUIRE_FALSE(condition)
#define RX_BOOL_REQUIRE_FALSE_MSG(condition, msg, ...)
#define RX_BOOL_CHECK_FALSE(condition)
#define RX_BOOL_CHECK_FALSE_MSG(condition, msg, ...)
Integer Assertions
#define RX_INT_REQUIRE_EQUAL(x1, x2)
#define RX_INT_REQUIRE_EQUAL_MSG(x1, x2, msg, ...)
#define RX_INT_CHECK_EQUAL(x1, x2)
#define RX_INT_CHECK_EQUAL_MSG(x1, x2, msg, ...)
#define RX_INT_REQUIRE_NOT_EQUAL(x1, x2)
#define RX_INT_REQUIRE_NOT_EQUAL_MSG(x1, x2, msg, ...)
#define RX_INT_CHECK_NOT_EQUAL(x1, x2)
#define RX_INT_CHECK_NOT_EQUAL_MSG(x1, x2, msg, ...)
#define RX_INT_REQUIRE_GREATER(x1, x2)
#define RX_INT_REQUIRE_GREATER_MSG(x1, x2, msg, ...)
#define RX_INT_CHECK_GREATER(x1, x2)
#define RX_INT_CHECK_GREATER_MSG(x1, x2, msg, ...)
#define RX_INT_REQUIRE_LESSER(x1, x2)
#define RX_INT_REQUIRE_LESSER_MSG(x1, x2, msg, ...)
#define RX_INT_CHECK_LESSER(x1, x2)
#define RX_INT_CHECK_LESSER_MSG(x1, x2, msg, ...)
#define RX_INT_REQUIRE_GREATER_OR_EQUAL(x1, x2)
#define RX_INT_REQUIRE_GREATER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_INT_CHECK_GREATER_OR_EQUAL(x1, x2)
#define RX_INT_CHECK_GREATER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_INT_REQUIRE_LESSER_OR_EQUAL(x1, x2)
#define RX_INT_REQUIRE_LESSER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_INT_CHECK_LESSER_OR_EQUAL(x1, x2)
#define RX_INT_CHECK_LESSER_OR_EQUAL_MSG(x1, x2, msg, ...)
Unsigned Integer Assertions
#define RX_UINT_REQUIRE_EQUAL(x1, x2)
#define RX_UINT_REQUIRE_EQUAL_MSG(x1, x2, msg, ...)
#define RX_UINT_CHECK_EQUAL(x1, x2)
#define RX_UINT_CHECK_EQUAL_MSG(x1, x2, msg, ...)
#define RX_UINT_REQUIRE_NOT_EQUAL(x1, x2)
#define RX_UINT_REQUIRE_NOT_EQUAL_MSG(x1, x2, msg, ...)
#define RX_UINT_CHECK_NOT_EQUAL(x1, x2)
#define RX_UINT_CHECK_NOT_EQUAL_MSG(x1, x2, msg, ...)
#define RX_UINT_REQUIRE_GREATER(x1, x2)
#define RX_UINT_REQUIRE_GREATER_MSG(x1, x2, msg, ...)
#define RX_UINT_CHECK_GREATER(x1, x2)
#define RX_UINT_CHECK_GREATER_MSG(x1, x2, msg, ...)
#define RX_UINT_REQUIRE_LESSER(x1, x2)
#define RX_UINT_REQUIRE_LESSER_MSG(x1, x2, msg, ...)
#define RX_UINT_CHECK_LESSER(x1, x2)
#define RX_UINT_CHECK_LESSER_MSG(x1, x2, msg, ...)
#define RX_UINT_REQUIRE_GREATER_OR_EQUAL(x1, x2)
#define RX_UINT_REQUIRE_GREATER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_UINT_CHECK_GREATER_OR_EQUAL(x1, x2)
#define RX_UINT_CHECK_GREATER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_UINT_REQUIRE_LESSER_OR_EQUAL(x1, x2)
#define RX_UINT_REQUIRE_LESSER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_UINT_CHECK_LESSER_OR_EQUAL(x1, x2)
#define RX_UINT_CHECK_LESSER_OR_EQUAL_MSG(x1, x2, msg, ...)
Floating-Point Assertions
#define RX_REAL_REQUIRE_EQUAL(x1, x2)
#define RX_REAL_REQUIRE_EQUAL_MSG(x1, x2, msg, ...)
#define RX_REAL_CHECK_EQUAL(x1, x2)
#define RX_REAL_CHECK_EQUAL_MSG(x1, x2, msg, ...)
#define RX_REAL_REQUIRE_NOT_EQUAL(x1, x2)
#define RX_REAL_REQUIRE_NOT_EQUAL_MSG(x1, x2, msg, ...)
#define RX_REAL_CHECK_NOT_EQUAL(x1, x2)
#define RX_REAL_CHECK_NOT_EQUAL_MSG(x1, x2, msg, ...)
#define RX_REAL_REQUIRE_GREATER(x1, x2)
#define RX_REAL_REQUIRE_GREATER_MSG(x1, x2, msg, ...)
#define RX_REAL_CHECK_GREATER(x1, x2)
#define RX_REAL_CHECK_GREATER_MSG(x1, x2, msg, ...)
#define RX_REAL_REQUIRE_LESSER(x1, x2)
#define RX_REAL_REQUIRE_LESSER_MSG(x1, x2, msg, ...)
#define RX_REAL_CHECK_LESSER(x1, x2)
#define RX_REAL_CHECK_LESSER_MSG(x1, x2, msg, ...)
#define RX_REAL_REQUIRE_GREATER_OR_EQUAL(x1, x2)
#define RX_REAL_REQUIRE_GREATER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_REAL_CHECK_GREATER_OR_EQUAL(x1, x2)
#define RX_REAL_CHECK_GREATER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_REAL_REQUIRE_LESSER_OR_EQUAL(x1, x2)
#define RX_REAL_REQUIRE_LESSER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_REAL_CHECK_LESSER_OR_EQUAL(x1, x2)
#define RX_REAL_CHECK_LESSER_OR_EQUAL_MSG(x1, x2, msg, ...)
#define RX_REAL_REQUIRE_FUZZY_EQUAL(x1, x2, tol)
#define RX_REAL_REQUIRE_FUZZY_EQUAL_MSG(x1, x2, tol, msg, ...)
#define RX_REAL_CHECK_FUZZY_EQUAL(x1, x2, tol)
#define RX_REAL_CHECK_FUZZY_EQUAL_MSG(x1, x2, tol, msg, ...)
#define RX_REAL_REQUIRE_FUZZY_NOT_EQUAL(x1, x2, tol)
#define RX_REAL_REQUIRE_FUZZY_NOT_EQUAL_MSG(x1, x2, tol, msg, ...)
#define RX_REAL_CHECK_FUZZY_NOT_EQUAL(x1, x2, tol)
#define RX_REAL_CHECK_FUZZY_NOT_EQUAL_MSG(x1, x2, tol, msg, ...)
Note: The
EQUAL
andNOT_EQUAL
comparisons perform strict equality checks, which is usually not what you would want to do in the case of floating-point due to precision errors.Therefore, each of these two assertions come with a corresponding
FUZZY
alternative that takes a tolerance parameter and performs a relative comparison followed by an absolute one, similar to what is described in Bruce Dawson's article Comparing Floating Point Numbers, 2012 Edition.The fuzzy comparison test is described with the following logic:
int are_equal_fuzzy(float a, float b, float tol) { float diff; diff = fabs(a - b); if (diff <= tol) { return 1; } a = fabs(a); b = fabs(b); return diff <= (a > b ? a : b) * tol; }
String Assertions
#define RX_STR_REQUIRE_EQUAL(s1, s2)
#define RX_STR_REQUIRE_EQUAL_MSG(s1, s2, msg, ...)
#define RX_STR_CHECK_EQUAL(s1, s2)
#define RX_STR_CHECK_EQUAL_MSG(s1, s2, msg, ...)
#define RX_STR_REQUIRE_NOT_EQUAL(s1, s2)
#define RX_STR_REQUIRE_NOT_EQUAL_MSG(s1, s2, msg, ...)
#define RX_STR_CHECK_NOT_EQUAL(s1, s2)
#define RX_STR_CHECK_NOT_EQUAL_MSG(s1, s2, msg, ...)
#define RX_STR_REQUIRE_EQUAL_NO_CASE(s1, s2)
#define RX_STR_REQUIRE_EQUAL_NO_CASE_MSG(s1, s2, msg, ...)
#define RX_STR_CHECK_EQUAL_NO_CASE(s1, s2)
#define RX_STR_CHECK_EQUAL_NO_CASE_MSG(s1, s2, msg, ...)
#define RX_STR_REQUIRE_NOT_EQUAL_NO_CASE(s1, s2)
#define RX_STR_REQUIRE_NOT_EQUAL_NO_CASE_MSG(s1, s2, msg, ...)
#define RX_STR_CHECK_NOT_EQUAL_NO_CASE(s1, s2)
#define RX_STR_CHECK_NOT_EQUAL_NO_CASE_MSG(s1, s2, msg, ...)
Pointer Assertions
#define RX_PTR_REQUIRE_EQUAL(x1, x2)
#define RX_PTR_REQUIRE_EQUAL_MSG(x1, x2, msg, ...)
#define RX_PTR_CHECK_EQUAL(x1, x2)
#define RX_PTR_CHECK_EQUAL_MSG(x1, x2, msg, ...)
#define RX_PTR_REQUIRE_NOT_EQUAL(x1, x2)
#define RX_PTR_REQUIRE_NOT_EQUAL_MSG(x1, x2, msg, ...)
#define RX_PTR_CHECK_NOT_EQUAL(x1, x2)
#define RX_PTR_CHECK_NOT_EQUAL_MSG(x1, x2, msg, ...)
#define RX_PTR_REQUIRE_ALIGNED(x, alignment)
#define RX_PTR_REQUIRE_ALIGNED_MSG(x, alignment, msg, ...)
Runner
High-level API to run all the unit tests.
rx_main
Runs the given tests.
enum rx_status
rx_main(size_t test_case_count,
const struct rx_test_case *test_cases,
int argc,
const char * const *argv)
The rx_main
function can be seen as composing all the orthogonal calls into
a single higher level API for ease of use and is likely to be the only one
function needing to ever be called by most users.
If the test_cases
argument is NULL
, then the tests found through
the framework's automatic registration feature are used.
This function is implemented using the lower level API described in
building blocks. Use these directly instead of rx_main
if you'd like to further customize the process.
Building Blocks
Core of Rexo—data structures, required macros, and lower level API calls.
Most of these are abstracted away when using the runner, the framework's automatic registration feature, and the assertion macros.
Enumerators
rx_status
Return codes.
enum rx_status {
RX_SUCCESS = 0,
RX_ERROR = -1,
RX_ERROR_ALLOCATION = -2,
RX_ERROR_MAX_SIZE_EXCEEDED = -3
}
Error codes come in different categories that all evaluate to negative numbers.
rx_severity
Severity levels for test failures.
enum rx_severity { RX_NONFATAL = 0, RX_FATAL = 1 }
Nonfatal failures arise from RX_CHECK*
assertions while RX_REQUIRE*
trigger
fatal failures.
See also the assertion macros.
rx_log_level
Logging level.
enum rx_log_level {
RX_LOG_LEVEL_NONE = 0,
RX_LOG_LEVEL_FATAL = 1,
RX_LOG_LEVEL_ERROR = 3,
RX_LOG_LEVEL_WARNING = 4,
RX_LOG_LEVEL_INFO = 5,
RX_LOG_LEVEL_DEBUG = 6,
RX_LOG_LEVEL_ALL = RX_LOG_LEVEL_DEBUG
};
The purpose of each level is defined as follows:
level | description | example |
---|---|---|
fatal | critical failure that causes premature termination of the application | lack of disk space, data corruption |
error | failure that doesn't require the application to be prematurely terminated | unable to open a file, allocation failure |
warning | situation that is not ideal but that is not an actual failure per se | use of a deprecated API, poor use of an API |
info | event of general interest corresponding to normal application behaviour | addition of a database entry, statistics |
debug | detailed information to help maintainers troubleshooting problems | allocation size, state of data |
trace | control flow of the application to help maintainers pinpointing problems | entry/exit of functions, dump of argument values |
Function Pointers
rx_set_up_fn
Function part of the fixture feature, to be called before the function defining the tests is run.
typedef enum rx_status (*rx_set_up_fn)(struct rx_context *, void *)
The struct rx_context *
parameter is reserved for the implementation and
shouldn't be directly accessed by the users.
The void *
parameter is used to output any data initialized within
this function. The pointer can be accessed within the function's definition
using the RX_DATA
macro.
When explicitly registering tests, the macros
RX_PARAM_CONTEXT
and RX_PARAM_DATA
need to be used to define
the parameter names.
The function is expected to return an error code if something went wrong,
or RX_SUCCESS
otherwise.
rx_tear_down_fn
Function part of the fixture feature, to be called after the function defining the tests is run.
typedef void (*rx_tear_down_fn)(struct rx_context *, void *)
The struct rx_context *
parameter is reserved for the implementation and
shouldn't be directly accessed by the users.
The void *
parameter is used to output any data initialized within
this function. The pointer can be accessed within the function's definition
using the RX_DATA
macro.
When explicitly registering tests, the macros
RX_PARAM_CONTEXT
and RX_PARAM_DATA
need to be used to define
the parameter names.
rx_run_fn
Function defining the tests to run.
typedef void (*rx_run_fn)(struct rx_context *, void *)
The struct rx_context *
parameter is reserved for the implementation and
shouldn't be directly accessed by the users.
The void *
parameter is used to output any data initialized within
this function. The pointer can be accessed within the function's definition
using the RX_DATA
macro.
When explicitly registering tests, the macros
RX_PARAM_CONTEXT
and RX_PARAM_DATA
need to be used to define
the parameter names.
Parameters
RX_PARAM_CONTEXT
Refers to the name expected for the context parameter.
#define RX_PARAM_CONTEXT
The context parameter is used for the functions rx_set_up_fn, rx_tear_down_fn, and rx_run_fn.
It is not to be directly used unless when
explicitly registering tests. In this case,
the macro RX_PARAM_CONTEXT
is required to name
the struct rx_context *
parameter.
RX_PARAM_DATA
Refers to the name expected for the data parameter.
#define RX_PARAM_DATA
The context parameter is used for the functions rx_set_up_fn, rx_tear_down_fn, and rx_run_fn.
It is not to be directly used unless when
explicitly registering tests. In this case,
the macro RX_PARAM_DATA
is required to name the void *
parameter.
Data Accessors
RX_DATA
Access the data's pointer.
#define RX_DATA
This macro can be used within the definitions of the functions rx_set_up_fn, rx_tear_down_fn, and rx_run_fn.
Types
rx_uint32
Type for 32-bit unsigned integers.
typedef TYPE rx_uint32;
The type is determined by the value of
the RX_UINT32_TYPE
macro. If the macro isn't set,
unsigned int
is used, which fits the common data models, that is ILP32
(most recent 32-bit systems), LP64 (Unix-like systems), and LLP64 (Windows).
rx_uint64
Type for 64-bit unsigned integers.
typedef TYPE rx_uint64;
The type is determined by the value of
the RX_UINT64_TYPE
macro. If the macro isn't set,
unsigned long long
is used, which fits the common data models, that is ILP32
(most recent 32-bit systems), LP64 (Unix-like systems), and LLP64 (Windows).
rx_size
Type to use in place of size_t
.
typedef TYPE rx_size;
The type is determined by the value of the RX_SIZE_TYPE
macro. If the macro isn't set, either rx_uint32
or rx_uint64
is used, depending on whether the environment
is running on a 32-bit or 64-bit platform.
Structures
rx_test_case_config
Configuration object to apply to a test case.
struct rx_test_case_config {
int skip;
struct rx_fixture fixture;
}
In the event where a test case should be skipped by the runner,
the skip
option can be used.
Fixtures are defined through the fixture
option, see
the rx_fixture
struct.
Filling the struct with the value 0
sets all the members to
their default values.
rx_fixture_config
Configuration object to apply to a fixture.
struct rx_fixture_config {
rx_set_up_fn set_up;
rx_tear_down_fn tear_down;
}
The set_up
and tear_down
options respectively define
the rx_set_up_fn and
the rx_tear_down_fn functions.
Filling the struct with the value 0
sets all the members to
their default values.
rx_fixture
Fixture defining data type size and function pointers to run before and after the test is run.
struct rx_fixture {
rx_size size;
struct rx_fixture_config config;
};
Any configuration can be set through the config
option. See
the rx_fixture_config
struct.
rx_test_case
Definition of a single test case.
struct rx_test_case {
const char *suite_name;
const char *name;
rx_run_fn run;
struct rx_test_case_config config;
};
The run
function pointer needs to point to the function that contains
the tests for the test case. See the rx_run_fn
function.
Any configuration can be set through the config
option. See
the rx_test_case_config
struct.
rx_failure
Information related to a test that failed.
struct rx_failure {
const char *file;
int line;
enum rx_severity severity;
const char *msg;
const char *diagnostic_msg;
}
rx_summary
Report from running a test case.
struct rx_summary {
const struct rx_test_case *test_case;
int skipped;
const char *error;
rx_size assessed_count;
rx_size failure_count;
struct rx_failure *failures;
rx_uint64 elapsed;
}
rx_context
Opaque data required internally by Rexo.
struct rx_context
Functions
rx_abort
Aborts the execution of the test case being currently run.
void
rx_abort(struct rx_context *context)
rx_handle_test_result
Handles the result of a single test.
enum rx_status
rx_handle_test_result(struct rx_context *context,
int result,
const char *file,
int line,
enum rx_severity severity,
const char *failure_msg,
const char *diagnostic_msg)
This records the result and any error or diagnostic messages related to it.
rx_summary_initialize
Initializes a summary.
enum rx_status
rx_summary_initialize(struct rx_summary *summary,
const struct rx_test_case *test_case)
The struct rx_summary
must be already manually
allocated beforehand.
rx_summary_terminate
Terminates a summary.
void
rx_summary_terminate(struct rx_summary *summary)
The struct rx_summary
must be manually freed
afterwards, if needed.
rx_summary_print
Prints a summary covering the result of running a single test case.
void
rx_summary_print(const struct rx_summary *summary)
The summary is printed out to stderr
as it is not intended for further
processing, but to only show the progress of each test case as the results
come in.
rx_test_case_run
Runs a single test case.
enum rx_status
rx_test_case_run(struct rx_summary *summary,
const struct rx_test_case *test_case)
The run
function set for the given test case is being executed with
the results are being stored in the summary
argument.
rx_enumerate_test_cases
Enumerates the test cases automatically registered.
void
rx_enumerate_test_cases(size_t *test_case_count,
struct rx_test_case *test_cases)
If test_cases
is NULL
, then the number of test cases available is returned
in test_case_count
. Otherwise, test_case_count
must point to a variable set
by the user to the number of elements in the test_cases
array, and on return
the variable is overwritten with the number of objects actually written to
test_cases
.
If test_case_count
is less than the number of test cases available,
at most test_case_count
objects will be written.
Constants
Versioning
RX_MAJOR_VERSION
Major version of Rexo.
#define RX_MAJOR_VERSION
RX_MINOR_VERSION
Minor version of Rexo.
#define RX_MINOR_VERSION
RX_PATCH_VERSION
Patch version of Rexo.
#define RX_PATCH_VERSION
RX_VERSION
Version of Rexo as a single integer.
#define RX_VERSION
The major, minor, and patch versions are combined into RX_VERSION
as a single
integer value that can be used for comparison purposes.
Compile-Time Configuration
One strength of single-file header libraries is that they allow to easily
change some behaviour at compile-time by allowing to redefine some macros
before including rexo.h
.
This page lists all such macros that can be overridden if desired.
Note: See the guide for a detailed example.
Flag Macros
RX_ENABLE_EXTERNAL_LINKING
Sets the storage class qualifier of the public functions to extern
.
#define RX_ENABLE_EXTERNAL_LINKING
If not set, the public functions are defined with the static
qualifier
instead.
RX_ENABLE_NPRINTF
Enables the usage of the standard *nprintf
functions.
#define RX_ENABLE_NPRINTF
If neither the RX_ENABLE_NPRINTF
nor the RX_DISABLE_NPRINTF
macros are explicitly defined, the standard *nprintf
functions are used
depending on the language (C or C++) and its version.
RX_DISABLE_NPRINTF
Disables the usage of the standard *nprintf
functions.
#define RX_DISABLE_NPRINTF
If neither the RX_ENABLE_NPRINTF
nor the RX_DISABLE_NPRINTF
macros are explicitly defined, the standard *nprintf
functions are used
depending on the language (C or C++) and its version.
This macro takes precedence over
the [RX_ENABLE_NPRINTF
][macro-rx_enable-nprintf] macro.
RX_ENABLE_VARIADIC_MACROS
Enables the usage of variadic macros.
#define RX_ENABLE_VARIADIC_MACROS
If neither the RX_ENABLE_VARIADIC_MACROS
nor the RX_DISABLE_VARIADIC_MACROS
macros are explicitly defined, variadic macros are used depending on
the language (C or C++) and its version.
See the variadic macros gotcha.
RX_DISABLE_VARIADIC_MACROS
Disables the usage of variadic macros.
#define RX_DISABLE_VARIADIC_MACROS
If neither the RX_ENABLE_VARIADIC_MACROS
nor the RX_DISABLE_VARIADIC_MACROS
macros are explicitly defined, variadic macros are used depending on
the language (C or C++) and its version.
This macro takes precedence over
the [RX_ENABLE_VARIADIC_MACROS
][macro-rx_enable-variadic-macros] macro.
See the variadic macros gotcha.
RX_ENABLE_DEBUGGING
Enables the debugging mode.
#define RX_ENABLE_DEBUGGING
If neither the RX_ENABLE_DEBUGGING
nor the RX_DISABLE_DEBUGGING
macros are
explicitly defined, the debugging mode is enabled depending on the values of
the DEBUG
and NDEBUG
macros.
This takes precedence over
the RX_DISABLE_DEBUGGING
macro.
RX_DISABLE_DEBUGGING
Disables the debugging mode.
#define RX_DISABLE_DEBUGGING
If neither the RX_ENABLE_DEBUGGING
nor the RX_DISABLE_DEBUGGING
macros are
explicitly defined, the debugging mode is enabled depending on the values of
the DEBUG
and NDEBUG
macros.
RX_DISABLE_LOGGING
Disables logging.
#define RX_DISABLE_LOGGING
It suppresses any log that is intended to be displayed in the shell.
RX_SET_LOGGING_LEVEL
Defines the logging level.
#define RX_SET_LOGGING_LEVEL_NONE
#define RX_SET_LOGGING_LEVEL_FATAL
#define RX_SET_LOGGING_LEVEL_ERROR
#define RX_SET_LOGGING_LEVEL_WARNING
#define RX_SET_LOGGING_LEVEL_INFO
#define RX_SET_LOGGING_LEVEL_DEBUG
#define RX_SET_LOGGING_LEVEL_ALL
The logging level can be set to only output logs of a level greater or equal to
the given one, e.g.: if the macro RX_SET_LOGGING_LEVEL_INFO
is set,
then logs of the info, warning, and error levels are printed out.
See the enum rx_log_level
enumerator for a description
of each level.
RX_DISABLE_LOG_STYLING
Disables the styling of logs in the shell.
#define RX_DISABLE_LOG_STYLING
If the RX_LOG
macro hasn't been redefined, its default
implementation adds colours to the logs outputted to Unix shells in order to
visually highlight some bits of information and to help with readability.
The RX_DISABLE_LOG_STYLING
macro ensures that no styling is ever applied to
the output logs.
RX_DISABLE_TEST_DISCOVERY
Disables the automatic discovery of tests.
#define RX_DISABLE_TEST_DISCOVERY
Type Macros
RX_UINT32_TYPE
Override the unsigned integer 32-bit type.
#define RX_UINT32_TYPE
See the rx_uint32
type for more info.
RX_UINT64_TYPE
Override the unsigned integer 64-bit type.
#define RX_UINT64_TYPE
See the rx_uint64
type for more info.
RX_SIZE_TYPE
Override the size_t
type.
#define RX_SIZE_TYPE
See the rx_size
type for more info.
Function-Like Macros
RX_ASSERT
Assertion macro.
#define RX_ASSERT(condition)
Its purpose is to output an error when the argument condition
evaluates
to zero. The default implementation runs this check only when the NDEBUG
macro
is set, otherwise it does nothing.
If not redefined, the standard header file assert.h
is included.
RX_MALLOC
Allocation macro.
#define RX_MALLOC(size)
Allocates the given size
in bytes of uninitialized storage.
It returns a pointer to the allocated block of memory, or NULL
if the operation failed.
If not redefined, the standard header file stdlib.h
is included.
RX_REALLOC
Reallocation macro.
#define RX_REALLOC(ptr, size)
Reallocates the given block of memory pointed by ptr
. It is being done either
by expanding/shrinking the existing block of memory, if possible, or allocating
a new block of memory otherwise, before copying the data over and freeing
the previous block.
It returns a pointer to the reallocated block of memory.
If the operation failed, NULL
is returned and the original block of memory
isn't freed.
If not redefined, the standard header file stdlib.h
is included.
RX_FREE
Deallocation macro.
#define RX_FREE(ptr)
Deallocates the given block of memory pointed by ptr
, that was previously
allocated through RX_MALLOC
or RX_REALLOC
.
If not redefined, the standard header file stdlib.h
is included.
RX_LOG
Logging macro.
#define RX_LOG(level, format, ...)
Prints a message to stderr
with a level defined through
enum rx_log_level
.
The content of the message is defined through a combination of the format
argument and the variadic arguments, in a similar fashion to the standard
printf()
function.
Gotchas
Variadic Macros
Since variadic macros are not available as part of the C89 and C++98 specifications, having the variadic macros flag disabled assumes no support for variadic macros and defines an alternative set of macros for each macro that would otherwise accept variadic arguments.
In other words, a macro documented as RX_DO_DOMETHING(fixed_arg, ...)
translates in fact to set of macros suffixed with the number of variadic
arguments such as:
RX_DO_DOMETHING(fixed_arg)
.RX_DO_DOMETHING_1(fixed_arg, var_arg_1)
.RX_DO_DOMETHING_2(fixed_arg, var_arg_1, var_arg_2)
.- ...
RX_DO_DOMETHING_N(fixed_arg, var_arg_1, var_arg_2, ..., var_arg_n)
.
Note: The set of macros suffixed with a number is available whether the variadic macros flag is enabled or not in order to provide a compatibility layer between the two modes if needed. The only difference comes from the base macro
RX_DO_DOMETHING
being defined with variadic arguments or only fixed arguments otherwise.
Examples of such macros can be found in the framework and as part of the assertion macros.