diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000..acd39c09 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,51 @@ +name: End-to-End Tests + +on: + pull_request: + branches: [development] + +concurrency: + group: e2e-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build & E2E Test (Ubuntu) + runs-on: ubuntu-latest + env: + CTEST_OUTPUT_ON_FAILURE: 1 + CCACHE_DIR: ${{ github.workspace }}/.ccache + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + ninja-build build-essential cmake \ + libboost-all-dev libspdlog-dev libtinyxml2-dev libconfig++-dev \ + libssl-dev libnl-3-dev zlib1g-dev ccache clang-tidy clang g++-12 + + - name: Configure (CMake) + run: | + cmake -S . -B build -GNinja -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build --parallel + + - name: List end-to-end tests (debug) + run: ctest --test-dir build/tests -N -R '^e2e:' + + - name: Run end-to-end tests + run: | + set -o pipefail + ctest --test-dir build/tests -R '^e2e:' --output-on-failure --verbose -j 1 | tee build/ctest-e2e-log.txt + + - name: Upload test log + if: always() + uses: actions/upload-artifact@v4 + with: + name: ctest-e2e-log + path: build/ctest-e2e-log.txt + if-no-files-found: warn diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index d19d893c..0f938e4c 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -35,12 +35,12 @@ jobs: run: cmake --build build --parallel - name: List tests (debug) - run: ctest --test-dir build/tests -N + run: ctest --test-dir build/tests -N -R '^unit:' - name: Run unit tests run: | set -o pipefail - ctest --test-dir build/tests --output-on-failure -j 2 | tee build/ctest-log.txt + ctest --test-dir build/tests -R '^unit:' --output-on-failure -j 2 | tee build/ctest-log.txt - name: Upload test log if: always() diff --git a/AGENTS.md b/AGENTS.md index 358b3c3f..6e8f601e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,28 +33,40 @@ Compiler flags are set in the root `CMakeLists.txt`: ## Test Commands Test framework: Google Test v1.17.0 (fetched automatically via CMake FetchContent). -Test source: `tests/test_transmitter.cpp`. Single binary: `flute_tests`. +Test sources: `tests/test_transmitter.cpp` and `tests/test_end_to_end.cpp`. +Test binaries: `build/tests/flute_unit_tests` and `build/tests/flute_e2e_tests`. ```bash # Run all tests ctest --test-dir build/tests --output-on-failure -j 2 +# Run unit tests only +ctest --test-dir build/tests -R '^unit:' --output-on-failure -j 2 + +# Run end-to-end tests only +ctest --test-dir build/tests -R '^e2e:' --output-on-failure -j 1 + # List available tests without running ctest --test-dir build/tests -N # Run a single test by name (regex match) ctest --test-dir build/tests -R "RateLimitGetterSetter" -# Run a single test via GoogleTest binary directly -./build/tests/flute_tests --gtest_filter="TransmitterGetterSetterTest.RateLimitGetterSetter" +# Run a single unit test via GoogleTest binary directly +./build/tests/flute_unit_tests --gtest_filter="TransmitterGetterSetterTest.RateLimitGetterSetter" + +# Run the end-to-end test binary directly +./build/tests/flute_e2e_tests --gtest_filter="FluteEndToEndTest.TransmitsFileToReceiver" # Run all tests in a suite -./build/tests/flute_tests --gtest_filter="TransmitterGetterSetterTest.*" +./build/tests/flute_unit_tests --gtest_filter="TransmitterGetterSetterTest.*" # List all tests from the binary -./build/tests/flute_tests --gtest_list_tests +./build/tests/flute_unit_tests --gtest_list_tests ``` +Pull requests against `development` and `main` run separate GitHub Actions workflows for unit tests and end-to-end tests. + ## Lint / Static Analysis clang-tidy runs automatically during compilation (set globally in `CMakeLists.txt:44`). @@ -83,7 +95,7 @@ include/ Public API headers (Receiver.h, Transmitter.h, File.h, AlcPack src/ Implementation files (.cpp) utils/ Third-party utilities (base64) examples/ Demo apps (flute-receiver, flute-transmitter) -tests/ Unit tests (Google Test) +tests/ Unit and end-to-end tests (Google Test) ``` ## Code Style Guidelines diff --git a/README.md b/README.md index ecff43f9..bea3627f 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ sudo setcap 'cap_net_admin=eip' ./flute-receiver ## Testing -To execute the unit tests make sure to have built the project with the unit tests enabled (see Step 3: Build setup). +To execute the tests make sure to have built the project with testing enabled (see Step 3: Build setup). Then run @@ -147,6 +147,24 @@ cd build/tests ctest ```` +To run only the unit tests: + +```` +ctest -R '^unit:' +```` + +To run only the end-to-end FLUTE transmitter/receiver test: + +```` +ctest -R '^e2e:' +```` + +To see the end-to-end test's transmit/receive debug output locally: + +```` +ctest -R '^e2e:' --verbose +```` + ## Documentation Documentation of the source code can be found at: https://5g-mag.github.io/rt-libflute/ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8df202ff..cea11e0f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,18 +23,26 @@ FetchContent_MakeAvailable(googletest) enable_testing() -add_executable(flute_tests - test_transmitter.cpp -) +include(GoogleTest) -# Link against library under test -target_link_libraries( - flute_tests - PRIVATE - flute - GTest::gtest_main -) +function(add_flute_test_executable target_name test_source test_prefix) + add_executable(${target_name} + ${test_source} + ) -target_include_directories(flute_tests PRIVATE ${PROJECT_SOURCE_DIR}/include) -include(GoogleTest) -gtest_discover_tests(flute_tests) + target_link_libraries( + ${target_name} + PRIVATE + flute + GTest::gtest_main + ) + + target_include_directories(${target_name} PRIVATE ${PROJECT_SOURCE_DIR}/include) + + gtest_discover_tests(${target_name} + TEST_PREFIX ${test_prefix} + ) +endfunction() + +add_flute_test_executable(flute_unit_tests test_transmitter.cpp "unit:") +add_flute_test_executable(flute_e2e_tests test_end_to_end.cpp "e2e:") diff --git a/tests/test_end_to_end.cpp b/tests/test_end_to_end.cpp new file mode 100644 index 00000000..fe7d42d4 --- /dev/null +++ b/tests/test_end_to_end.cpp @@ -0,0 +1,145 @@ +// libflute - FLUTE/ALC library +// +// Copyright (C) 2026 Fraunhofer FOKUS +// +// Licensed under the License terms and conditions for use, reproduction, and +// distribution of 5G-MAG software (the "License"). You may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// https://www.5g-mag.com/reference-tools. Unless required by applicable law or +// agreed to in writing, software distributed under the License is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. +// +// See the License for the specific language governing permissions and limitations +// under the License. +// + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Receiver.h" +#include "Transmitter.h" + +namespace { + +auto read_fixture_file(const std::filesystem::path& file_path) -> std::string { + std::ifstream input(file_path, std::ios::binary); + if (!input.is_open()) { + throw std::runtime_error("Failed to open end-to-end fixture file"); + } + + return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; +} + +} // namespace + +TEST(FluteEndToEndTest, TransmitsFileToReceiver) { + namespace fs = std::filesystem; + using namespace std::chrono_literals; + + constexpr short kPort = 18091; + const fs::path fixtures_dir = fs::path{__FILE__}.parent_path() / "tmp"; + const fs::path input_file = fixtures_dir / "e2e_payload.bin"; + const std::string expected_location = "e2e/payload.bin"; + const std::string expected_payload = read_fixture_file(input_file); + const auto now = std::chrono::system_clock::now(); + + ASSERT_FALSE(expected_payload.empty()); + + boost::asio::io_context receiver_io; + boost::asio::io_context transmitter_io; + + LibFlute::Receiver receiver("0.0.0.0", "239.255.0.1", kPort, 4242, receiver_io); + LibFlute::Transmitter transmitter( + "239.255.0.1", + kPort, + 4242, + 1400, + 0, + transmitter_io, + std::nullopt, + LibFlute::FileDeliveryTable::FDT_NS_DRAFT_2005); + + auto file_description = std::make_shared( + expected_location, + input_file.string()); + file_description->set_content_type("application/octet-stream"); + file_description->set_expiry_time(now + 60s); + + std::promise> received_file_promise; + std::promise transmitted_toi_promise; + auto received_file_future = received_file_promise.get_future(); + auto transmitted_toi_future = transmitted_toi_promise.get_future(); + + receiver.register_completion_callback( + [&received_file_promise, &receiver, &receiver_io](const std::shared_ptr& file) { + std::cout << "Received file TOI " << file->meta().toi + << " location '" << file->meta().content_location << "'" + << " with " << file->length() << " bytes" << std::endl; + received_file_promise.set_value(file); + receiver.stop(); + receiver_io.stop(); + }); + + transmitter.register_completion_callback( + [&transmitted_toi_promise, &transmitter, &transmitter_io](const uint32_t toi) { + std::cout << "Transmitted file TOI " << toi << std::endl; + transmitted_toi_promise.set_value(toi); + transmitter.deactivate(); + transmitter_io.stop(); + }); + + std::thread receiver_thread([&receiver_io]() { receiver_io.run(); }); + std::thread transmitter_thread([&transmitter_io]() { transmitter_io.run(); }); + + std::cout << "Sending fixture '" << input_file.string() << "' as '" << expected_location + << "' with " << expected_payload.size() << " bytes" << std::endl; + transmitter.send(file_description); + + const auto transmitted_ready = transmitted_toi_future.wait_for(5s); + const auto received_ready = received_file_future.wait_for(5s); + + if (transmitted_ready != std::future_status::ready || received_ready != std::future_status::ready) { + std::cerr << "Timed out waiting for end-to-end completion. transmitted_ready=" + << (transmitted_ready == std::future_status::ready) + << ", received_ready=" << (received_ready == std::future_status::ready) << std::endl; + transmitter.deactivate(); + transmitter_io.stop(); + receiver.stop(); + receiver_io.stop(); + } + + if (receiver_thread.joinable()) { + receiver_thread.join(); + } + if (transmitter_thread.joinable()) { + transmitter_thread.join(); + } + + ASSERT_EQ(transmitted_ready, std::future_status::ready); + ASSERT_EQ(received_ready, std::future_status::ready); + + const auto transmitted_toi = transmitted_toi_future.get(); + const auto received_file = received_file_future.get(); + ASSERT_NE(received_file, nullptr); + + EXPECT_EQ(transmitted_toi, file_description->toi()); + EXPECT_EQ(received_file->meta().toi, transmitted_toi); + EXPECT_EQ(received_file->meta().content_location, expected_location); + EXPECT_EQ(received_file->meta().content_length, expected_payload.size()); + + const std::string received_payload(received_file->buffer(), received_file->length()); + EXPECT_EQ(received_payload, expected_payload); +} diff --git a/tests/tmp/e2e_payload.bin b/tests/tmp/e2e_payload.bin new file mode 100644 index 00000000..1a8782ff --- /dev/null +++ b/tests/tmp/e2e_payload.bin @@ -0,0 +1,2 @@ +FLUTE end-to-end payload +1234567890abcdefghijklmnopqrstuvwxyz