152 lines
4.5 KiB
C++
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());
|
|
}
|