#include #include #include #include #include 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(5); auto dst = make_node(5); Channel 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(5); auto b = make_node(5); auto c = make_node(5); Channel 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(8); auto dstA = make_node(8); auto dstB = make_node(8); Channel outA(8), outB(8); dstA.set_output_channel<0>(&outA); dstB.set_output_channel<0>(&outB); // Two edges from the same output port — FanoutNode 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(16); auto dstA = make_node(16); auto dstB = make_node(16); Channel 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(5); auto dst = make_node(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(5); // auto y = make_node(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(5); auto b = make_node(5); Channel 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(8); auto b = make_node(8); // same func, tag=1 → distinct type Channel 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 — the same // C++ type — and find_node would wire all four consumers to the first instance. auto src_a = make_node(8); auto src_b = make_node(8); auto cA = make_node(8); auto cB = make_node(8); auto cC = make_node(8); auto cD = make_node(8); Channel 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 → cA, cB edge(src_a.output<0>(), cB.input<0>()), edge(src_b.output<0>(), cC.input<0>()), // src_b → FanoutNode → 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(8); auto incA = make_node(8); auto mulA = make_node(8); auto incB = make_node(8); auto mulB = make_node(8); Channel 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(5)); REQUIRE(MyNode::label() == "my_node"); REQUIRE(MyNode::unique_tag == 0); using TaggedNode = decltype(make_node(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(8); auto dstA = make_node(8); auto dstB = make_node(8); Channel 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(); }