14 releases (4 breaking)

0.5.0 Oct 22, 2024
0.4.0 Oct 21, 2024
0.3.1 Oct 19, 2024
0.2.1 Oct 17, 2024
0.1.7 Oct 14, 2024

#149 in Debugging

MIT/Apache

49KB
817 lines

NoBug provides GLARING! assertions and annotations for rust code. These have different semantic than the stdlib assertions. The Idea behind NoBug is to have active annotations with semantic meaning. The annotations are not just comments but actively checked at runtime or compile time. By using uppercase macros they are easily visible and searchable in source code.

Overview

Unlike the stdlib assertions which panic on failure, the NoBug assertions will abort the program and thus are not catchable. Without the unwinding overhead they wont generate any excess code. Only in debug build tests they will panic because the test harness running tests in threads and catching panics. When aborting at runtime is not desired then the *_DBG! variants of the macros can be used. Those will only check the condition in debug builds. This can be enforced by disabling the abort_on_fail feature.

Assertions

Aside from a generic (and discouraged) ASSERT! macro we provide:

  • REQUIRE! -- checking preconditions
    Asserts that input parameters for functions and methods are valid. This is important when a function or method takes inputs that have a wider range of valid ranges than the function or method expects. This can be numeric data or text for example.
  • ENSURE! -- checking postconditions
    Asserts that the output of a code block is valid. To be used when a computation could produce a result that is not within a valid range.
  • CHECK! -- generic checks for testing and similar cases.
    Substitute for std::assert! macros in tests.

Each of those has a correspondending ASSERT_DBG!, REQUIRE_DBG! and ENSURE_DBG! variants that inserts checks only in debug builds.

REQUIRE! and CHECK! have somme specializations for const asserts and comparisons which give more concise error messages.

Since some programs must not abort on assertion failure we provide a abort_on_failure feature. When this is not enabled then any DIE! and any other macro calling it will cause a compile error in release, non-test builds. One can only use the *_DBG! variants of the nobug macros then.

Threre is a special ASSERT_ONCE! macro that will only check the condition once and then never again.

Annotations

NoBug defines macros to actively annotate code. Actively means that these macros can do different things in release or debug builds, depending on configuration:

  • Be a [compile_error!()]
    When things must be resolved before software is released and deployed.
  • Check that assertions are valid
  • Abort the program with a message
    When a code path can't make progress because of unfinished code.
  • Log a message each time the macro is encountered
    Just to nag the programmer
  • Log a message once when the macro is encountered for the first time
    Less nagging
  • Do nothing/optimize out

In release builds all annotations are either optimized out or compile errors. Thus they have zero cost in release builds.

We provide following annotation macros (with the noted defaults):

  • FIXME! -- Bug that must be fixed.
  • FIXED! -- Annotate fixed bugs.
  • TODO! -- Incomplete but required code.
  • DONE! -- Completed code.
  • WIP! -- Work in progress.
  • IDEA! -- Not required improvement of existing code.
  • PLANNED! -- Potential future planned feature.

FIXME!, FIXED!, WIP! and DONE! can be augmented with optional code blocks that are checked at runtime. Notifying about assertions still being valid and detecting regressions.

MOCK! must have code blocks either a single one or conditional IF-THEN-ELSE blocks. This can be used where WIP! is not sufficient.

Message logging

Nobug logs messages to stderr (using the log or tracing crate is planned for the future). Messages can be of different severity levels.

  • CRITICAL! -- Print a critical message.
    This is used when the program is going to abort.
  • NOTICE! -- Print a notice to the user.
    This is used for annotations.
  • TRACE! -- Print a trace message to the user.
    This is used for debugging/progress.
  • NOTICE_ONCE! -- Print a notice to the user.
    This is used for annotations.

Each of these has a *_DBG! variant that only logs in debug builds. There is a TRACE_DBG_IF! macro that does conditional logging in debug builds.

Message Format

The messages are entries delimited by a colon followed by a space, they always start with the file and line number of the message.

<file>:<line>: <reason>: <message>

where <message> can have multiple free-form parts each delimited by colon space as well

Tool Macros

The above uses some tool macros which may be useful on their own in some cases.

  • DIE! -- Abort (or panic). compile_error!() when abort_on_fail is not enabled.
  • ONCE! -- Execute a block only once.
  • MOCK! -- Trivial code mocking.

Feature Flags

  • abort_on_fail
    This is enabled by default. When not enabled then DIE! and all macros using it will cause a compile error in release/non-test builds. This is useful when you want to make sure that nobug will never abort in release builds. One can only use the *_DBG! variants of the nobug macros in release builds then.

  • const_expr
    This is enabled by default. When not enabled then the REQUIRE! and CHECK! macros will not use const {} expression specializations and fall back to a inferior implementation. Disabling this feature is required when using a compiler before v1.79.0.

  • trace_nobug
    Makes nobug TRACE! all calls to its own macros. This is useful for debugging nobug itself or to get some quick insight of the a program's progress.

Workflow

The idea is to use the annotations and assertions to actively document the code. Assertions ideally stay in the code and are not removed. They are the contract of the code, only when performance is critical they can be replaced by a the *_DBG! variant. Keep in mind that in some cases the compiler can optimize the checks away or do more aggressive optimizations when it knows that certain conditions are always true.

Nobug assertions are not a substitution for the std::assert! macros. When required they can be mixed. For the cases where panics need to be caught and handled the std::assert! macros must be used. The NoBug macros are for the cases where the program is in a undefined state due to a programming error that can't be detected at compile time and should be aborted.

The annotations can be used to define a stateful workflow, for example:

  • FIXME! -> FIXED!
  • IDEA! -> WIP! -> DONE!
  • PLANNED! -> TODO! -> WIP! -> DONE!

This is not rigid, states can be left out and unused. Only keep whatever makes sense. Especially the final FIXED! and DONE! states can eventually accumulate and clutter the code. It may still be benefitical to keep them in the code for a while since they guard against regressions.

Found bugs that are annotated with FIXME! may as well transformed into a test case within the unit or integration tests.

Incomplete code can be mocked with MOCK! and then replaced with the real implementation when it is done. In some cases this might be preferable to or complement WIP! since it allows to run some tests with the mocked code.

Testsuites may prefer to use the CHECK! macro if appropriate.

History

In 2006 I started a C Debug library of the same name (https://git.pipapo.org/cehteh/nobug). This implementation will carry some of its ideas over to rust.

Roadmap

Currently we only implemented most basic functionally. In future more features will be added.

  • Eventually some of the macros will become proc macros
  • Support for invariants on objects (needs proc macros)
  • With proc macros we can have some nicer syntax, for example:
    #[nobug]
    fn example(i: i32 where i > 0) -> i32
    where
        result < 100
    {
        ...
    }
    
  • Add support for logging and tracing

Dependencies