First attempt

This commit is contained in:
2026-05-08 17:48:16 +02:00
commit 5e77dc836b
36 changed files with 4806 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
cmake_minimum_required(VERSION 3.21)
# ── Catch2 ────────────────────────────────────────────────────────────────────
find_package(Catch2 3 QUIET)
if(NOT Catch2_FOUND)
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.3
)
FetchContent_MakeAvailable(Catch2)
endif()
# ── Google Test ───────────────────────────────────────────────────────────────
find_package(GTest QUIET)
if(NOT GTest_FOUND)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
endif()
# ── Test executable ───────────────────────────────────────────────────────────
add_executable(kpn_tests
test_fixed_string.cpp
test_traits.cpp
test_channel.cpp
test_node.cpp
test_network.cpp
)
target_link_libraries(kpn_tests PRIVATE
kpn
Catch2::Catch2WithMain
GTest::gtest
)
include(CTest)
include(Catch)
catch_discover_tests(kpn_tests)
+99
View File
@@ -0,0 +1,99 @@
#include <catch2/catch_test_macros.hpp>
#include <kpn/channel.hpp>
#include <thread>
using namespace kpn;
TEST_CASE("channel storage policy: small trivial type by value", "[channel]") {
STATIC_REQUIRE(channel_storage_policy<int>::by_value);
STATIC_REQUIRE(channel_storage_policy<float>::by_value);
}
TEST_CASE("channel storage policy: large type by shared_ptr", "[channel]") {
struct Big { char data[64]; };
STATIC_REQUIRE(!channel_storage_policy<Big>::by_value);
}
TEST_CASE("push and pop single value", "[channel]") {
Channel<int> ch(5);
ch.push(42);
REQUIRE(ch.pop() == 42);
}
TEST_CASE("channel respects capacity", "[channel]") {
Channel<int> ch(2);
ch.push(1);
ch.push(2);
REQUIRE_THROWS_AS(ch.push(3), ChannelOverflowError);
}
TEST_CASE("pop blocks until value available", "[channel]") {
Channel<int> ch(5);
int result = 0;
std::thread producer([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
ch.push(99);
});
result = ch.pop();
producer.join();
REQUIRE(result == 99);
}
TEST_CASE("try_pop returns false on timeout", "[channel]") {
Channel<int> ch(5);
int out = 0;
REQUIRE_FALSE(ch.try_pop(out, std::chrono::milliseconds(10)));
}
TEST_CASE("try_pop succeeds when value present", "[channel]") {
Channel<int> ch(5);
ch.push(7);
int out = 0;
REQUIRE(ch.try_pop(out, std::chrono::milliseconds(10)));
REQUIRE(out == 7);
}
TEST_CASE("disable unblocks waiting pop", "[channel]") {
Channel<int> ch(5);
std::thread disabler([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
ch.disable();
});
REQUIRE_THROWS_AS(ch.pop(), ChannelClosedError);
disabler.join();
}
TEST_CASE("push to disabled channel is silently dropped", "[channel]") {
Channel<int> ch(5);
ch.disable();
ch.push(99); // must not throw, must not enqueue
REQUIRE(ch.size() == 0);
}
TEST_CASE("disable clears existing queue contents", "[channel]") {
Channel<int> ch(5);
ch.push(1);
ch.push(2);
REQUIRE(ch.size() == 2);
ch.disable();
REQUIRE(ch.size() == 0);
}
TEST_CASE("enable re-accepts pushes after disable", "[channel]") {
Channel<int> ch(5);
ch.disable();
ch.push(1);
REQUIRE(ch.size() == 0);
ch.enable();
ch.push(42);
REQUIRE(ch.pop() == 42);
}
TEST_CASE("large type stored as shared_ptr — no copy on pop", "[channel]") {
struct Big { char data[64]; int tag; };
Channel<Big> ch(5);
Big b{}; b.tag = 123;
ch.push(b);
auto out = ch.pop();
REQUIRE(out.tag == 123);
}
+27
View File
@@ -0,0 +1,27 @@
#include <catch2/catch_test_macros.hpp>
#include <kpn/fixed_string.hpp>
using namespace kpn;
TEST_CASE("fixed_string equality", "[fixed_string]") {
constexpr fixed_string a("img");
constexpr fixed_string b("img");
constexpr fixed_string c("sigma");
STATIC_REQUIRE(a == b);
STATIC_REQUIRE(!(a == c));
}
TEST_CASE("fixed_string view", "[fixed_string]") {
constexpr fixed_string s("hello");
REQUIRE(s.view() == "hello");
}
TEST_CASE("index_of hit", "[fixed_string]") {
constexpr auto idx = index_of<fixed_string("b"), fixed_string("a"), fixed_string("b"), fixed_string("c")>();
STATIC_REQUIRE(idx == 1);
}
TEST_CASE("index_of miss returns npos", "[fixed_string]") {
constexpr auto idx = index_of<fixed_string("z"), fixed_string("a"), fixed_string("b")>();
STATIC_REQUIRE(idx == npos);
}
+56
View File
@@ -0,0 +1,56 @@
#include <catch2/catch_test_macros.hpp>
#include <kpn/kpn.hpp>
#include <chrono>
#include <thread>
using namespace kpn;
static int increment(int x) { return x + 1; }
static int multiply2(int x) { return x * 2; }
TEST_CASE("network build and run: linear pipeline", "[network]") {
// Nodes declared first — they own their input channels and must outlive the network
auto src = make_node<increment>(5);
auto dst = make_node<multiply2>(5);
auto& src_in = src.input_channel<0>();
Channel<int> final_out(5);
dst.set_output_channel<0>(&final_out);
Network net;
net.add("src", src)
.add("dst", dst)
.connect("src", src.output<0>(), "dst", dst.input<0>())
.build();
net.start();
src_in.push(5); // 5 → increment → 6 → multiply2 → 12
int result = final_out.pop();
net.stop();
REQUIRE(result == 12);
}
TEST_CASE("network detects cycle", "[network]") {
auto a = make_node<increment>(5);
auto b = make_node<increment>(5);
Network net;
net.add("a", a).add("b", b);
net.connect("a", a.output<0>(), "b", b.input<0>());
net.connect("b", b.output<0>(), "a", a.input<0>());
REQUIRE_THROWS_AS(net.build(), NetworkCycleError);
}
TEST_CASE("stop disables input channels — producer push is silently dropped", "[network]") {
auto node = make_node<increment>(5);
auto& in_ch = node.input_channel<0>();
node.start();
node.stop();
// After stop, channel is disabled — push must not throw
in_ch.push(99);
REQUIRE(in_ch.size() == 0);
}
+49
View File
@@ -0,0 +1,49 @@
#include <catch2/catch_test_macros.hpp>
#include <kpn/node.hpp>
#include <chrono>
#include <thread>
using namespace kpn;
static int double_it(int x) { return x * 2; }
static std::tuple<int, float> split_it(int x) { return {x, float(x) * 0.5f}; }
static void consume_it(int x) { (void)x; }
TEST_CASE("node input/output counts", "[node]") {
STATIC_REQUIRE(Node<double_it>::input_count == 1);
STATIC_REQUIRE(Node<double_it>::output_count == 1);
STATIC_REQUIRE(Node<split_it>::output_count == 2);
STATIC_REQUIRE(Node<consume_it>::output_count == 0);
}
TEST_CASE("node named port index resolution", "[node]") {
using N = Node<double_it, in<"value">, out<"result">>;
STATIC_REQUIRE(index_of<fixed_string("value"), fixed_string("value")>() == 0);
STATIC_REQUIRE(index_of<fixed_string("result"), fixed_string("result")>() == 0);
}
TEST_CASE("node processes a single item end-to-end", "[node]") {
Node<double_it> src_node(5); // used only as input source placeholder
Node<double_it> node(5);
// Manually wire: push to input channel, connect a downstream channel, run one item
auto& in_ch = node.input_channel<0>();
Channel<int> out_ch(5);
node.set_output_channel<0>(&out_ch);
node.start();
in_ch.push(21);
int result = out_ch.pop();
node.stop();
REQUIRE(result == 42);
}
TEST_CASE("node stop unblocks cleanly", "[node]") {
Node<double_it> node(5);
node.start();
// Node is blocked waiting for input — stop() must return without deadlock
node.stop();
REQUIRE_FALSE(node.running());
}
+42
View File
@@ -0,0 +1,42 @@
#include <catch2/catch_test_macros.hpp>
#include <kpn/traits.hpp>
#include <tuple>
using namespace kpn;
static int free_func(int a, float b) { return a; }
static void sink_func(int a) {}
static std::tuple<int, float> multi_func(int a) { return {a, 1.f}; }
TEST_CASE("arity of free function", "[traits]") {
STATIC_REQUIRE(arity_v<decltype(free_func)> == 2);
}
TEST_CASE("return type of free function", "[traits]") {
STATIC_REQUIRE(std::is_same_v<return_t<decltype(free_func)>, int>);
}
TEST_CASE("normalised return: single value", "[traits]") {
STATIC_REQUIRE(std::is_same_v<normalised_return_t<int>, std::tuple<int>>);
}
TEST_CASE("normalised return: void", "[traits]") {
STATIC_REQUIRE(std::is_same_v<normalised_return_t<void>, std::tuple<>>);
}
TEST_CASE("normalised return: tuple passthrough", "[traits]") {
using T = std::tuple<int, float>;
STATIC_REQUIRE(std::is_same_v<normalised_return_t<T>, T>);
}
TEST_CASE("output_count: single return", "[traits]") {
STATIC_REQUIRE(output_count_v<decltype(free_func)> == 1);
}
TEST_CASE("output_count: void return", "[traits]") {
STATIC_REQUIRE(output_count_v<decltype(sink_func)> == 0);
}
TEST_CASE("output_count: tuple return", "[traits]") {
STATIC_REQUIRE(output_count_v<decltype(multi_func)> == 2);
}