diff --git a/include/silo/common/panic.h b/include/silo/common/panic.h index 004e613fb..100ebaa94 100644 --- a/include/silo/common/panic.h +++ b/include/silo/common/panic.h @@ -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 @@ -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 diff --git a/src/silo/common/panic.cpp b/src/silo/common/panic.cpp index 4ca724bc8..2f2595470 100644 --- a/src/silo/common/panic.cpp +++ b/src/silo/common/panic.cpp @@ -4,6 +4,8 @@ #include #include +#include + namespace silo::common { namespace { @@ -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); } diff --git a/src/silo/common/panic.test.cpp b/src/silo/common/panic.test.cpp new file mode 100644 index 000000000..e3f039747 --- /dev/null +++ b/src/silo/common/panic.test.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include + +/* 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 +}