First attempt
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
# ── Catch2 ────────────────────────────────────────────────────────────────────
|
||||
find_package(Catch2 3 QUIET)
|
||||
if(NOT Catch2_FOUND)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
Catch2
|
||||
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
||||
GIT_TAG v3.5.3
|
||||
)
|
||||
FetchContent_MakeAvailable(Catch2)
|
||||
endif()
|
||||
|
||||
# ── Google Test ───────────────────────────────────────────────────────────────
|
||||
find_package(GTest QUIET)
|
||||
if(NOT GTest_FOUND)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||
GIT_TAG v1.14.0
|
||||
)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
endif()
|
||||
|
||||
# ── Test executable ───────────────────────────────────────────────────────────
|
||||
add_executable(kpn_tests
|
||||
test_fixed_string.cpp
|
||||
test_traits.cpp
|
||||
test_channel.cpp
|
||||
test_node.cpp
|
||||
test_network.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(kpn_tests PRIVATE
|
||||
kpn
|
||||
Catch2::Catch2WithMain
|
||||
GTest::gtest
|
||||
)
|
||||
|
||||
include(CTest)
|
||||
include(Catch)
|
||||
catch_discover_tests(kpn_tests)
|
||||
@@ -0,0 +1,99 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <kpn/channel.hpp>
|
||||
#include <thread>
|
||||
|
||||
using namespace kpn;
|
||||
|
||||
TEST_CASE("channel storage policy: small trivial type by value", "[channel]") {
|
||||
STATIC_REQUIRE(channel_storage_policy<int>::by_value);
|
||||
STATIC_REQUIRE(channel_storage_policy<float>::by_value);
|
||||
}
|
||||
|
||||
TEST_CASE("channel storage policy: large type by shared_ptr", "[channel]") {
|
||||
struct Big { char data[64]; };
|
||||
STATIC_REQUIRE(!channel_storage_policy<Big>::by_value);
|
||||
}
|
||||
|
||||
TEST_CASE("push and pop single value", "[channel]") {
|
||||
Channel<int> ch(5);
|
||||
ch.push(42);
|
||||
REQUIRE(ch.pop() == 42);
|
||||
}
|
||||
|
||||
TEST_CASE("channel respects capacity", "[channel]") {
|
||||
Channel<int> ch(2);
|
||||
ch.push(1);
|
||||
ch.push(2);
|
||||
REQUIRE_THROWS_AS(ch.push(3), ChannelOverflowError);
|
||||
}
|
||||
|
||||
TEST_CASE("pop blocks until value available", "[channel]") {
|
||||
Channel<int> ch(5);
|
||||
int result = 0;
|
||||
std::thread producer([&] {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
ch.push(99);
|
||||
});
|
||||
result = ch.pop();
|
||||
producer.join();
|
||||
REQUIRE(result == 99);
|
||||
}
|
||||
|
||||
TEST_CASE("try_pop returns false on timeout", "[channel]") {
|
||||
Channel<int> ch(5);
|
||||
int out = 0;
|
||||
REQUIRE_FALSE(ch.try_pop(out, std::chrono::milliseconds(10)));
|
||||
}
|
||||
|
||||
TEST_CASE("try_pop succeeds when value present", "[channel]") {
|
||||
Channel<int> ch(5);
|
||||
ch.push(7);
|
||||
int out = 0;
|
||||
REQUIRE(ch.try_pop(out, std::chrono::milliseconds(10)));
|
||||
REQUIRE(out == 7);
|
||||
}
|
||||
|
||||
TEST_CASE("disable unblocks waiting pop", "[channel]") {
|
||||
Channel<int> ch(5);
|
||||
std::thread disabler([&] {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
ch.disable();
|
||||
});
|
||||
REQUIRE_THROWS_AS(ch.pop(), ChannelClosedError);
|
||||
disabler.join();
|
||||
}
|
||||
|
||||
TEST_CASE("push to disabled channel is silently dropped", "[channel]") {
|
||||
Channel<int> ch(5);
|
||||
ch.disable();
|
||||
ch.push(99); // must not throw, must not enqueue
|
||||
REQUIRE(ch.size() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("disable clears existing queue contents", "[channel]") {
|
||||
Channel<int> ch(5);
|
||||
ch.push(1);
|
||||
ch.push(2);
|
||||
REQUIRE(ch.size() == 2);
|
||||
ch.disable();
|
||||
REQUIRE(ch.size() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("enable re-accepts pushes after disable", "[channel]") {
|
||||
Channel<int> ch(5);
|
||||
ch.disable();
|
||||
ch.push(1);
|
||||
REQUIRE(ch.size() == 0);
|
||||
ch.enable();
|
||||
ch.push(42);
|
||||
REQUIRE(ch.pop() == 42);
|
||||
}
|
||||
|
||||
TEST_CASE("large type stored as shared_ptr — no copy on pop", "[channel]") {
|
||||
struct Big { char data[64]; int tag; };
|
||||
Channel<Big> ch(5);
|
||||
Big b{}; b.tag = 123;
|
||||
ch.push(b);
|
||||
auto out = ch.pop();
|
||||
REQUIRE(out.tag == 123);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <kpn/fixed_string.hpp>
|
||||
|
||||
using namespace kpn;
|
||||
|
||||
TEST_CASE("fixed_string equality", "[fixed_string]") {
|
||||
constexpr fixed_string a("img");
|
||||
constexpr fixed_string b("img");
|
||||
constexpr fixed_string c("sigma");
|
||||
STATIC_REQUIRE(a == b);
|
||||
STATIC_REQUIRE(!(a == c));
|
||||
}
|
||||
|
||||
TEST_CASE("fixed_string view", "[fixed_string]") {
|
||||
constexpr fixed_string s("hello");
|
||||
REQUIRE(s.view() == "hello");
|
||||
}
|
||||
|
||||
TEST_CASE("index_of hit", "[fixed_string]") {
|
||||
constexpr auto idx = index_of<fixed_string("b"), fixed_string("a"), fixed_string("b"), fixed_string("c")>();
|
||||
STATIC_REQUIRE(idx == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("index_of miss returns npos", "[fixed_string]") {
|
||||
constexpr auto idx = index_of<fixed_string("z"), fixed_string("a"), fixed_string("b")>();
|
||||
STATIC_REQUIRE(idx == npos);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <kpn/kpn.hpp>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
using namespace kpn;
|
||||
|
||||
static int increment(int x) { return x + 1; }
|
||||
static int multiply2(int x) { return x * 2; }
|
||||
|
||||
TEST_CASE("network build and run: linear pipeline", "[network]") {
|
||||
// Nodes declared first — they own their input channels and must outlive the network
|
||||
auto src = make_node<increment>(5);
|
||||
auto dst = make_node<multiply2>(5);
|
||||
|
||||
auto& src_in = src.input_channel<0>();
|
||||
Channel<int> final_out(5);
|
||||
dst.set_output_channel<0>(&final_out);
|
||||
|
||||
Network net;
|
||||
net.add("src", src)
|
||||
.add("dst", dst)
|
||||
.connect("src", src.output<0>(), "dst", dst.input<0>())
|
||||
.build();
|
||||
|
||||
net.start();
|
||||
src_in.push(5); // 5 → increment → 6 → multiply2 → 12
|
||||
int result = final_out.pop();
|
||||
net.stop();
|
||||
|
||||
REQUIRE(result == 12);
|
||||
}
|
||||
|
||||
TEST_CASE("network detects cycle", "[network]") {
|
||||
auto a = make_node<increment>(5);
|
||||
auto b = make_node<increment>(5);
|
||||
|
||||
Network net;
|
||||
net.add("a", a).add("b", b);
|
||||
net.connect("a", a.output<0>(), "b", b.input<0>());
|
||||
net.connect("b", b.output<0>(), "a", a.input<0>());
|
||||
|
||||
REQUIRE_THROWS_AS(net.build(), NetworkCycleError);
|
||||
}
|
||||
|
||||
TEST_CASE("stop disables input channels — producer push is silently dropped", "[network]") {
|
||||
auto node = make_node<increment>(5);
|
||||
auto& in_ch = node.input_channel<0>();
|
||||
|
||||
node.start();
|
||||
node.stop();
|
||||
|
||||
// After stop, channel is disabled — push must not throw
|
||||
in_ch.push(99);
|
||||
REQUIRE(in_ch.size() == 0);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <kpn/node.hpp>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <kpn/traits.hpp>
|
||||
#include <tuple>
|
||||
|
||||
using namespace kpn;
|
||||
|
||||
static int free_func(int a, float b) { return a; }
|
||||
static void sink_func(int a) {}
|
||||
static std::tuple<int, float> multi_func(int a) { return {a, 1.f}; }
|
||||
|
||||
TEST_CASE("arity of free function", "[traits]") {
|
||||
STATIC_REQUIRE(arity_v<decltype(free_func)> == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("return type of free function", "[traits]") {
|
||||
STATIC_REQUIRE(std::is_same_v<return_t<decltype(free_func)>, int>);
|
||||
}
|
||||
|
||||
TEST_CASE("normalised return: single value", "[traits]") {
|
||||
STATIC_REQUIRE(std::is_same_v<normalised_return_t<int>, std::tuple<int>>);
|
||||
}
|
||||
|
||||
TEST_CASE("normalised return: void", "[traits]") {
|
||||
STATIC_REQUIRE(std::is_same_v<normalised_return_t<void>, std::tuple<>>);
|
||||
}
|
||||
|
||||
TEST_CASE("normalised return: tuple passthrough", "[traits]") {
|
||||
using T = std::tuple<int, float>;
|
||||
STATIC_REQUIRE(std::is_same_v<normalised_return_t<T>, T>);
|
||||
}
|
||||
|
||||
TEST_CASE("output_count: single return", "[traits]") {
|
||||
STATIC_REQUIRE(output_count_v<decltype(free_func)> == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("output_count: void return", "[traits]") {
|
||||
STATIC_REQUIRE(output_count_v<decltype(sink_func)> == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("output_count: tuple return", "[traits]") {
|
||||
STATIC_REQUIRE(output_count_v<decltype(multi_func)> == 2);
|
||||
}
|
||||
Reference in New Issue
Block a user