This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user