![]() |
Arene Base
Fundamental Utilities For Safety Critical C++
|
Arene Base provides a unit testing support library, compatible with gtest, which provides STATIC_ASSERT_XXX macros which mirror their non-static counterparts in a 1:1 manner. This allows writing compile-time tests which look and feel like their runtime counterparts, with a couple of known limitations. These macros are only for use within test code; non-test code should continue to use static assertions.
When writing C++ code which can be evaluated at compile time such as a type trait, or constexpr function/method, it is often desirable to write unit test cases which:
Such tests are particularly important for constexpr annotated APIs: It is not a compilation error to use constexpr-incompatible logic in a constexpr annotated function; without a compile-time evaluated test developers might accidentally write logic they believe is constexpr but which is not. GoogleTest (hereafter gtest) is the standard unit testing framework in use throughout the Arene ecosystem, as well as in many non-Arene projects. Unfortunately, gtest lacks any official support for compile-time test cases: all of its ASSERT_XXX macros are evaluated exclusively at runtime, and it has no alternative facilities for writing static test cases. There are other major unit testing frameworks such as Catch2 which do support static assertions, but changing testing frameworks to get such functionality is inconvenient for most projects.
As a result, most developers write such test cases by implementing their own assertions using C++ static_assert declarations. A real example from arene_base:
There are several drawbacks of this approach:
ASSERT_XXX.static_assert and once with a regular gtest ASSERT_XXX, to properly measure coverage.The following set of assertions are provided by the framework:
| Assertion | Category | Meaning |
|---|---|---|
STATIC_ASSERT_EQ(lhs, rhs) | Equality | lhs == rhs |
STATIC_ASSERT_NE(lhs, rhs) | Equality | lhs != rhs |
STATIC_ASSERT_LT(lhs, rhs) | Relational | lhs < rhs |
STATIC_ASSERT_LE(lhs, rhs) | Relational | lhs <= rhs |
STATIC_ASSERT_GT(lhs, rhs) | Relational | lhs > rhs |
STATIC_ASSERT_GE(lhs, rhs) | Relational | lhs >= rhs |
STATIC_ASSERT_TRUE(value) | Boolean | value == true |
STATIC_ASSERT_FALSE(value) | Boolean | value == false |
STATIC_ASSERT_STREQ(lhs, rhs) | C-String | strcmp(lhs, rhs) == 0 |
STATIC_ASSERT_STRNE(lhs, rhs) | C-String | strcmp(lhs, rhs) != 0 |
STATIC_ASSERT_STREQ and STATIC_ASSERT_STRNE are intended to be used for c-strings only, resolving the ambiguity on how to interpret char*. For other string types, prefer STATIC_ASSERT_EQ and STATIC_ASSERT_NE. The case-insensitive string comparison assertions ASSERT_STRCASEEQ and ASSERT_STRCASENE do not currently have an analog in the framework. Users can fall back to the runtime versions of these assertions if they are needed.
The floating point specific equality assertions gtest provides are omitted because implementing them as constexpr in a C++14 context is difficult, involving the re-implementation of complex facilities for determining “units in the last place.” Users should fall back to the normal gtest ASSERT_FLOAT_EQ and ASSERT_DOUBLE_EQ assertions for more accurate floating point comparison if needed.
A limitation of any simple wrapper approach to compile-time unit tests using static_assert inside a runtime testing framework like gtest is that ultimately the input to the assertion must be a constant expression, and the test bodies are not inherently constant expressions. This can introduce hurdles to writing compile-time-evaluated unit tests which are both correct and meaningful if the test requires more complexity than a simple inline call of the method under test. Such situations are common when attempting to validate classes with mutable state. Consider the following example for testing the behavior of the push() method of a constexpr compatible container:
One cannot simply place constexpr in front of the type declaration and replace ASSERT_EQ with STATIC_ASSERT_EQ to turn this test into a compile time evaluated test; when applied to a variable declaration, constexpr implies const, and this test needs to call non-const methods of ConstexprContainer.
A workaround to this problem is to add a layer of indirection via a constexpr function, as within the body of that function the container can be declared normally and thus not be const. Ideally, this could be done inside the test body using a lambda. Unfortunately, C++14 constexpr support is more restrictive than later C++ versions, and lambda expressions are not constexpr. This leaves external free functions, which must return the value to be asserted on. Following these guidelines, the push test could be rewritten to:
This change in shape accomplishes the goal of failing at compile time if push() does not place the element at head, while maintaining the property of testing across a robust set of initial states. However its readability has been harmed in a couple of ways:
Because ultimately the assertion macros are simply wrapping static_assert, and until C++26 static_assert's message must be a string-literal, it's not possible for the assertions to print the value of the inputs that caused the failure.
For situations where this makes debugging failure cases difficult, there is a compile time configuration option, ARENE_GTEST_STATIC_ASSERTIONS, which if set to 0 decays the assertions back to their regular ASSERT_XXX form. This should be set on the commandline of your compiler invocation; for bazel this would look like this:
In addition to the assertions above which always perform both a runtime assertion and a static assertion, Arene Base also provides a set of assertion macros which are always checked at runtime and may or may not be checked statically depending on the characters of the type TypeParam. In type-parameterized tests such as those declared with TYPED_TEST_P, TypeParam is set by Google Test to be the type with which the test has been parameterized. By using these conditional macros, you can write tests which are asserted statically only for instantiations where TypeParam supports doing so, while always being asserted at runtime.
The following conditionally-static assertion macros are available:
| Assertion | Category | Meaning |
|---|---|---|
COND_STATIC_ASSERT_EQ(lhs, rhs) | Equality | lhs == rhs |
COND_STATIC_ASSERT_NE(lhs, rhs) | Equality | lhs != rhs |
COND_STATIC_ASSERT_LT(lhs, rhs) | Relational | lhs < rhs |
COND_STATIC_ASSERT_LE(lhs, rhs) | Relational | lhs <= rhs |
COND_STATIC_ASSERT_GT(lhs, rhs) | Relational | lhs > rhs |
COND_STATIC_ASSERT_GE(lhs, rhs) | Relational | lhs >= rhs |
COND_STATIC_ASSERT_TRUE(value) | Boolean | value == true |
COND_STATIC_ASSERT_FALSE(value) | Boolean | value == false |
These generate static assertions when TestFixture::constexpr_compatible is true, and generate dynamic assertions in any case. Note that this means that to use these assertions, you will need to define a static variable constexpr_compatible in the enclosing test fixture.
The same caveats noted above for always-on static assertions also apply to the conditional ones: notably, the assertion body must not refer to non-constexpr variables, and no dynamic error message is available. More useful error messages can be obtained by disabling static assertions using the same ARENE_GTEST_STATIC_ASSERTIONS mechanism described above for the unconditional STATIC_ASSERT macros.