diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9ab0aa8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + compiler: [gcc, clang] + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake ninja-build libboost-all-dev + + - name: Configure + run: | + cmake -S . -B build -G Ninja -DRTACO_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build -j + + - name: Run tests + run: ctest --test-dir build --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 661c962..c967a78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,4 +103,10 @@ if(RTACO_BUILD_DOCS) add_subdirectory(docs) endif() +option(RTACO_BUILD_TESTS "Build tests" OFF) +if(RTACO_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() + message(STATUS "Config: llmx-rtaco project configured") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..1cab645 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,53 @@ +cmake_minimum_required(VERSION 3.22) + +function(add_llmx_test target) + cmake_parse_arguments( + LLMX_TEST + "" + "TEST_NAME" + "" + ${ARGN} + ) + + if(LLMX_TEST_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "add_llmx_test does not accept source file arguments") + endif() + + add_executable(${target} ${target}.cxx) + target_link_libraries(${target} + PRIVATE + GTest::gtest + ${PROJECT_NAME} + ) + + if(LLMX_TEST_TEST_NAME) + set(test_name ${LLMX_TEST_TEST_NAME}) + else() + set(test_name ${target}) + endif() + + add_test(NAME ${test_name} COMMAND ${target}) +endfunction() + + +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.15.2 +) +FetchContent_MakeAvailable(googletest) + +add_executable(test_rtaco + test_signal.cpp + test_requesttask_compile.cpp + test_socket.cpp + test_nl_common.cpp +) + +target_link_libraries(test_rtaco PRIVATE llmx_rtaco GTest::gtest_main) + +enable_testing() + +include(GoogleTest) +gtest_discover_tests(test_rtaco) diff --git a/tests/test_nl_common.cpp b/tests/test_nl_common.cpp new file mode 100644 index 0000000..21e495f --- /dev/null +++ b/tests/test_nl_common.cpp @@ -0,0 +1,36 @@ +#include +#include + +#include "rtaco/core/nl_common.hxx" + +using namespace llmx::rtaco; + +TEST(NLCommonTest, TrimStringAndAttributeString) { + // trim_string should remove trailing nulls + std::string_view sv = "abc\0\0"; + auto trimmed = trim_string(sv); + EXPECT_EQ(trimmed, "abc"); + + // Build an rtattr with payload string + const char* payload = "eth0"; + const size_t payload_len = std::strlen(payload) + 1; + const size_t attr_len = RTA_LENGTH(payload_len); + std::vector buf(attr_len); + std::memset(buf.data(), 0, buf.size()); + + auto attr = reinterpret_cast(buf.data()); + attr->rta_len = static_cast(RTA_LENGTH(payload_len)); + attr->rta_type = IFLA_IFNAME; + std::memcpy(RTA_DATA(attr), payload, payload_len); + + auto s = attribute_string(*attr); + EXPECT_EQ(s, "eth0"); +} + +TEST(NLCommonTest, GetMsgPayloadShort) { + nlmsghdr short_hdr{}; + short_hdr.nlmsg_len = NLMSG_LENGTH(sizeof(ifinfomsg)) - 1; // too small + + auto ptr = get_msg_payload(short_hdr); + EXPECT_EQ(ptr, nullptr); +} diff --git a/tests/test_requesttask_compile.cpp b/tests/test_requesttask_compile.cpp new file mode 100644 index 0000000..243a744 --- /dev/null +++ b/tests/test_requesttask_compile.cpp @@ -0,0 +1,23 @@ +#include + +#include "rtaco/tasks/nl_request_task.hxx" + +using namespace llmx::rtaco; + +struct RequestBehaviorChecker { + void prepare_request() {} + std::span request_payload() const { + return {}; + } + auto process_message(const nlmsghdr&) + -> std::optional> { + return std::nullopt; + } +}; + +static_assert(request_behavior, + "RequestBehaviorChecker should satisfy request_behavior concept"); + +TEST(RequestTaskCompileTest, ConceptSatisfied) { + SUCCEED(); +} diff --git a/tests/test_signal.cpp b/tests/test_signal.cpp new file mode 100644 index 0000000..3448ded --- /dev/null +++ b/tests/test_signal.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include + +#include "rtaco/core/nl_signal.hxx" + +using namespace llmx::rtaco; + +TEST(SignalTest, SyncAndAsyncSlots) { + boost::asio::io_context io; + + Signal sig(io.get_executor()); + + auto conn1 = sig.connect([](int a, int b) { return a + b; }, ExecPolicy::Sync); + + auto conn2 = sig.connect([](int a, int b) { return a * b; }, ExecPolicy::Async); + + // Run the io_context on a background thread so async slots can execute + auto work = boost::asio::make_work_guard(io); + std::thread runner([&io] { io.run(); }); + + // Emit will block until all async slots complete (combiner collects values). + auto results = sig.emit(3, 4); + + ASSERT_EQ(results.size(), 2); + EXPECT_EQ(results[0], 7); + EXPECT_EQ(results[1], 12); + + work.reset(); + io.stop(); + runner.join(); +} diff --git a/tests/test_socket.cpp b/tests/test_socket.cpp new file mode 100644 index 0000000..59c0e26 --- /dev/null +++ b/tests/test_socket.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "rtaco/socket/nl_socket.hxx" +#include "rtaco/socket/nl_socket_guard.hxx" + +using namespace llmx::rtaco; + +TEST(SocketTest, DefaultClosed) { + boost::asio::io_context io; + Socket s(io, "test-socket"); + + EXPECT_FALSE(s.is_open()); + + auto rc = s.close(); + EXPECT_TRUE(static_cast(rc)); + + auto rc2 = s.cancel(); + (void)rc2; // cancel() may fail on a non-open socket on some platforms; ensure it + // doesn't throw +} + +TEST(SocketGuardTest, StopNoThrow) { + boost::asio::io_context io; + SocketGuard g(io, "test-guard"); + + // stop should be safe even if socket not open + EXPECT_NO_THROW(g.stop()); +}