@@ -31,6 +31,7 @@ add_executable(kpn_tests
|
||||
test_channel.cpp
|
||||
test_node.cpp
|
||||
test_network.cpp
|
||||
test_static_network.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(kpn_tests PRIVATE
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <kpn/kpn.hpp>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
using namespace kpn;
|
||||
|
||||
static int increment(int x) { return x + 1; }
|
||||
static int multiply2(int x) { return x * 2; }
|
||||
static int multiply3(int x) { return x * 3; }
|
||||
static int add10(int x) { return x + 10; }
|
||||
static int negate_val(int x) { return -x; }
|
||||
static int square(int x) { return x * x; }
|
||||
|
||||
// ── Linear pipeline ───────────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("static_network: linear pipeline produces correct result", "[static_network]") {
|
||||
auto src = make_node<increment>(5);
|
||||
auto dst = make_node<multiply2>(5);
|
||||
|
||||
Channel<int> final_out(5);
|
||||
dst.set_output_channel<0>(&final_out);
|
||||
|
||||
auto net = make_network(
|
||||
edge(src.output<0>(), dst.input<0>())
|
||||
);
|
||||
|
||||
net.start();
|
||||
src.input_channel<0>().push(5); // 5 → increment → 6 → multiply2 → 12
|
||||
int result = final_out.pop();
|
||||
net.stop();
|
||||
|
||||
REQUIRE(result == 12);
|
||||
}
|
||||
|
||||
TEST_CASE("static_network: three-node pipeline", "[static_network]") {
|
||||
auto a = make_node<increment>(5);
|
||||
auto b = make_node<multiply2>(5);
|
||||
auto c = make_node<add10>(5);
|
||||
|
||||
Channel<int> out(5);
|
||||
c.set_output_channel<0>(&out);
|
||||
|
||||
auto net = make_network(
|
||||
edge(a.output<0>(), b.input<0>()),
|
||||
edge(b.output<0>(), c.input<0>())
|
||||
);
|
||||
|
||||
net.start();
|
||||
a.input_channel<0>().push(3); // 3 → +1=4 → *2=8 → +10=18
|
||||
int result = out.pop();
|
||||
net.stop();
|
||||
|
||||
REQUIRE(result == 18);
|
||||
}
|
||||
|
||||
// ── Auto fan-out ──────────────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("static_network: auto fanout delivers to both consumers", "[static_network]") {
|
||||
// Use distinct functions so each node has a distinct type in the graph
|
||||
auto src = make_node<increment>(8);
|
||||
auto dstA = make_node<multiply2>(8);
|
||||
auto dstB = make_node<multiply3>(8);
|
||||
|
||||
Channel<int> outA(8), outB(8);
|
||||
dstA.set_output_channel<0>(&outA);
|
||||
dstB.set_output_channel<0>(&outB);
|
||||
|
||||
// Two edges from the same output port — FanoutNode<int,2> is auto-inserted
|
||||
auto net = make_network(
|
||||
edge(src.output<0>(), dstA.input<0>()),
|
||||
edge(src.output<0>(), dstB.input<0>())
|
||||
);
|
||||
|
||||
net.start();
|
||||
src.input_channel<0>().push(3); // 3 → +1=4 → *2=8 and *3=12
|
||||
|
||||
int a = outA.pop();
|
||||
int b = outB.pop();
|
||||
net.stop();
|
||||
|
||||
REQUIRE(a == 8);
|
||||
REQUIRE(b == 12);
|
||||
}
|
||||
|
||||
TEST_CASE("static_network: auto fanout preserves ordering across multiple items", "[static_network]") {
|
||||
auto src = make_node<increment>(16);
|
||||
auto dstA = make_node<multiply2>(16);
|
||||
auto dstB = make_node<negate_val>(16);
|
||||
|
||||
Channel<int> outA(16), outB(16);
|
||||
dstA.set_output_channel<0>(&outA);
|
||||
dstB.set_output_channel<0>(&outB);
|
||||
|
||||
auto net = make_network(
|
||||
edge(src.output<0>(), dstA.input<0>()),
|
||||
edge(src.output<0>(), dstB.input<0>())
|
||||
);
|
||||
|
||||
net.start();
|
||||
for (int i = 0; i < 5; ++i)
|
||||
src.input_channel<0>().push(i); // 0..4 → +1 → *2 or negate
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
REQUIRE(outA.pop() == (i + 1) * 2);
|
||||
REQUIRE(outB.pop() == -(i + 1));
|
||||
}
|
||||
net.stop();
|
||||
}
|
||||
|
||||
// ── Stop/start lifecycle ──────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("static_network: stop disables input channel", "[static_network]") {
|
||||
auto src = make_node<increment>(5);
|
||||
auto dst = make_node<multiply2>(5);
|
||||
|
||||
auto net = make_network(
|
||||
edge(src.output<0>(), dst.input<0>())
|
||||
);
|
||||
|
||||
net.start();
|
||||
net.stop();
|
||||
|
||||
// After stop, input channel disabled — push must not throw
|
||||
src.input_channel<0>().push(99);
|
||||
REQUIRE(src.input_channel<0>().size() == 0);
|
||||
}
|
||||
|
||||
// ── Compile-time cycle detection ──────────────────────────────────────────────
|
||||
// Cycles fire a static_assert in make_network(), so we can only test the
|
||||
// no-cycle path at runtime.
|
||||
//
|
||||
// To manually verify a cycle error: add
|
||||
// auto bad = make_network(edge(a.output<0>(), b.input<0>()),
|
||||
// edge(b.output<0>(), a.input<0>()));
|
||||
// and confirm: "make_network: graph contains a directed cycle"
|
||||
//
|
||||
// To manually verify a duplicate-tag error: add
|
||||
// auto x = make_node<increment>(5);
|
||||
// auto y = make_node<increment>(5); // same type as x — UniqueTag=0 for both
|
||||
// auto bad = make_network(edge(x.output<0>(), y.input<0>()));
|
||||
// and confirm: "make_network: two nodes have the same (Func, UniqueTag)"
|
||||
|
||||
TEST_CASE("static_network: acyclic graph does not trigger static_assert", "[static_network]") {
|
||||
auto a = make_node<increment>(5);
|
||||
auto b = make_node<add10>(5);
|
||||
|
||||
Channel<int> out(5);
|
||||
b.set_output_channel<0>(&out);
|
||||
|
||||
auto net = make_network(edge(a.output<0>(), b.input<0>()));
|
||||
|
||||
net.start();
|
||||
a.input_channel<0>().push(5);
|
||||
REQUIRE(out.pop() == 16); // 5 → +1=6 → +10=16
|
||||
net.stop();
|
||||
}
|
||||
|
||||
// ── Label and UniqueTag ───────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("static_network: same function distinguished by UniqueTag", "[static_network]") {
|
||||
// Two nodes wrapping the same function — only possible with distinct UniqueTag
|
||||
auto a = make_node<increment, "stage1", 0>(8);
|
||||
auto b = make_node<increment, "stage2", 1>(8); // same func, tag=1 → distinct type
|
||||
|
||||
Channel<int> out(8);
|
||||
b.set_output_channel<0>(&out);
|
||||
|
||||
auto net = make_network(edge(a.output<0>(), b.input<0>()));
|
||||
|
||||
net.start();
|
||||
a.input_channel<0>().push(10);
|
||||
REQUIRE(out.pop() == 12); // 10 → +1=11 → +1=12
|
||||
net.stop();
|
||||
}
|
||||
|
||||
TEST_CASE("static_network: label is accessible as static member", "[static_network]") {
|
||||
using MyNode = decltype(make_node<increment, "my_node">(5));
|
||||
REQUIRE(MyNode::label() == "my_node");
|
||||
REQUIRE(MyNode::unique_tag == 0);
|
||||
|
||||
using TaggedNode = decltype(make_node<increment, "tagged", 42>(5));
|
||||
REQUIRE(TaggedNode::label() == "tagged");
|
||||
REQUIRE(TaggedNode::unique_tag == 42);
|
||||
}
|
||||
|
||||
TEST_CASE("static_network: fanout with labelled same-function consumers", "[static_network]") {
|
||||
auto src = make_node<increment, "src" >(8);
|
||||
auto dstA = make_node<increment, "consumer_a", 1>(8);
|
||||
auto dstB = make_node<increment, "consumer_b", 2>(8);
|
||||
|
||||
Channel<int> outA(8), outB(8);
|
||||
dstA.set_output_channel<0>(&outA);
|
||||
dstB.set_output_channel<0>(&outB);
|
||||
|
||||
// Fan-out from src to two increment nodes — only possible because tags differ
|
||||
auto net = make_network(
|
||||
edge(src.output<0>(), dstA.input<0>()),
|
||||
edge(src.output<0>(), dstB.input<0>())
|
||||
);
|
||||
|
||||
net.start();
|
||||
src.input_channel<0>().push(5); // 5 → +1=6 → both +1=7
|
||||
|
||||
REQUIRE(outA.pop() == 7);
|
||||
REQUIRE(outB.pop() == 7);
|
||||
net.stop();
|
||||
}
|
||||
Reference in New Issue
Block a user