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

Buy: Safe C++: Fixing Memory Safety Issues

Safe C++ Safe C++: Fixing Memory Safety Issues:
  • The memory safety debate
  • Memory and non-memory safety
  • Pragmatic approach to safe C++
  • Rust versus C++
  • DIY memory safety methods
  • Safe standard C++ library

Get it from Amazon: Safe C++: Fixing Memory Safety Issues