diff --git a/CLAUDE.md b/CLAUDE.md index 4ea169c..f8f99c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,7 +69,7 @@ Dispatch enforces a **registration gate**: only CAP/PASS/NICK/USER/QUIT/PONG run ## Testing -Tests use Google Test but also feed every result into **PostMan** (`vendor/PostMan.cpp`), a styled Unicode-table reporter — `tests/test_main.cpp` bridges the two via a custom `TestEventListener`. `tests/Makefile` builds all of `src/` *except* `main.cpp` (linking `tier_full.cpp` as the one `registerExtensions` definition). Protocol-level suites share `tests/TestHarness.hpp` (TCP `TestClient` + `IrcServerTest` fixture; subclass and override `portBase()` per suite, `onServerReady()` to inject probe extensions). Test files: `test_message`, `test_client`, `test_channel`, `test_bot`, `test_integration`, `test_robustness`, `test_security`, `test_filetransfer`, `test_extensions`, `test_libcpp98`. Suite is 139/139; PostMan's leak counter is atomic and `assertNoLeaks` takes `const char*` (a `std::string` argument would count itself as a leak — keep it that way). +Tests use Google Test but also feed every result into **PostMan** (`vendor/PostMan.cpp`), a styled Unicode-table reporter — `tests/test_main.cpp` bridges the two via a custom `TestEventListener`. `tests/Makefile` builds all of `src/` *except* `main.cpp` (linking `tier_full.cpp` as the one `registerExtensions` definition). Protocol-level suites share `tests/TestHarness.hpp` (TCP `TestClient` + `IrcServerTest` fixture; subclass and override `portBase()` per suite, `onServerReady()` to inject probe extensions). Test files: `test_message`, `test_client`, `test_channel`, `test_bot`, `test_integration`, `test_robustness`, `test_security`, `test_filetransfer`, `test_extensions`, `test_libcpp98`. ~149 tests (see PostMan table); PostMan's leak counter is atomic and `assertNoLeaks` takes `const char*` (a `std::string` argument would count itself as a leak — keep it that way). ## Known traps @@ -91,4 +91,17 @@ Tests use Google Test but also feed every result into **PostMan** (`vendor/PostM process with SIGPIPE (exit 141). Now installed in `tests/test_main.cpp`. Keep it there; it is a property of the process, not of any one fixture. - Autodeterminded flood; overlap is structural, not a race; don't return it as fixed FLOOD_LINES. +- **Backpressure tests use a self-terminating flood**: the T6 frozen-reader + tests (`test_robustness.cpp`) flood until the test says stop (`stopFlood`), + bounded by a `FLOOD_CAP` safety net — NOT for a fixed `FLOOD_LINES` count. + A fixed volume makes the overlap a race: the flood has to happen to outlast + the probe on every machine. The 200k-line version passed locally and failed + in the CI container. Do not "simplify" this back to a fixed count. +- **PostMan silently dropped rows above 256** (fixed in T7): `PM_MAX_ROWS` was + a fixed-array cap and `record()` returned early past it, with no warning. + Under `--gtest_repeat=3` the table printed "All 256 assertions passed" while + 444 tests had actually run — and a real FAIL past row 256 was swallowed, + leaving a green table against a red exit code. `_rows` is now a + `std::vector` with no cap. **Any `--gtest_repeat` validation done before T7 + may have read a truncated report.** \ No newline at end of file diff --git a/tests/Makefile b/tests/Makefile index 613ef1d..cb4e73e 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -66,7 +66,8 @@ TEST_SRCS = test_main.cpp \ test_security.cpp \ test_filetransfer.cpp \ test_extensions.cpp \ - test_libcpp98.cpp + test_libcpp98.cpp \ + test_postman.cpp # ── Vendor sources ──────────────────────────────────────────── GTEST_SRC = ../vendor/googletest/googletest/src/gtest-all.cc diff --git a/tests/test_postman.cpp b/tests/test_postman.cpp new file mode 100644 index 0000000..68d9a46 --- /dev/null +++ b/tests/test_postman.cpp @@ -0,0 +1,28 @@ +/* ─── PostMan self-test: guards against the T7 row-truncation regression ─── */ + +#include +#include +#include "PostMan.hpp" + +/* + * TestReport is a process-wide singleton (see PostMan.hpp), so this test + * records synthetic rows into the same instance the rest of the suite + * feeds via OnTestEnd. It only ever records passing rows and never touches + * _currentSuite, so it cannot affect any other test's pass/fail outcome — + * it can only add an extra suite section to the cosmetic PostMan table, + * which is the point: the final table must account for every row. + */ +TEST(PostManTruncationRegression, RecordsAllRowsAboveLegacyCap) +{ + const int kSynthetic = 300; /* comfortably above the removed 256-row cap */ + const int before = TestReport::instance().rowCount(); + + for (int i = 0; i < kSynthetic; ++i) + { + std::ostringstream label; + label << "T7_synthetic_" << i; + TestReport::instance().record(label.str(), true); + } + + EXPECT_EQ(TestReport::instance().rowCount() - before, kSynthetic); +} diff --git a/vendor/PostMan.cpp b/vendor/PostMan.cpp index 6d73f1e..e9aa184 100644 --- a/vendor/PostMan.cpp +++ b/vendor/PostMan.cpp @@ -129,7 +129,7 @@ TestReport& TestReport::instance() { return inst; } -TestReport::TestReport() : _count(0), _currentSuite("(none)") {} +TestReport::TestReport() : _currentSuite("(none)") {} /** * @brief Begin a new test suite section. @@ -144,21 +144,22 @@ void TestReport::beginSuite(const std::string& name) { _currentSuite = name; } /** * @brief Record a test assertion result. * - * Stores the assertion in the internal buffer at _rows[_count]. - * If the buffer is full (>= PM_MAX_ROWS), the assertion is ignored. + * Appends the assertion to the internal buffer, which grows as needed. * * @param label Description of what the assertion tests. * @param passed True if assertion passed, false if failed. */ void TestReport::record(const std::string& label, bool passed) { - if (_count >= PM_MAX_ROWS) return; - _rows[_count].id = _count + 1; - _rows[_count].suite = _currentSuite; - _rows[_count].label = label; - _rows[_count].passed = passed; - ++_count; + PmRow row; + row.id = static_cast(_rows.size()) + 1; + row.suite = _currentSuite; + row.label = label; + row.passed = passed; + _rows.push_back(row); } +int TestReport::rowCount() const { return static_cast(_rows.size()); } + /* ======================================================================== * UTF-8 String Measurement & Manipulation * ======================================================================== */ @@ -491,18 +492,19 @@ void TestReport::drawVerdict(const PmCols& c, int p, int f) { * @note Call this after all test assertions have been recorded. */ void TestReport::print() const { + const int n = static_cast(_rows.size()); int passed = 0, failed = 0; - for (int i = 0; i < _count; ++i) + for (int i = 0; i < n; ++i) _rows[i].passed ? ++passed : ++failed; - PmCols cols = calcCols(_rows, _count); + PmCols cols = calcCols(_rows.data(), n); std::cout << "\n\n"; - drawTitle(cols, passed, failed, _count); + drawTitle(cols, passed, failed, n); drawHeader(cols); drawHLine(cols, 7); std::string lastSuite; int sp = 0, sf = 0; - for (int i = 0; i < _count; ++i) { + for (int i = 0; i < n; ++i) { if (_rows[i].suite != lastSuite) { if (i != 0) { drawHLine(cols, 9); @@ -515,7 +517,7 @@ void TestReport::print() const { _rows[i].passed ? ++sp : ++sf; drawRow(cols, _rows[i]); } - if (_count > 0) { + if (n > 0) { drawHLine(cols, 9); drawSuiteSum(cols, lastSuite, sp, sf); } diff --git a/vendor/PostMan.hpp b/vendor/PostMan.hpp index a3b02ff..d0d4d63 100644 --- a/vendor/PostMan.hpp +++ b/vendor/PostMan.hpp @@ -31,6 +31,7 @@ #include #include +#include /** @defgroup ColorMacros ANSI Color & Style Macros */ /** @{ */ @@ -73,9 +74,6 @@ /** @} */ -/** Maximum number of assertion records that can be stored before overflow. */ -#define PM_MAX_ROWS 256 - /** * @struct PmRow * @brief Represents a single test assertion record. @@ -141,7 +139,7 @@ struct PmCols { * @class TestReport * @brief A Singleton class that collects and prints formatted test results. * - * The TestReport class maintains an internal array of up to PM_MAX_ROWS test + * The TestReport class maintains a dynamically-sized list of test * records. It provides methods to: * - Group tests into logical suites with beginSuite() * - Record individual pass/fail assertions with record() @@ -188,8 +186,7 @@ class TestReport { /** * @brief Record a single assertion result. * - * Stores the assertion in the internal buffer. If the buffer is full - * (PM_MAX_ROWS reached), the assertion is silently ignored. + * Stores the assertion in the internal buffer, which grows as needed. * * @param label The description of what the assertion tests. * @param passed True if the assertion passed, false otherwise. @@ -210,6 +207,12 @@ class TestReport { */ void print() const; + /** + * @brief Number of rows currently recorded. + * @return The number of assertion rows recorded so far. + */ + int rowCount() const; + private: int _allocSnapshot; //NEW: Stores the baseline memory count /** @brief Private constructor (Singleton pattern). */ @@ -221,11 +224,8 @@ class TestReport { /** @brief Deleted assignment operator (non-assignable). */ TestReport& operator=(const TestReport&); - /** Array of test assertion records. */ - PmRow _rows[PM_MAX_ROWS]; - - /** Current number of recorded assertions. */ - int _count; + /** Dynamically-sized list of test assertion records. */ + std::vector _rows; /** Name of the current test suite. */ std::string _currentSuite;