memlnaut-nisps/tests/cpp/test_helpers.hpp
w1n5t0n e5bf2aa055 feat: nisps build + host test harness
CMakeLists.txt:
- Native host build by default; Emscripten-target detection plumbed but
  WASM emit deferred to stream 7 (playground build script).
- Header-only INTERFACE library `nisps_core`.
- Host test executable `nisps_core_tests` compiled with -Wall -Wextra
  -Werror -Wpedantic (Chris's rules: clean build is non-negotiable).

tests/cpp/test_helpers.hpp:
- Minimal NISPS_TEST / NISPS_EXPECT / NISPS_EXPECT_NEAR macros, no external
  deps. Rationale documented in-file: Catch2/doctest would add ~10MB and 30s
  for what is currently <100 LOC of test runtime.

22 unit tests covering FixedBuffer (5), RingBuffer (5), Rng (7), math (5).
All green; verified via `cmake --build nisps/build && ./nisps/build/nisps_core_tests`.
2026-04-29 15:22:01 +03:00

100 lines
3.8 KiB
C++

// tests/cpp/test_helpers.hpp — minimal assertion-based test harness.
//
// Why roll our own: zero external dependencies, builds in <1s, integrates
// trivially with CMake. Catch2/doctest would add a fetch step (≈ 10 MB and
// 30s of compile time) for what is currently a handful of asserts. If the
// suite grows beyond a few hundred lines we can revisit.
//
// API:
// NISPS_TEST(name) — declare a test
// NISPS_EXPECT(cond) — non-fatal check (records, keeps running)
// NISPS_ASSERT(cond) — fatal check (aborts the test)
// NISPS_EXPECT_NEAR(a, b, eps)
//
// Usage:
// NISPS_TEST(my_test) {
// NISPS_EXPECT(1 + 1 == 2);
// }
// int main() { return nisps::test::run_all(); }
#pragma once
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
namespace nisps::test {
struct TestFailure { const char* msg; };
struct TestCase {
const char* name;
void (*fn)();
};
inline std::vector<TestCase>& registry() {
static std::vector<TestCase> r;
return r;
}
struct Registrar {
Registrar(const char* name, void (*fn)()) {
registry().push_back({name, fn});
}
};
inline int run_all() {
int passed = 0, failed = 0;
for (const auto& tc : registry()) {
std::printf("[ RUN ] %s\n", tc.name);
try {
tc.fn();
std::printf("[ OK ] %s\n", tc.name);
++passed;
} catch (const TestFailure& f) {
std::printf("[ FAILED ] %s — %s\n", tc.name, f.msg);
++failed;
}
}
std::printf("\n[==========] %d passed, %d failed\n", passed, failed);
return failed == 0 ? 0 : 1;
}
} // namespace nisps::test
#define NISPS_TEST_CONCAT_(a, b) a##b
#define NISPS_TEST_CONCAT(a, b) NISPS_TEST_CONCAT_(a, b)
#define NISPS_TEST(name) \
static void NISPS_TEST_CONCAT(nisps_test_, name)(); \
static ::nisps::test::Registrar NISPS_TEST_CONCAT(nisps_test_reg_, name)( \
#name, &NISPS_TEST_CONCAT(nisps_test_, name)); \
static void NISPS_TEST_CONCAT(nisps_test_, name)()
#define NISPS_EXPECT(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, \
" EXPECT failed at %s:%d: %s\n", \
__FILE__, __LINE__, #cond); \
throw ::nisps::test::TestFailure{#cond}; \
} \
} while (0)
#define NISPS_ASSERT(cond) NISPS_EXPECT(cond)
#define NISPS_EXPECT_NEAR(a, b, eps) \
do { \
const double da = static_cast<double>(a); \
const double db = static_cast<double>(b); \
if (std::fabs(da - db) > static_cast<double>(eps)) { \
std::fprintf(stderr, \
" EXPECT_NEAR failed at %s:%d: %s ≈ %s " \
"(|%g - %g| = %g > %g)\n", \
__FILE__, __LINE__, #a, #b, da, db, \
std::fabs(da - db), static_cast<double>(eps)); \
throw ::nisps::test::TestFailure{#a " ≈ " #b}; \
} \
} while (0)