Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Panic utilities improvements: ensure DEBUG_ASSERT compiles, add ASSERT_EQ etc., add tests #571

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 89 additions & 10 deletions include/silo/common/panic.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,57 @@ 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 internal_assert_op__v1 = (e1); \
auto internal_assert_op__v2 = (e2); \
if (!(internal_assert_op__v1 op internal_assert_op__v2)) { \
silo::common::assertOpFailure( \
prefix_str, \
#e1, \
#op, \
#e2, \
fmt::format("{} " #op " {}", internal_assert_op__v1, internal_assert_op__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 All @@ -70,22 +121,50 @@ namespace silo::common {
#ifndef DEBUG_ASSERTIONS
#warning \
"DEBUG_ASSERTIONS is not set, should be 0 to ignore DEBUG_ASSERT, 1 to compile it in, assuming 0"
#define DEBUG_ASSERT(e)
#else // DEBUG_ASSERTIONS is defined
#if DEBUG_ASSERTIONS == 0 /* never */
#define DEBUG_ASSERT(e)
#define DEBUG_ASSERTIONS 0
#else
#if DEBUG_ASSERTIONS == 0 /* never */
#elif DEBUG_ASSERTIONS == 1 /* always */
#define DEBUG_ASSERT(e) \
do { \
if (!(e)) { \
silo::common::debugAssertFailure(#e, __FILE__, __LINE__); \
} \
} while (0)
#else
#error "DEBUG_ASSERTIONS should be 0 to ignore DEBUG_ASSERT, 1 to compile it in"
#endif
#endif

#define DEBUG_ASSERT(e) \
do { \
if (DEBUG_ASSERTIONS) { \
if (!(e)) { \
silo::common::debugAssertFailure(#e, __FILE__, __LINE__); \
} \
} \
} while (0)

[[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
}