Aussie AI
Chapter 12. Self-Testing Code
-
Book Excerpt from "Safe C++: Fixing Memory Safety Issues"
-
by David Spuler
What is Self-Testing Code?
Instead of doing work yourself, get a machine to do it. Who would have ever thought of that?
Getting your code to test itself means you can go get a cup of coffee and still be billing hours. The basic techniques include:
- Unit tests
- Regression tests
- Error checking
- Assertions
- Self-testing code blocks
- Debug wrapper functions
The simplest of these is unit tests, which aim to build quality brick-by-brick from the bottom of the code hierarchy. The largest techniques are to run full regression tests suites, or to add huge self-testing code blocks.
Self-Testing Code Block
Sometimes an assertion, unit test, or debug tracing printout is too small to check everything. Then you have to write a bigger chunk of self-testing code. The traditional way to do this in code is to wrap it in a preprocessor macro:
#if DEBUG
... // block of test code
#endif
Another reason to use a different type of self-testing code than assertions is that you’ve probably decided to leave the simpler assertions in production code. A simple test like this is probably fine for production:
assert(ptr != NULL); // Fast
But a bigger amount of arithmetic may be something that’s not for production:
assert(aussie_vector_sum(v, n) == 0.0); // Slow
So, you probably want to have macros and preprocessor settings for both production and debug-only assertions and self-testing code blocks. The simple way looks like this:
#if DEBUG
assert(aussie_vector_sum(v, n) == 0.0);
#endif
Or you could have your own debug-only version of assertions that are skipped for production mode:
assert_debug(aussie_vector_sum(v, n) == 0.0);
The definition of “assert_debug” then looks like this in the header file:
#if DEBUG
#define assert_debug(cond) assert(cond) // Debug mode
#else
#define assert_debug(cond) // nothing in production
#endif
This makes the “assert_debug” macro a normal assertion in debug mode,
but the whole coded expression disappears to nothing in production build mode.
The above example assumes a separate set of build flags for a production build.
Self-test Code Block Macro
An alternative formulation of a macro for installing self-testing code using a block-style, rather than a function-like macro, is as follows:
SELFTEST {
// block of debug or self-test statements
}
The definition of the SELFTEST macro looks like:
#if DEBUG
#define SELFTEST // nothing (enables!)
#else
#define SELFTEST if(1) {} else // disabled
#endif
This method relies on the C++ optimizer to fix the non-debug version,
by noticing that “if(1)” invalidates the else clause,
so as to remove the block of unreachable self-testing code that’s not ever executed.
Note also that SELFTEST is not function-like, so we don’t have the “forgotten semicolon” risk
when removing SELFTEST as “nothing”.
In fact, the nothing version is actually when SELFTEST code is enabled,
which is the opposite situation of that earlier problem.
Furthermore, we cannot use the “do-while(0)” trick in this different syntax formulation.
Self-Test Block Macro with Debug Flags
The compile-time on/off decision about self-testing code is not the most flexible method.
The block version of SELFTEST can also have levels or debug flag areas.
One natural extension is to implement a “flags” idiom for areas,
to allow configuration of what areas of self-testing code are executed for a particular run
(e.g., a decoding algorithm flag, a normalization flag, a MatMul flag, etc.).
One Boolean flag is set for each debugging area, which controls whether or not
the self-testing code in that module is enabled or not.
A macro definition of SELFTEST(flagarea) can be hooked into the run-time
configuration library for debugging output.
In this way, it has both a compile-out setting (DEBUG==0)
and dynamic runtime “areas” for self-testing code.
Here’s the definition of the self-testing code areas:
enum self_test_areas {
SELFTEST_NORMALIZATION,
SELFTEST_MATMUL,
SELFTEST_SOFTMAX,
// ... more
};
A use of the SELFTEST method with areas looks like:
SELFTEST(SELFTEST_NORMALIZATION) {
// ... self-test code
}
The SELFTEST macro definition with area flags looks like:
extern bool g_aussie_debug_enabled; // Global override
extern bool DEBUG_FLAGS[100]; // Area flags
#if DEBUG
#define SELFTEST(flagarea) \
if(g_aussie_debug_enabled == 0 || DEBUG_FLAGS[flagarea] == 0) \
{ /* do nothing */ } else
#else
#define SELFTEST if(1) {} else // disabled completely
#endif
This uses a “debug flags” array idea as for the debugging output commands, rather than a single “level” of debugging. Naturally, a better implementation would allow separation of the areas for debug trace output and self-testing code, with two different sets of levels/flags, but this is left as an extension for the reader.
Debug Stacktrace
There are various situations where it can be useful to have a programmatic method for reporting the “stack trace” or “backtrace” of the function call stack in C++. Some examples include:
- Your assertion macro can report the full stack trace on failure.
- Self-testing code similarly can report the location.
- Debug wrapper functions too.
- Writing your own memory allocation tracker library.
C++ is about to have standard stack trace capabilities
with its standardization in C++23.
This is available via the “std::stacktrace” facility, such as printing the current stack via:
std::cout << "Stacktrace: " << std::stacktrace::current() << std::endl;
The C++23 stacktrace library is already supported by GCC and early support in MSVS
is available via a compiler flag “/std:c++latest”.
There are also two different longstanding implementations of stack trace capabilities:
glibc backtrace and Boost StackTrace.
The C++23 standardized version is based on Boost’s version.
|
• Online: Table of Contents • PDF: Free PDF book download |
|
Safe C++: Fixing Memory Safety Issues:
Get it from Amazon: Safe C++: Fixing Memory Safety Issues |