Add a per node error handler possibility
🧪 Test / test (push) Successful in 6m7s

This commit is contained in:
2026-05-10 19:51:23 +02:00
parent 1e9ba5ee66
commit c39db82763
4 changed files with 197 additions and 0 deletions
+102
View File
@@ -2,6 +2,7 @@
#include <kpn/node.hpp>
#include <chrono>
#include <thread>
#include <utility>
using namespace kpn;
@@ -47,3 +48,104 @@ TEST_CASE("node stop unblocks cleanly", "[node]") {
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());
}