#ifndef CEPIS_TESTS_FRAMEWORK_HPP #define CEPIS_TESTS_FRAMEWORK_HPP #include #include #include #include #include namespace cepis::testing { /// Registers a test case. `CEPIS_TEST` does this for you. void register_test(const char* name, std::function body); /// Runs every registered test. Returns the process exit code. int run_all(); /// Thrown by the assertion helpers; caught by the runner. class AssertionFailure : public std::runtime_error { public: explicit AssertionFailure(const std::string& message) : std::runtime_error(message) {} }; struct Registrar { Registrar(const char* name, std::function body) { register_test(name, std::move(body)); } }; [[noreturn]] inline void fail(const std::string& detail, const char* file, int line) { std::ostringstream message; message << file << ':' << line << ": " << detail; throw AssertionFailure(message.str()); } inline void check_true(bool value, const char* text, const char* file, int line) { if (!value) { fail(std::string("expected ") + text, file, line); } } /// Comparison lives in a function rather than a macro so that the arguments are /// bound as parameters: a `CHECK_EQ` on a member of a temporary would otherwise /// compare against a reference that has already dangled. template void check_equal(const Actual& actual, const Expected& expected, const char* text, const char* file, int line) { if (actual == expected) { return; } std::ostringstream detail; detail << "expected " << text << ", got [" << actual << "] vs [" << expected << ']'; fail(detail.str(), file, line); } inline void check_near(double actual, double expected, double tolerance, const char* text, const char* file, int line) { if (std::fabs(actual - expected) <= tolerance) { return; } std::ostringstream detail; detail << "expected " << text << ", got " << actual << " vs " << expected; fail(detail.str(), file, line); } } // namespace cepis::testing #define CEPIS_TEST(test_name) \ static void test_name(); \ static const ::cepis::testing::Registrar registrar_##test_name(#test_name, &(test_name)); \ static void test_name() #define CHECK(condition) ::cepis::testing::check_true((condition), #condition, __FILE__, __LINE__) #define CHECK_EQ(actual, expected) \ ::cepis::testing::check_equal((actual), (expected), #actual " == " #expected, __FILE__, __LINE__) #define CHECK_NEAR(actual, expected, tolerance) \ ::cepis::testing::check_near((actual), (expected), (tolerance), #actual " ~= " #expected, \ __FILE__, __LINE__) #define CHECK_THROWS(expression, exception_type) \ do { \ bool cepis_threw = false; \ try { \ (void)(expression); \ } catch (const exception_type&) { \ cepis_threw = true; \ } \ if (!cepis_threw) { \ ::cepis::testing::fail("expected " #expression " to throw " #exception_type, __FILE__, \ __LINE__); \ } \ } while (false) #endif // CEPIS_TESTS_FRAMEWORK_HPP