Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f760661
docs: PipeTests framework design spec
muit Sep 3, 2026
f7f26b3
docs: PipeTests implementation plan
muit Sep 3, 2026
31a9b45
build: add PipeTests library target
muit Sep 3, 2026
683e3f6
feat: declare PipeTests registration API
muit Sep 3, 2026
cbe2f17
feat: add PipeTests registry and runner
muit Sep 3, 2026
85ccb14
test: add PipeTests self-test suite
muit Sep 4, 2026
0c3bd53
docs: macro-free registration, PipeTestsLib rename in plan+spec
muit Sep 4, 2026
cddd073
feat: add Expect fluent matcher
muit Sep 4, 2026
e4bb22b
fix: use source_location for call-site failure reporting
muit Sep 4, 2026
20136dd
docs: source_location for call-site EXPECT reporting
muit Sep 4, 2026
832d97c
refactor: TestSettings RunTests, Pipe types, TestDescribe/TestContext…
muit Sep 4, 2026
ebc9b5a
test: file-scope auto-register, PipeTest lib, PipeTests suite, TestSe…
muit Sep 4, 2026
7ced119
test: clean up spec files, remove PipeTestsSelf, add CLI features
muit Sep 4, 2026
ab5ffb0
fix: missing period in PipeTests colored summary format string
muit Sep 4, 2026
f156ef1
fix: correct placeholder count in colored summary format string
muit Sep 4, 2026
e06836f
Added log colors and docs
muit Sep 4, 2026
b5ab97f
Added new reporters, fixes, help and version and formatting
muit Sep 4, 2026
7e540b0
P_SPEC macro improvements, added TTypeId
muit Sep 5, 2026
b480ebc
Refactored Reflection and Type headers
muit Sep 6, 2026
2808c5f
Solved dependency issue
muit Sep 6, 2026
dc14d8e
Small fix for Windows
muit Sep 6, 2026
3d27ec3
SMall fix on non-windows builds
muit Sep 6, 2026
f5ccde2
Removed plans
muit Sep 6, 2026
d0d1c0f
Small changes to testing api
muit Sep 6, 2026
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
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
[submodule "Extern/Bandit"]
path = Extern/Bandit
url = https://github.com/banditcpp/bandit.git
17 changes: 16 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON)
################################################################################
# Project

project(Pipe VERSION 0.1 LANGUAGES CXX C)
project(Pipe VERSION 1.7 LANGUAGES CXX C)


if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
Expand Down Expand Up @@ -63,7 +63,9 @@ pipe_target_define_platform(Pipe)
target_include_directories(Pipe PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/Include>)
target_include_directories(Pipe PRIVATE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/Src>)
file(GLOB_RECURSE PIPE_SOURCE_FILES CONFIGURE_DEPENDS Src/*.cpp Src/*.c)
list(FILTER PIPE_SOURCE_FILES EXCLUDE REGEX ".*/Src/Tests/.*")
target_sources(Pipe PRIVATE ${PIPE_SOURCE_FILES})
target_compile_definitions(Pipe PUBLIC P_VERSION="${PROJECT_VERSION}")
target_compile_definitions(Pipe PRIVATE NOMINMAX)

if(PIPE_ENABLE_ALLOCATION_STACKS)
Expand All @@ -81,6 +83,19 @@ pipe_target_shared_output_directory(Pipe)
pipe_target_disable_rtti(Pipe PRIVATE)


################################################################################
# PipeTest (test framework library, not part of the runtime Pipe library)

add_library(PipeTest STATIC Src/Tests/PipeTest.cpp)
add_library(Pipe::Test ALIAS PipeTest)
pipe_target_define_platform(PipeTest)
target_include_directories(PipeTest PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/Include>)
pipe_target_enable_CPP20(PipeTest)
pipe_target_disable_rtti(PipeTest PRIVATE)
pipe_target_shared_output_directory(PipeTest)
target_link_libraries(PipeTest PUBLIC Pipe)


################################################################################
# Pipe Tests (compiled) executable

Expand Down
95 changes: 95 additions & 0 deletions Docs/Log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Log

The logging system in Pipe provides a callback-based logger with three severity levels: `Info`, `Warning`, and `Error`.

## Default Behavior

The default logger outputs messages with no formatting:

```cpp
p::Info("hello world"); // prints: hello world
p::Warning("something"); // prints: something
p::Error("bad state"); // prints: bad state
```

## API

### Logger Struct

```cpp
struct Logger
{
std::function<void(StringView)> infoCallback;
std::function<void(StringView)> warningCallback;
std::function<void(StringView)> errorCallback;
};
```

Each field is a callback invoked for its respective log level. All three must be provided.

### InitLog / ShutdownLog

```cpp
void InitLog(Logger* logger = nullptr);
void ShutdownLog();
```

- `InitLog(nullptr)` — uses the default logger.
- `InitLog(&myLogger)` — uses a custom logger. The pointer must remain valid until `ShutdownLog()`.
- `ShutdownLog()` — clears the active logger. Calls to `Info`/`Warning`/`Error` become no-ops until `InitLog` is called again.

### Info / Warning / Error

```cpp
void Info(StringView msg);
void Warning(StringView msg);
void Error(StringView msg);
```

These dispatch to the corresponding callback on the active logger. If no logger is active messages are silently ignored.

Text can be formatted (using `p::Format`):

```cpp
p::Info("Loaded {} assets.", count);
p::Warning("Deprecated: {}", feature);
p::Error("Failed to open {}", path);
```

## Custom Logger Example

The following reproduces the old default format with timestamps and level tags:

```cpp
p::Logger timestampedLogger{
.infoCallback = [](p::StringView msg) {
p::String text;
auto now = p::DateTime::Now();
now.ToString("[%Y/%m/%d %H:%M:%S]", text);
p::FormatTo(text, "[Info] {}\n", msg);
std::cout << text;
},
.warningCallback = [](p::StringView msg) {
p::String text;
auto now = p::DateTime::Now();
now.ToString("[%Y/%m/%d %H:%M:%S]", text);
p::FormatTo(text, "[Warning] {}\n", msg);
std::cout << text;
},
.errorCallback = [](p::StringView msg) {
p::String text;
auto now = p::DateTime::Now();
now.ToString("[%Y/%m/%d %H:%M:%S]", text);
p::FormatTo(text, "[Error] {}\n", msg);
std::cout << text;
}
};

p::InitLog(&timestampedLogger);
p::Info("Server Started"); // [2026/09/04 12:00:00] [Info] server started
```

Outputs:
```
[2026/09/04 12:00:00] [Info] server started
```
182 changes: 182 additions & 0 deletions Docs/Specs/2026-09-04-pipe-tests-framework-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# PipeTests Framework — Design

Date: 2026-09-04
Status: Draft (pending spec review)

## Problem

The Pipe and Rift test suites rely on the vendored **Bandit** third-party test framework (`Extern/Pipe/Extern/Bandit/`) plus its bundled **Snowhouse** assertion library. Bandit is heavy, third-party, and its capture-scoping API does not fit the project's taste. We want to replace it step by step with a small, Pipe-native test framework owned by the project, following Pipe code style and the project's "simple solutions, C-like cpp, minimal templates, comments only when code is not enough" preference.

## Goals

- Provide a native test framework module **`PipeTests`** in the Pipe source tree.
- Used by **both** `PipeTests` and `RiftTests`.
- Mirror the bandit structure (`Spec`/`Describe`/`It`/`XIt`/`BeforeEach`/`AfterEach`) so migration is mechanical.
- Replace Snowhouse `AssertThat(x, Equals(y))` with a fluent `Expect(x).ToEqual(y)` style.
- Detailed failure messages (actual vs expected, `file:line`), extensible for user types.
- Remove the Bandit dependency entirely **only at the very end**, once the framework is proven.
- No new runtime burden on the shipped `Pipe` library.

## Non-Goals

- No exception-based matchers (`ToThrow`). Project disables RTTI project-wide and no current test throws; YAGNI.
- No (or minimal) CLI reporter/arg machinery in the first iteration.
- Not migrating the existing bandit tests or removing Bandit until the framework is complete and self-tested (see Migration Phasing).

## API Design

Style: **imgui-style global context**. All functions are global in namespace `p`; the user writes `using namespace p;`. There is no capture-scoped "only relevant functions" context — context is global and tracked as functions are called.

**Macro-free registration (2026-09-04):** The framework uses **no macros** — `Spec`/`Describe`/`It`/`XIt`/`BeforeEach`/`AfterEach` are plain functions. Because a bare function call is ill-formed at namespace scope (C++ permits only declarations there), specs **cannot self-register at global scope**. Registrations run from a function: each spec file exports a `RegisterXxxTests()` routine, and the test executable's `main()` calls each one before `p::RunTests`. `Spec(name, fn)` opens a first-level named group; `Spec(fn)` registers the top level only.

```cpp
using namespace p;

// Registration runs inside a function — never at namespace scope (no macros).
void RegisterGroupTests()
{
// Spec(name, fn) = go_bandit + first-level named group
Spec("Group", []() {
BeforeEach([]() { /* setup */ });
AfterEach([]() { /* teardown */ });

Describe("Sub", []() {
It("does something", []() {
Expect(value).ToEqual(4);
Expect(value).ToNotEqual(5);
Expect(value).ToBeLess(5);
Expect(value).ToBeLessOrEqual(4);
Expect(value).ToBeGreater(2);
});
XIt("disabled test", []() { /* never runs */ });
});

It("top-level test", []() {
Expect("acidic").ToContain("acid");
Expect(flag).ToBeTrue();
Expect(other).ToBeFalse();
});
});
}
```

### Top-level semantics

- `Spec(name, fn)` — top-level entry that **also** opens a first-level group named `name`. This is the go_bandit + first `Describe` replacement.
- `Spec(fn)` (nameless) — registers the top level only (like `go_bandit`), adding no extra group; `Describe` is then used inside it.
- `Describe` is **runtime-checked** to only be valid inside a `Spec`. If used outside a `Spec`: **log an error and ignore** the offending group (no exception).

### Functions (namespace `p`)

| Function | Role |
|----------|------|
| `Spec(name, fn)` / `Spec(fn)` | Top-level group; called from a `Register*Tests()` routine |
| `Describe(name, fn)` | Push a nested group; runtime-checked inside a `Spec` |
| `It(name, fn)` | Register a runnable leaf test in the current group |
| `XIt(name, fn)` | Register a leaf test marked **skipped** (never runs) |
| `BeforeEach(fn)` | Setup hook attached to the current group |
| `AfterEach(fn)` | Teardown hook attached to the current group |
| `Expect(value)` | Returns a fluent matcher over `value` |
| `RunTests(argc, argv)` | Runs the suite; returns process exit code |

### Assertions — `Expect(value)`

Fluent matcher methods, naming in Pipe CamelCase:

- `ToEqual(x)` / `ToNotEqual(x)`
- `ToBeLess(x)` / `ToBeLessOrEqual(x)` / `ToBeGreater(x)` / `ToBeGreaterOrEqual(x)`
- `ToBeTrue()` / `ToBeFalse()`
- `ToContain(sub)` / `ToNotContain(sub)` (strings / containers)

### Failure reporting

- On failure: print `file:line`, a description, and the **actual vs expected** values (detailed).
- Value stringification uses an **extensible formatter** customization point.
- Default support: arithmetic types via `std::format`/`std::to_string`; `StringView` / `const char*` / `std::string_view` for strings.
- Users extend by specializing/overloading a `TestFormatter<T>` (or `TestFormat(value)`) hook for their own types.
- Prefer `StringView` over `String`.

## Implementation

### New files (in the Pipe submodule)

- `Extern/Pipe/Include/PipeTest.h` — public API (global functions, `Expect` matcher, formatter hook). Mostly templates; **no macros**.
- `Extern/Pipe/Src/Tests/PipeTest.cpp` — function-local static `TestContext`, registration functions (`Spec`/`Describe`/`It`/`XIt`/`BeforeEach`/`AfterEach`), `p::RunTests(settings)` + `p::RunTests(int, char**)` argv forwarder.

### Build — separate target, not into the runtime Pipe lib

`Extern/Pipe/CMakeLists.txt`:

- Define `add_library(PipeTest ...)` (alias `Pipe::Test`) **unconditionally** (alongside `Pipe`, **before** the `PIPE_BUILD_TESTS` gate) so Rift can consume it via the submodule. (The suite executable keeps the `PipeTests` name with no alias.)
- **Exclude `Src/Tests/PipeTest.cpp`** from Pipe's `file(GLOB_RECURSE ... Src/*.cpp)` (currently line 65) so the test framework does **not** ship in the runtime `Pipe` library. Add it only to the `PipeTests` target.
- Give `PipeTests` the standard Pipe setup (`pipe_target_define_platform`, `pipe_target_enable_CPP20`, `pipe_target_disable_rtti`, `pipe_target_shared_output_directory`) and link `Pipe`; expose `Include/`.

### Runner

`p::RunTests(const TestSettings&) -> int` (plus an `argc`/`argv` overload that parses `--filter=X` / `--filter X` / positional into settings and forwards, so other systems can run tests without text args):

- Iterates the registered test tree, running only tests whose full name contains `settings.filter` (empty filter runs all).
- Runs each test, skipping `XIt`.
- Reports pass/fail/skip counts plus the names/locations of failures.
- Returns a process exit code (`0` when all pass).

### Global registration cursor

A current-describe cursor in `PipeTests.cpp` (`TestContext::currentDescribe`, accessed via `GetTestContext()`/`CurrentDescribe()`). `Describe` pushes its describe, runs `fn` (children register against it via the global functions), then pops. `It`/`XIt`/`BeforeEach`/`AfterEach` attach to the current describe. Pipe types throughout: `i32` counters, `TFunction<void()>` for immediately-invoked `Spec`/`Describe` callbacks, owning `std::function` for stored test bodies/hooks (`TFunction` is non-owning and would dangle).

## Migration Phasing

**Deliberate: do NOT migrate existing tests and do NOT remove third-party libraries until the very end, when the framework is done and self-tested.** Bandit stays linked and coexists throughout development.

1. **Add `PipeTest` module** — header + source; `add_library(PipeTest)`; source-glob exclusion. Build succeeds.
2. **Self-test the framework with small new tests** (no migration of existing tests):
- Create small **new** framework tests in `Extern/Pipe/Tests/PipeTests/` (e.g. `PipeTests.spec.cpp`) written with the new API to validate: `Spec`/`Describe`/`It`/`XIt`/`BeforeEach`/`AfterEach`, all `Expect` matchers, failure reporting, skip counting, `RunTests` exit code, and `Describe`-outside-`Spec` behavior. The spec file holds `Spec(...)` at file scope (auto-registers).
- Wire a **separate small runner** (its own `main.cpp` calling `p::RunTests`) for this smoke target, running **alongside** the existing bandit `PipeTests` executable.
- Verify via `ctest` that **both** the new self-tests and the untouched bandit suite pass. Iterate until the framework is proven.
3. **Final flip (LAST, only when the system is done):**
- Migrate existing `*spec.cpp` files file-by-file (transform map below). Each migrated file holds `Spec(...)` at file scope (auto-registers; no wrapper, no `main` changes).
- `Extern/Pipe/Tests/CMakeLists.txt`: keep suite exe `PipeTests` (no alias), link `Pipe` + `PipeTest`, drop `Bandit`; `main.cpp` calls `p::RunTests(argc, argv)`; drop `--reporter=spec`.
- Migrate Rift `Tests/*.spec.cpp` + `Tests/CMakeLists.txt`: replace `Bandit` with `Pipe::Test` (the alias); Rift's `Tests/main.cpp` only swaps `bandit::run` for `p::RunTests`.
- Remove `Bandit`: delete the `Bandit` INTERFACE target from `Extern/Pipe/Extern/CMakeLists.txt`, remove the vendored `Extern/Pipe/Extern/Bandit/` directory, strip remaining bandit includes.
- Final full `ctest` + `ClangFormat`/`ClangTidy` pass.

### Transform map (for step 3)

| Before (bandit) | After (PipeTests) |
|-----------------|-------------------|
| `#include <bandit/bandit.h>` + `using namespace snowhouse; using namespace bandit;` | `#include <PipeTest.h>` + `using namespace p;` |
| `go_bandit([](){ describe("G", ...) })` (global scope) | `Spec("G", [](){ ... })` inside a TU-local static registrar (`namespace { const bool autoRegistered = [](){ Spec(...); return true; }(); }`); auto-registers, no `main` changes |
| `describe(...)` | `Describe(...)` |
| `it(...)` | `It(...)` |
| `xit(...)` | `XIt(...)` |
| `before_each(...)` | `BeforeEach(...)` |
| `after_each(...)` | `AfterEach(...)` |
| `AssertThat(v, Equals(x))` | `Expect(v).ToEqual(x)` |
| `AssertThat(v, !Equals(x))` | `Expect(v).ToNotEqual(x)` |
| `AssertThat(x, Equals(true/false))` | `Expect(x).ToBeTrue()/ToBeFalse()` |
| `<` / `<=` relations | `ToBeLess` / `ToBeLessOrEqual` |
| contains checks | `ToContain` / `ToNotContain` |

Note: existing test files that `using namespace snowhouse; using namespace bandit;` also add `using namespace p;` where Pipe types are used (e.g. `StringView.spec.cpp`). After migration these files use only `using namespace p;`.

## Wiring facts (verified)

- `Bandit` INTERFACE target is defined in `Extern/Pipe/Extern/CMakeLists.txt`, added **unconditionally** (Pipe `CMakeLists.txt` line 47 `add_subdirectory(Extern)` runs before the `PIPE_BUILD_TESTS` gate).
- Pipe supplies its own tests executable gated behind `PIPE_BUILD_TESTS` (default `PIPE_IS_PROJECT`, i.e. ON when Pipe is the top project).
- Rift builds its **own** `RiftTests` executable in `Tests/CMakeLists.txt`, linking `RiftASTLib` + `Bandit` (imported through Pipe's `Extern`).
- Therefore the `PipeTest` target (alias `Pipe::Test`) must be defined **unconditionally** in Pipe's `CMakeLists.txt` so Rift can link it.

## Constraints & style

- Namespace `p`; CamelCase functions; `camelBack` parameters/variables; tabs; 100-col limit; `.clang-format` (Microsoft base).
- Minimal templates; comments only where code is insufficient.
- No exceptions / no RTTI (`-fno-rtti` project-wide).
- Prefer `StringView` over `String`.

## Open Questions

- Confirmed during design: no `ToThrow` (see Non-Goals); `Describe` misuse logs + ignores; `RunTests(settings)` + `RunTests(int, char**)` signatures; API names `Spec/Describe/It/XIt/BeforeEach/AfterEach/Expect` in Pipe CamelCase.

## Out of Scope / Follow-ups

- Reporter selection (deferred).
1 change: 0 additions & 1 deletion Extern/Bandit
Submodule Bandit deleted from a16c74
4 changes: 0 additions & 4 deletions Extern/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@

set(BUILD_SHARED_LIBS OFF)
set(EFSW_INSTALL OFF)


add_library(Bandit INTERFACE)
target_include_directories(Bandit INTERFACE Bandit)
17 changes: 8 additions & 9 deletions Include/Misc/PipeDebug.h
Original file line number Diff line number Diff line change
Expand Up @@ -2571,8 +2571,8 @@ namespace p
{
break;
}
const p::Color ac = details::GetArenaColor(va.arena->GetTypeId());
const ImU32 dotCol = ac.DWColor();
const p::Color ac = details::GetArenaColor(va.arena->GetTypeId());
const ImU32 dotCol = ac.DWColor();
sizet arenaUsed = 0;
sizet arenaCapacity = 0;
bool found = false;
Expand Down Expand Up @@ -2614,16 +2614,16 @@ namespace p
static String usedStr;
static String capStr;
static String line;
const float h = ImGui::GetTextLineHeight();
const float sq = h * 0.7f;
const float h = ImGui::GetTextLineHeight();
const float sq = h * 0.7f;
const float pad = (h - sq) * 0.5f;
for (i32 r = 0; r < rowCount; ++r)
{
const TipRow& row = rows[r];
usedStr.clear();
Strings::ParseMemorySizeTo(usedStr, row.used);
const ImVec4 col{row.col.r / 255.0f, row.col.g / 255.0f,
row.col.b / 255.0f, 1.0f};
const ImVec4 col{
row.col.r / 255.0f, row.col.g / 255.0f, row.col.b / 255.0f, 1.0f};
const ImVec2 p = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(p.x, p.y + pad),
ImVec2(p.x + sq, p.y + pad + sq), row.col.DWColor());
Expand Down Expand Up @@ -4464,9 +4464,8 @@ namespace p
dst.parentArenaIdx = src.parentArenaIdx;

// Deep-copy live allocs so the capture owns its data.
dst.ownedLiveAllocs = src.captured
? src.ownedLiveAllocs
: (src.live ? *src.live : TSet<MemoryStatsEvent>{});
dst.ownedLiveAllocs = src.captured ? src.ownedLiveAllocs
: (src.live ? *src.live : TSet<MemoryStatsEvent>{});
dst.captured = true;
dst.live = nullptr;

Expand Down
2 changes: 1 addition & 1 deletion Include/Pipe/Core/Broadcast.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ namespace p
{
if (instance && method)
{
if constexpr (IsObject<Type>)
if constexpr (Derived<Type, class BaseObject, false>)
{
return Bind<Type>(instance->AsPtr(), Move(method));
}
Expand Down
Loading
Loading