Skip to content
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
15 changes: 14 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.**

3 changes: 2 additions & 1 deletion tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions tests/test_postman.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* ─── PostMan self-test: guards against the T7 row-truncation regression ─── */

#include <gtest/gtest.h>
#include <sstream>
#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);
}
30 changes: 16 additions & 14 deletions vendor/PostMan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<int>(_rows.size()) + 1;
row.suite = _currentSuite;
row.label = label;
row.passed = passed;
_rows.push_back(row);
}

int TestReport::rowCount() const { return static_cast<int>(_rows.size()); }

/* ========================================================================
* UTF-8 String Measurement & Manipulation
* ======================================================================== */
Expand Down Expand Up @@ -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<int>(_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);
Expand All @@ -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);
}
Expand Down
22 changes: 11 additions & 11 deletions vendor/PostMan.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

#include <iostream>
#include <string>
#include <vector>

/** @defgroup ColorMacros ANSI Color & Style Macros */
/** @{ */
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand All @@ -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). */
Expand All @@ -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<PmRow> _rows;

/** Name of the current test suite. */
std::string _currentSuite;
Expand Down
Loading