Skip to content

Commit

Permalink
refactor: panic: add ASSERT_EQ and similar, add test
Browse files Browse the repository at this point in the history
  • Loading branch information
pflanze committed Sep 12, 2024
1 parent cc05ca6 commit 2b97851
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 0 deletions.
71 changes: 71 additions & 0 deletions include/silo/common/panic.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,51 @@ namespace silo::common {

[[noreturn]] void assertFailure(const char* msg, const char* file, int line);

#define INTERNAL_ASSERT_OP_(prefix_str, e1, op, e2) \
do { \
auto v1 = (e1); \
auto v2 = (e2); \
if (!(v1 op v2)) { \
silo::common::assertOpFailure( \
prefix_str, #e1, #op, #e2, fmt::format("{} " #op " {}", v1, v2), __FILE__, __LINE__ \
); \
} \
} while (0)

#define ASSERT_OP_(partial_prefix, e1, op, e2) \
INTERNAL_ASSERT_OP_("ASSERT_" #partial_prefix, e1, op, e2)

[[noreturn]] void assertOpFailure(
const char* prefix,
const char* e1_str,
const char* op_str,
const char* e2_str,
const std::string& values,
const char* file,
int line
);

/// Asserts that the two given expressions evaluate to the same
/// value, compared via `==`. On failure calls `panic` with the stringification of the code and the
/// two values passed through fmt::format, plus file/line information. Always compiled in, if
/// performance overrides safety, use `DEBUG_ASSERT_EQ` instead.
#define ASSERT_EQ(e1, e2) ASSERT_OP_(EQ, e1, ==, e2)

/// Like ASSERT_EQ but asserts that `e1 <= e2`.
#define ASSERT_LE(e1, e2) ASSERT_OP_(LE, e1, <=, e2)

/// Like ASSERT_EQ but asserts that `e1 < e2`.
#define ASSERT_LT(e1, e2) ASSERT_OP_(LT, e1, <, e2)

/// Like ASSERT_EQ but asserts that `e1 >= e2`.
#define ASSERT_GE(e1, e2) ASSERT_OP_(GE, e1, >=, e2)

/// Like ASSERT_EQ but asserts that `e1 > e2`.
#define ASSERT_GT(e1, e2) ASSERT_OP_(GT, e1, >, e2)

/// Like ASSERT_EQ but asserts that `e1 != e2`.
#define ASSERT_NE(e1, e2) ASSERT_OP_(NE, e1, !=, e2)

/// `DEBUG_ASSERT` is like `ASSERT`, but for cases where performance
/// is more important than verification in production: instantiations
/// are only active when compiling SILO in debug (via
Expand Down Expand Up @@ -90,4 +135,30 @@ namespace silo::common {

[[noreturn]] void debugAssertFailure(const char* msg, const char* file, int line);

#define DEBUG_ASSERT_OP_(partial_prefix, e1, op, e2) \
do { \
if (DEBUG_ASSERTIONS) { \
INTERNAL_ASSERT_OP_("DEBUG_ASSERT_" #partial_prefix, e1, op, e2); \
} \
} while (0)

/// Like `ASSERT_EQ`, but like `DEBUG_ASSERT`, for cases where
/// performance is more important than verification in production.
#define DEBUG_ASSERT_EQ(e1, e2) DEBUG_ASSERT_OP_(EQ, e1, ==, e2)

/// Like DEBUG_ASSERT_EQ but asserts that `e1 <= e2`.
#define DEBUG_ASSERT_LE(e1, e2) DEBUG_ASSERT_OP_(LE, e1, <=, e2)

/// Like DEBUG_ASSERT_EQ but asserts that `e1 < e2`.
#define DEBUG_ASSERT_LT(e1, e2) DEBUG_ASSERT_OP_(LT, e1, <, e2)

/// Like DEBUG_ASSERT_EQ but asserts that `e1 >= e2`.
#define DEBUG_ASSERT_GE(e1, e2) DEBUG_ASSERT_OP_(GE, e1, >=, e2)

/// Like DEBUG_ASSERT_EQ but asserts that `e1 > e2`.
#define DEBUG_ASSERT_GT(e1, e2) DEBUG_ASSERT_OP_(GT, e1, >, e2)

/// Like DEBUG_ASSERT_EQ but asserts that `e1 != e2`.
#define DEBUG_ASSERT_NE(e1, e2) DEBUG_ASSERT_OP_(NE, e1, !=, e2)

} // namespace silo::common
14 changes: 14 additions & 0 deletions src/silo/common/panic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <cstring>
#include <iostream>

#include <fmt/format.h>

namespace silo::common {

namespace {
Expand Down Expand Up @@ -53,6 +55,18 @@ namespace {
panic("ASSERT failure: ", msg, file, line);
}

[[noreturn]] void assertOpFailure(
const char* prefix,
const char* e1_str,
const char* op_str,
const char* e2_str,
const std::string& values,
const char* file,
int line
) {
panic(fmt::format("{} failure: {} {} {}: ", prefix, e1_str, op_str, e2_str), values, file, line);
}

[[noreturn]] void debugAssertFailure(const char* msg, const char* file, int line) {
panic("DEBUG_ASSERT failure: ", msg, file, line);
}
Expand Down
101 changes: 101 additions & 0 deletions src/silo/common/panic.test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include <fmt/format.h>
#include <gtest/gtest.h>
#include <cctype>
#include <cstdlib>

/* get rid of gtest's definition, we're going to test our own */
#undef ASSERT_EQ

#include "silo/common/panic.h"

namespace {
// Since we can't use ASSERT_EQ from gtest, make a simple
// replacement. `expected` should be without the file:line
// information, whereas `got` should contain it.
void assertMsg(std::string got, std::string expected) {
auto start = got.substr(0, expected.size());
if (start != expected) {
throw std::runtime_error(
fmt::format("expected '{}', got '{}' (full: '{}')", expected, start, got)
);
}
auto remainder = got.substr(expected.size(), got.size() - expected.size());
if (remainder.size() < 4) {
throw std::runtime_error("missing ' at ..' part of exception message");
}
auto the_at = remainder.substr(0, 4);
if (the_at != std::string(" at ")) {
throw std::runtime_error(fmt::format("expected '{}', got '{}'", " at ", the_at));
}

const char last = remainder[remainder.size() - 1];
if (!isdigit(last)) {
throw std::runtime_error(fmt::format("expected a digit at the end of '{}'", got));
}
// good enough, ignore the rest of the remainder.
}
} // namespace

// NOLINTNEXTLINE(readability-identifier-naming,readability-function-cognitive-complexity)
TEST(panic, assertEqPanicModes) {
ASSERT_EQ(1 + 1, 2);

setenv("SILO_PANIC", "", 1);
try {
ASSERT_EQ(1 + 1, 3);
} catch (const std::exception& ex) {
assertMsg(ex.what(), "ASSERT_EQ failure: 1 + 1 == 3: 2 == 3");
};

setenv("SILO_PANIC", "abort", 1);
ASSERT_DEATH(ASSERT_EQ(1 + 1, 3), "ASSERT_EQ failure: 1 \\+ 1 == 3: 2 == 3");

// revert it back
setenv("SILO_PANIC", "", 1);
}

// NOLINTNEXTLINE(readability-identifier-naming,readability-function-cognitive-complexity)
TEST(panic, debugAssertBehavesAsPerCompilationMode) {
// should never complain
DEBUG_ASSERT(1 + 1 == 2);

// Check that DEBUG_ASSERT is active if DEBUG_ASSERTIONS==1, off
// otherwise; each of those branches is only tested when compiling
// the unit tests in debug or release mode, respectively.

#if DEBUG_ASSERTIONS

setenv("SILO_PANIC", "", 1);
try {
DEBUG_ASSERT(1 + 1 == 3);
} catch (const std::exception& ex) {
assertMsg(ex.what(), "DEBUG_ASSERT failure: 1 + 1 == 3");
};

#else
// check that DEBUG_ASSERT is disabled
DEBUG_ASSERT(1 + 1 == 3);
#endif
}

// NOLINTNEXTLINE(readability-identifier-naming,readability-function-cognitive-complexity)
TEST(panic, debugAssertGeWorks) {
// stand-in for all the DEBUG_ASSERT_* variants

DEBUG_ASSERT_GE(1 + 5, 6);
DEBUG_ASSERT_GE(1 + 5, 5);

#if DEBUG_ASSERTIONS

setenv("SILO_PANIC", "", 1);
try {
DEBUG_ASSERT_GE(1 + 5, 7);
} catch (const std::exception& ex) {
assertMsg(ex.what(), "DEBUG_ASSERT_GE failure: 1 + 5 >= 7: 6 >= 7");
};

#else
// check that DEBUG_ASSERT is disabled
DEBUG_ASSERT_GE(1 + 5, 7);
#endif
}

0 comments on commit 2b97851

Please sign in to comment.