diff --git a/CMakeLists.txt b/CMakeLists.txt index e3c8be61..75c3021c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,6 @@ pipe_target_define_platform(Pipe) target_include_directories(Pipe PUBLIC $) target_include_directories(Pipe PRIVATE $) 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) @@ -83,19 +82,6 @@ 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 $) -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 diff --git a/Docs/PipeTest.md b/Docs/PipeTest.md new file mode 100644 index 00000000..dc224011 --- /dev/null +++ b/Docs/PipeTest.md @@ -0,0 +1,234 @@ +--- +title: Pipe Test +--- +**Header:** [``](https://github.com/PipeRift/pipe/blob/main/Include/PipeTest.h)`] +**Namespace:** `p` +**Uses:** [`PipeReflect`](./PipeECS.md) `PipeStrings` `PipeTime` + +## Overview + +`PipeTest` is test framework built into Pipe, used by the library to test itself. +It is heavily inspired by `Bandit`, following similar APIs and outputs to fully support existing IDE tools built for that library. + +```cpp +#include + +using namespace p; + +P_SPEC("Math", []() +{ + BeforeEach([]() { /* runs before each test */ }); + + It("clamps to range", []() + { + Expect(Clamp(5, 0, 3)).ToEqual(3); + }); + + XIt("disabled case", []() + { + // never runs + }); + + AfterEach([]() { /* runs after each test */ }); +}); +``` + +## Philosophy + +A few guiding decisions shape the whole test API. + +- **Macro-free registration:** Registration of tests (`Describe`, `It`, `XIt`, `BeforeEach`, `AfterEach`) are plain functions. The only macro is `P_SPEC`, which it is just syntax sugar to statically register an spec. +- **Immediate global context:** All registration functions live in namespace `p` and operate on a global `TestContext`. +- **Fluent assertions:** `Expect(x)` followed by `ToEqual(y)`, `ToBeTrue()`, `ToContain(sub)`, etc to cover many different testing scenarios. +- **Extensible value formatting:** Failure messages are messaged in a human-readable way, and extensible by the user for new types. +- **Mirror bandit's vocabulary:** `Describe`/`It`/`XIt`/`BeforeEach`/`AfterEach`, `--only`/`--skip`/`--reporter`/`--dry-run`, `Spec`/`Dots`/`Singleline`/`Info` reporters. Migrating existing tests is trivial. +- **No exceptions / no RTTI:** Pipe doesn't require or use RTTI. This is the same with PipeTests. + +## Writing Tests +On any cpp file in our test module, we can define one or more specs. + +```cpp +// Specs register tests inside and optionally adds root Describe("Files.Paths") +P_SPEC("Files.Paths", []() +{ + BeforeEach([]() { /* setup */ }); + + Describe("Join", []() + { + // Describes group all tests inside. They can be nested. + It("joins two segments", []() + { + Expect(Join("a", "b")).ToEqual("a/b"); + }); + + It("joins three segments", []() + { + Expect(Join("a", "b", "c")).ToEqual("a/b/c"); + }); + + XIt("join four segments", []() { /* 'XIt' are disabled tests */ }); + }); + + It("top-level test", []() + { + Expect(true).ToBeTrue(); + Expect(false).ToBeFalse(); + }); + + AfterEach ([]() { /* teardown */ }); +}); +``` + +> [!Note] +> Because registration runs from a constructor (`SpecAutoRegister`), `P_SPEC` can only appear at namespace scope. Inside a function, register the spec directly with `RegisterSpec(name, callback)`. + +## Running Tests + +`PipeTest` exposes two runner entry points: + +```cpp +int RunTests(const TestSettings& settings); +int RunTests(int argc, char** argv); +``` + +The second overload parses CLI args into `TestSettings`. + +In CLI, simply call your test binary. You can use these args: + +| Argument | Effect | +| ------------------------------------- | --------------------------------------------------------------- | +| `--only=` | Run only describes/its whose full name contains `` | +| `--skip=` | Skip describes/its whose full name contains `` | +| `--break-on-failure` | Stop on the first failing test | +| `--dry-run`, `-l`, `--list` | Report the full tree as SKIPPED, run nothing | +| `--report-timing` | Report per-test and total run durations | +| `-r=`, `--reporter=` | Select reporter by (case-insensitive) name — see [Reporter Name Lookup](#Reporter-Name-Lookup) | +| `--colorizer=off`, `--no-color`, `-c` | Disable colorized output | +| `--version` | Print Pipe version and exit | +| `--help` | Print usage and exit | + +## Reporters + +Reporters share the same per-test execution flow but format the run their own way: + +| Reporter | Style | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `spec`
(default) | Indented contexts, `- it ... OK` lines, summary block. Compatible with the Bandit VSCode adapter (one tab per indent level — any other indent breaks hierarchy detection). | +| `dots` | One character per test, color-coded, wrapped at a fixed line width. | +| `singleline` | Live overwriting status line on real terminals (`Executed N tests.`). On redirected streams it prints nothing per-test, only the final summary. | +| `info` | Verbose: `begin/end ` lines, per-context totals (`{n} total`, `{n} skipped`, `{n} failed`), `[ PASS ]`/`[ FAIL ]`/`-ERROR->` test lines, and a final failures list. | + +`--report-timing` adds a colored duration suffix (e.g. ` (0.00012s)` / `(0.4us)`) to every test line and a total-time line at the end in the `info` reporter. + +Custom reporters are supported by inheriting `p::ITestReporter`. +They can be called by their name (`--reporter=SpecialReporter` or just `--reporter=Special`): +```cpp +struct SpecialReporter : p::ITestReporter +{ + using Super = p::ITestReporter; + P_STRUCT(SpecialReporter) + + // ... override the reporter callbacks ... +}; +``` + +## Filters & Behavior + +- Tests not matched by `--only`, matched by `--skip`, marked `XIt`, or after a `--break-on-failure` trigger are all reported as **SKIPPED** (bandit semantics). They are not hidden — only excluded from execution. +- Failures are buffered during the run and emitted by the reporter at the end, so they don't interleave with deferred context output. + +--- + +## Reference +### `P_SPEC` Macro +Automatically registers an battery of tests. + +```cpp +P_SPEC("Topic", []() +{ + // body +}); +``` + +```cpp +#define P_SPEC static const p::SpecAutoRegister P_CAT(_pipeSpecReg_, __COUNTER__) +``` +### Registration Functions + +All in namespace `p`. Bodies in `P_SPEC` / `Describe` are stored as `TFunction` (non-owning, called immediately during registration). Test bodies and hooks are stored as `std::function` (owning) because they run later from `RunTests`. + +| Function | Signature | Purpose | +| ------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `RegisterSpec` | `(StringView name, TFunction fn)` | Internal: pushes a named first-level describe, runs `fn`, pops. | +| `RegisterSpec` | `(TFunction fn)` | Runs `fn` against the virtual root describe. | +| `SpecAutoRegister` | ctor `(StringView, TFunction)` / `(TFunction)` | Static-init registrar used by `P_SPEC`. | +| `Describe` | `(StringView name, TFunction fn)` | Push a nested describe. Runtime-checked inside a `Spec`; logs + ignores otherwise. | +| `It` | `(StringView name, std::function fn)` | Register a runnable leaf test. | +| `XIt` | `(StringView name, std::function fn)` | Register a leaf test that is never run. | +| `BeforeEach` | `(std::function fn)` | Attach a setup hook to the current describe. | +| `AfterEach` | `(std::function fn)` | Attach a teardown hook to the current describe. | + +### `Expect(value)` — Fluent Matchers + +`Expect(value, loc = std::source_location::current())` returns an `ExpectValue` bound to the caller's `file:line`. The matchers available today: + +| Matcher | Signature | Failure message | +|---------|-----------|-----------------| +| `ToEqual(expected)` | `template void ToEqual(const E&) const` | `Expected {actual} to equal {expected}` | +| `ToNotEqual(expected)` | `template void ToNotEqual(const E&) const` | `Expected {actual} to not equal {expected}` | +| `ToBeLess(other)` | `void ToBeLess(const Actual&) const` | `Expected {actual} to be less than {other}` | +| `ToBeLessOrEqual(other)` | `void ToBeLessOrEqual(const Actual&) const` | `Expected {actual} to be less or equal to {other}` | +| `ToBeGreater(other)` | `void ToBeGreater(const Actual&) const` | `Expected {actual} to be greater than {other}` | +| `ToBeGreaterOrEqual(other)` | `void ToBeGreaterOrEqual(const Actual&) const` | `Expected {actual} to be greater or equal to {other}` | +| `ToBeTrue()` | `void ToBeTrue() const` | `Expected value to be true` | +| `ToBeFalse()` | `void ToBeFalse() const` | `Expected value to be false` | +| `ToContain(sub)` | `void ToContain(StringView) const` | `Expected {actual} to contain {sub}` | +| `ToNotContain(sub)` | `void ToNotContain(StringView) const` | `Expected {actual} to not contain {sub}` | + +- **String-aware equality.** `ToEqual`/`ToNotEqual` treat string-like operands (`StringView`, `const char*`, `std::string`, `p::String`) as strings and compare via `StringView`; everything else uses `operator==`. +- **Containment.** `ToContain`/`ToNotContain` require a `StringView` substring; the actual value is constructed into a `StringView` view for the search. +- **Source location.** The matcher's `file:line` is captured at the call site via `std::source_location` defaults, so failure reports point at the assertion line, not inside the framework. +- **Assertion counter.** Every matcher increments an internal counter. Tests that ran zero asserts are surfaced by reporters as "no assertions" rather than `[ PASS ]`. + +## `TestString` — Custom Value Formatting + +Failure messages format actual/expected values through `String TestString(const T&)`. The default implementation handles two cases: +- **Formattable types** (`std::formatter, char>` exists): `Format("{}", value)`. +- **Non-formattable types** (structs, byte views): a generic placeholder `` so the framework stays compilable for opaque types. + +Predefined specializations and overloads: `bool` (`true`/`false`), `char`, `StringView`, `p::String`, `const char*` (`(null)` for null), `std::string`, and any pointer type (`0xADDR`). + +Extend for your own types by specializing the template: + +```cpp +template<> +inline String p::TestString(const MyType& v) +{ + return Format("MyType({})", v.id); +} +``` + +Prefer `StringView` overloads over `String` so views and literals both work without allocation. + +### `TestSettings` + +```cpp +struct TestSettings +{ + StringView only; // Run only describes/its containing substring. + StringView skip; // Skip describes/its containing substring. + bool dryRun = false; // Report full tree as SKIPPED, run nothing. + bool breakOnFailure = false; // Stop the test run on the first failing test. + bool useColor = true; // Colorized output. + bool reportTiming = false; // Report per-test timing information. + TTypeId reporter; // Reporter id; default = SpecReporter. +}; +``` + +Process exit code: `0` when every test passed, `1` otherwise. + +### Failure Reporting + +- Failures are deferred: per-test assertions only update a per-test buffer (`file:line: msg`); the reporter flushes them at the end of the run. +- Each failed test contributes one entry to the global `failures` list, formatted as `context.test:\n
\n`. +- Failure messages are written to **stdout** (not stderr) so test-tooling parsers that follow bandit's convention (e.g. the Bandit VSCode adapter) keep working. diff --git a/Include/Pipe/Core/StringView.h b/Include/Pipe/Core/StringView.h index 94ff4d99..cb6f3fad 100644 --- a/Include/Pipe/Core/StringView.h +++ b/Include/Pipe/Core/StringView.h @@ -2,6 +2,7 @@ #pragma once +#include "Pipe/Core/Char.h" #include "Pipe/Core/Hash.h" #include "Pipe/Core/Optional.h" #include "PipePlatform.h" @@ -84,6 +85,23 @@ namespace p return str.size() == 1 && str[0] == c; } + template + constexpr bool IEquals(const TStringView str, const TStringView other) + { + return str.size() == other.size() + && std::equal(str.begin(), str.end(), other.begin(), [](CharType a, CharType b) + { + return TCharHelpers::ToLower(a) == TCharHelpers::ToLower(b); + }); + } + + template + constexpr bool IEquals(const TStringView str, const CharType c) + { + return str.size() == 1 + && TCharHelpers::ToLower(str[0]) == TCharHelpers::ToLower(c); + } + template constexpr bool StartsWith( const TStringView str, const TStringView subStr) @@ -228,6 +246,16 @@ namespace p return Equals(str, c); } + P_API inline bool IEquals(const StringView str, const StringView other) + { + return IEquals(str, other); + } + + P_API constexpr bool IEquals(const StringView str, const char c) + { + return IEquals(str, c); + } + P_API constexpr bool StartsWith(const StringView str, const StringView subStr) { return StartsWith(str, subStr); diff --git a/Include/PipeTest.h b/Include/PipeTest.h index 960416dd..f1ca811e 100644 --- a/Include/PipeTest.h +++ b/Include/PipeTest.h @@ -36,8 +36,8 @@ namespace p requires(const T& value) { std::formatter, Char>{}; }; } // namespace details - void RegisterSpec(StringView name, TFunction fn); - void RegisterSpec(TFunction fn); + P_API void RegisterSpec(StringView name, TFunction fn); + P_API void RegisterSpec(TFunction fn); // Self-registering top-level describe. Registers its spec body on // construction (same pattern as TTypeAutoRegister). The body runs @@ -70,20 +70,42 @@ namespace p #define P_SPEC static const p::SpecAutoRegister P_CAT(_pipeSpecReg_, __COUNTER__) // Nested describe. Only valid inside a Spec; otherwise logs an error and ignores. - void Describe(StringView name, TFunction fn); + P_API void Describe(StringView name, TFunction fn); // Register a runnable test in the current describe. // Bodies are stored until RunTests, so std::function (owning) is required here. - void It(StringView name, std::function fn); + P_API void It(StringView name, std::function fn); // Register a disabled test; never run. - void XIt(StringView name, std::function fn); + P_API void XIt(StringView name, std::function fn); // Setup hook attached to the current describe. - void BeforeEach(std::function fn); + P_API void BeforeEach(std::function fn); // Teardown hook attached to the current describe. - void AfterEach(std::function fn); + P_API void AfterEach(std::function fn); - // Reporter interface (defined in Src/Tests/PipeTest.cpp). Forward - // declaration so TestSettings can reference its type id. - struct ITestReporter; + + // Reporter interface. Each reporter formats the run differently. + struct P_API ITestReporter + { + P_STRUCT(ITestReporter) + + virtual ~ITestReporter() = default; + + virtual void TestRunStarting() {} + virtual void TestRunComplete() = 0; + virtual void ContextStarting(StringView) {} + virtual void ContextEnded(StringView) {} + virtual void ItStarting(StringView) {} + virtual void ItSucceeded(StringView) {} + // Test passed but made no assertions (e.g. smoke tests). + virtual void ItSucceededNoAssertions(StringView) {} + virtual void ItFailed(StringView) {} + virtual void ItUnknownError(StringView) {} + virtual void ItSkipped(StringView) {} + }; + + + // Finds a registered reporter type by name (case-insensitive). Matches + // either the full type name ("SpecReporter") or its preffix ("Spec"). + P_API TTypeId FindReporter(StringView name); // Settings for a test run. struct TestSettings @@ -100,8 +122,8 @@ namespace p TTypeId reporter; }; - int RunTests(const TestSettings& settings); - int RunTests(int argc, char** argv); + P_API int RunTests(const TestSettings& settings); + P_API int RunTests(int argc, char** argv); // Extensible value-to-string hook for failure messages. @@ -167,10 +189,10 @@ namespace p namespace details { // Format failure message from source location + description. - void Fail(const std::source_location& loc, StringView message); + P_API void Fail(const std::source_location& loc, StringView message); // Counts an assertion. Used to detect tests that ran no expects. - void CountAssert(); + P_API void CountAssert(); // Detect types constructible into a StringView (string-ish values). template diff --git a/Include/PipeType.h b/Include/PipeType.h index 0c26690c..b7730d29 100644 --- a/Include/PipeType.h +++ b/Include/PipeType.h @@ -370,4 +370,4 @@ struct std::formatter> : public std::formatter inline consteval p::StringView p::GetFullTypeName(bool includeNamespaces) \ { \ return name; \ - } \ No newline at end of file + } diff --git a/Src/Tests/PipeTest.cpp b/Src/PipeTest.cpp similarity index 92% rename from Src/Tests/PipeTest.cpp rename to Src/PipeTest.cpp index 91461f49..7527c4bd 100644 --- a/Src/Tests/PipeTest.cpp +++ b/Src/PipeTest.cpp @@ -1,17 +1,10 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. -#ifndef P_OVERRIDE_NEWDELETE - #define P_OVERRIDE_NEWDELETE 1 -#endif -#if P_OVERRIDE_NEWDELETE - #include "PipeNewDelete.h" -#endif +#include "PipeTest.h" -#include "Pipe.h" #include "Pipe/Core/Log.h" #include "Pipe/Memory/OwnPtr.h" #include "PipeStrings.h" -#include "PipeTest.h" #include "PipeTime.h" #include @@ -69,8 +62,6 @@ namespace p // Assertion detail accumulated for the current test (file:line: msg). String currentFailureDetail; - // Id of the selected reporter (matches settings.reporter). - TTypeId reporter{}; bool useColor = true; bool reportTiming = false; @@ -124,12 +115,12 @@ namespace p namespace details { - void CountAssert() + P_API void CountAssert() { ++GetTestContext().currentTestAssertCount; } - void Fail(const std::source_location& loc, StringView message) + P_API void Fail(const std::source_location& loc, StringView message) { // Not printed immediately: failures are reported by the reporter // at the end of the run, so they never interleave with deferred @@ -145,7 +136,7 @@ namespace p } } // namespace details - void RegisterSpec(StringView name, TFunction fn) + P_API void RegisterSpec(StringView name, TFunction fn) { TestContext& context = GetTestContext(); context.currentDescribe = &AddDescribe(context.root, name); @@ -153,7 +144,7 @@ namespace p context.currentDescribe = nullptr; } - void RegisterSpec(TFunction fn) + P_API void RegisterSpec(TFunction fn) { TestContext& context = GetTestContext(); context.currentDescribe = &context.root; @@ -161,7 +152,7 @@ namespace p context.currentDescribe = nullptr; } - void Describe(StringView name, TFunction fn) + P_API void Describe(StringView name, TFunction fn) { TestDescribe*& current = CurrentDescribe(); if (!current) @@ -176,17 +167,17 @@ namespace p current = prevDescribe; } - void It(StringView name, std::function fn) + P_API void It(StringView name, std::function fn) { AddTest("It", name, Move(fn), false); } - void XIt(StringView name, std::function fn) + P_API void XIt(StringView name, std::function fn) { AddTest("XIt", name, Move(fn), true); } - void BeforeEach(std::function fn) + P_API void BeforeEach(std::function fn) { TestDescribe*& current = CurrentDescribe(); if (!current) @@ -197,7 +188,7 @@ namespace p current->beforeEach = fn; } - void AfterEach(std::function fn) + P_API void AfterEach(std::function fn) { TestDescribe*& current = CurrentDescribe(); if (!current) @@ -208,25 +199,25 @@ namespace p current->afterEach = fn; } - // Reporter interface (public so TestSettings can reference its type id). - // Mirrors bandit's reporter callbacks. Each reporter formats the run - // differently; all share the same per-test execution flow in RunNested. - struct ITestReporter + P_API TTypeId FindReporter(StringView name) { - virtual ~ITestReporter() = default; - - virtual void TestRunStarting() {} - virtual void TestRunComplete() = 0; - virtual void ContextStarting(StringView) {} - virtual void ContextEnded(StringView) {} - virtual void ItStarting(StringView) {} - virtual void ItSucceeded(StringView) {} - // Test passed but made no assertions (e.g. smoke tests). - virtual void ItSucceededNoAssertions(StringView) {} - virtual void ItFailed(StringView) {} - virtual void ItUnknownError(StringView) {} - virtual void ItSkipped(StringView) {} - }; + const TypeId baseId = GetTypeId(); + for (const TypeId id : GetRegisteredTypeIds()) + { + if (!IsTypeParentOf(baseId, id)) + { + continue; + } + + // "spec" matches SpecReporter; the full name also matches ("specreporter"). + const StringView typeName = RemoveNamespace(GetTypeName(id)); + if (Strings::IEquals(Strings::RemoveFromEnd(typeName, "Reporter"), name)) + { + return TTypeId{id}; + } + } + return TTypeId{TypeId{}}; // No match: invalid id + } namespace @@ -299,6 +290,9 @@ namespace p // bandit's `spec` reporter: indented contexts, "- it ... OK". struct SpecReporter : ITestReporter { + using Super = ITestReporter; + P_STRUCT(SpecReporter) + i32 indentation = 0; String lastIt; @@ -365,6 +359,9 @@ namespace p // lines (a fresh line every kLineWidth tests) rather than one line each. struct DotsReporter : ITestReporter { + using Super = ITestReporter; + P_STRUCT(DotsReporter) + bool anyResults = false; void ItSucceeded(StringView) override @@ -415,6 +412,9 @@ namespace p // exactly once. struct SinglelineReporter : ITestReporter { + using Super = ITestReporter; + P_STRUCT(SinglelineReporter) + SinglelineReporter() { #if P_PLATFORM_WINDOWS @@ -507,6 +507,9 @@ namespace p // and its own summary. Honors --report-timing on every test line. struct InfoReporter : ITestReporter { + using Super = ITestReporter; + P_STRUCT(InfoReporter) + // One entry per active describe, outermost first. struct ContextInfo { @@ -866,7 +869,7 @@ namespace p } // namespace - int RunTests(const TestSettings& settings) + P_API int RunTests(const TestSettings& settings) { #if P_PLATFORM_WINDOWS // Enable ANSI escape sequences on the Windows console, otherwise @@ -890,7 +893,6 @@ namespace p context.runTests = 0; context.failedTests = 0; context.skippedTests = 0; - context.reporter = settings.reporter; context.useColor = settings.useColor; context.reportTiming = settings.reportTiming; context.contextStack.Clear(); @@ -947,7 +949,7 @@ namespace p return context.failedTests == 0 ? 0 : 1; } - int RunTests(int argc, char** argv) + P_API int RunTests(int argc, char** argv) { TestSettings settings; for (i32 i = 1; i < argc; ++i) @@ -967,23 +969,8 @@ namespace p StringView name = Strings::RemoveFromStart(arg, StringView{"-r="}); name = Strings::RemoveFromStart(name, StringView{"--reporter="}); - if (Strings::Equals(name, StringView{"dots"})) - { - settings.reporter = TTypeId(); - } - else if (Strings::Equals(name, StringView{"singleline"})) - { - settings.reporter = TTypeId(); - } - else if (Strings::Equals(name, StringView{"spec"})) - { - settings.reporter = TTypeId(); - } - else if (Strings::Equals(name, StringView{"info"})) - { - settings.reporter = TTypeId(); - } - else + settings.reporter = FindReporter(name); + if (!settings.reporter) { Warning("PipeTest: unknown reporter '{}'. Using default.", name); } diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index a87397a4..ce189f36 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -8,7 +8,7 @@ pipe_target_enable_CPP20(PipeTests) pipe_target_disable_rtti(PipeTests PRIVATE) pipe_target_define_platform(PipeTests) pipe_target_shared_output_directory(PipeTests) -target_link_libraries(PipeTests PUBLIC Pipe PipeTest) +target_link_libraries(PipeTests PUBLIC Pipe) pipe_add_sanitizers(PipeTests) add_test(NAME PipeTests COMMAND $ --reporter=spec)