#include #include #include #include #include using namespace kpn; static int double_it(int x) { return x * 2; } static std::tuple 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::input_count == 1); STATIC_REQUIRE(Node::output_count == 1); STATIC_REQUIRE(Node::output_count == 2); STATIC_REQUIRE(Node::output_count == 0); } TEST_CASE("node named port index resolution", "[node]") { using N = Node, out<"result">>; STATIC_REQUIRE(index_of() == 0); STATIC_REQUIRE(index_of() == 0); } TEST_CASE("node processes a single item end-to-end", "[node]") { Node src_node(5); // used only as input source placeholder Node node(5); // Manually wire: push to input channel, connect a downstream channel, run one item auto& in_ch = node.input_channel<0>(); Channel 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 node(5); node.start(); // Node is blocked waiting for input — stop() must return without deadlock node.stop(); REQUIRE_FALSE(node.running()); } // ── Error handler tests ─────────────────────────────────────────────────────── namespace { struct SometimesThrower { bool throw_next = true; int operator()(int x) { if (std::exchange(throw_next, false)) throw std::runtime_error("deliberate skip"); return x * 2; } }; struct AlwaysThrows { int operator()(int) { throw std::runtime_error("always throws"); return 0; } }; static int always_throws_fn(int) { throw std::runtime_error("nttp always throws"); return 0; } } // namespace TEST_CASE("node stops cleanly on exception with no error handler", "[node][error_handler]") { AlwaysThrows obj; auto node = make_node(obj); node.start(); node.input_channel<0>().push(1); std::this_thread::sleep_for(std::chrono::milliseconds(50)); REQUIRE_FALSE(node.running()); } TEST_CASE("node continues when error handler returns true", "[node][error_handler]") { SometimesThrower obj; auto node = make_node(obj); Channel out_ch(5); node.set_output_channel<0>(&out_ch); bool handler_called = false; node.set_error_handler([&](std::string_view, std::exception_ptr) { handler_called = true; return true; }); node.start(); node.input_channel<0>().push(0); // throws → skipped, no output node.input_channel<0>().push(21); // succeeds → 42 int result = out_ch.pop(); // blocking — waits for the second item node.stop(); REQUIRE(handler_called); REQUIRE(result == 42); } TEST_CASE("node stops when error handler returns false", "[node][error_handler]") { AlwaysThrows obj; auto node = make_node(obj); node.set_error_handler([](std::string_view, std::exception_ptr) { return false; }); node.start(); node.input_channel<0>().push(1); std::this_thread::sleep_for(std::chrono::milliseconds(50)); REQUIRE_FALSE(node.running()); } TEST_CASE("error handler receives node name and exception", "[node][error_handler]") { AlwaysThrows obj; auto node = make_node(obj); node.set_name("test_node"); std::string captured_name; std::string captured_msg; node.set_error_handler([&](std::string_view name, std::exception_ptr ep) { captured_name = name; try { std::rethrow_exception(ep); } catch (const std::exception& e) { captured_msg = e.what(); } return false; }); node.start(); node.input_channel<0>().push(1); std::this_thread::sleep_for(std::chrono::milliseconds(50)); node.stop(); REQUIRE(captured_name == "test_node"); REQUIRE(captured_msg == "always throws"); } TEST_CASE("Node stops cleanly on exception with no error handler", "[node][error_handler]") { auto node = make_node(); node.start(); node.input_channel<0>().push(1); std::this_thread::sleep_for(std::chrono::milliseconds(50)); REQUIRE_FALSE(node.running()); }