KPN/tests/test_node.cpp
Duncan Tourolle c39db82763
All checks were successful
🧪 Test / test (push) Successful in 6m7s
Add a per node error handler possibility
2026-05-10 19:51:23 +02:00

152 lines
4.5 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <kpn/node.hpp>
#include <chrono>
#include <thread>
#include <utility>
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());
}
// ── 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<int> 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<NTTP> stops cleanly on exception with no error handler", "[node][error_handler]") {
auto node = make_node<always_throws_fn>();
node.start();
node.input_channel<0>().push(1);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
REQUIRE_FALSE(node.running());
}