274 lines
9.7 KiB
C++
274 lines
9.7 KiB
C++
#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: two independent fan-outs of the same element type are wired independently", "[static_network]") {
|
|
// Both src_a and src_b fan out to two consumers each.
|
|
// Without the FanoutId fix, both would produce FanoutNode<int,2> — the same
|
|
// C++ type — and find_node would wire all four consumers to the first instance.
|
|
auto src_a = make_node<add10, "src_a">(8);
|
|
auto src_b = make_node<negate_val, "src_b">(8);
|
|
auto cA = make_node<increment, "cA">(8);
|
|
auto cB = make_node<multiply2, "cB">(8);
|
|
auto cC = make_node<multiply3, "cC">(8);
|
|
auto cD = make_node<square, "cD">(8);
|
|
|
|
Channel<int> outA(8), outB(8), outC(8), outD(8);
|
|
cA.set_output_channel<0>(&outA);
|
|
cB.set_output_channel<0>(&outB);
|
|
cC.set_output_channel<0>(&outC);
|
|
cD.set_output_channel<0>(&outD);
|
|
|
|
auto net = make_network(
|
|
edge(src_a.output<0>(), cA.input<0>()), // src_a → FanoutNode<int,2,0> → cA, cB
|
|
edge(src_a.output<0>(), cB.input<0>()),
|
|
edge(src_b.output<0>(), cC.input<0>()), // src_b → FanoutNode<int,2,1> → cC, cD
|
|
edge(src_b.output<0>(), cD.input<0>())
|
|
);
|
|
|
|
net.start();
|
|
src_a.input_channel<0>().push(0); // 0 → +10=10 → {+1=11, *2=20}
|
|
src_b.input_channel<0>().push(5); // 5 → negate=-5 → {*3=-15, ^2=25}
|
|
|
|
REQUIRE(outA.pop() == 11);
|
|
REQUIRE(outB.pop() == 20);
|
|
REQUIRE(outC.pop() == -15);
|
|
REQUIRE(outD.pop() == 25);
|
|
net.stop();
|
|
}
|
|
|
|
TEST_CASE("static_network: code reuse - same function at corresponding stages of parallel branches", "[static_network]") {
|
|
// Both branches use increment and multiply2 — code reuse via distinct UniqueTag.
|
|
// Topology: src → fanout → { increment(tag=1) → multiply2(tag=1) → outA }
|
|
// → { increment(tag=2) → multiply2(tag=2) → outB }
|
|
auto src = make_node<add10, "src" >(8);
|
|
auto incA = make_node<increment, "inc", 1>(8);
|
|
auto mulA = make_node<multiply2, "mul", 1>(8);
|
|
auto incB = make_node<increment, "inc", 2>(8);
|
|
auto mulB = make_node<multiply2, "mul", 2>(8);
|
|
|
|
Channel<int> outA(8), outB(8);
|
|
mulA.set_output_channel<0>(&outA);
|
|
mulB.set_output_channel<0>(&outB);
|
|
|
|
auto net = make_network(
|
|
edge(src.output<0>(), incA.input<0>()),
|
|
edge(src.output<0>(), incB.input<0>()),
|
|
edge(incA.output<0>(), mulA.input<0>()),
|
|
edge(incB.output<0>(), mulB.input<0>())
|
|
);
|
|
|
|
net.start();
|
|
src.input_channel<0>().push(0); // 0 → +10=10 → both: +1=11 → *2=22
|
|
|
|
REQUIRE(outA.pop() == 22);
|
|
REQUIRE(outB.pop() == 22);
|
|
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();
|
|
}
|