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

Unlicense.

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 the rx_main function to NULL 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 * and void * are required to be respectively named using the RX_PARAM_CONTEXT and RX_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 = &section_begin + 1; it < &section_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 and NOT_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:

leveldescriptionexample
fatalcritical failure that causes premature termination of the applicationlack of disk space, data corruption
errorfailure that doesn't require the application to be prematurely terminatedunable to open a file, allocation failure
warningsituation that is not ideal but that is not an actual failure per seuse of a deprecated API, poor use of an API
infoevent of general interest corresponding to normal application behaviouraddition of a database entry, statistics
debugdetailed information to help maintainers troubleshooting problemsallocation size, state of data
tracecontrol flow of the application to help maintainers pinpointing problemsentry/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 function pointers to run before and after the function defining the test is run.

struct rx_fixture {
    rx_set_up_fn set_up;
    rx_tear_down_fn tear_down;
};

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.

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.