Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
24 changes: 18 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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/
34 changes: 21 additions & 13 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:")
145 changes: 145 additions & 0 deletions tests/test_end_to_end.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>

#include <boost/asio.hpp>

#include <chrono>
#include <filesystem>
#include <fstream>
#include <future>
#include <iostream>
#include <iterator>
#include <memory>
#include <stdexcept>
#include <string>
#include <thread>

#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<char>(input), std::istreambuf_iterator<char>()};
}

} // 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<LibFlute::Transmitter::FileDescription>(
expected_location,
input_file.string());
file_description->set_content_type("application/octet-stream");
file_description->set_expiry_time(now + 60s);

std::promise<std::shared_ptr<LibFlute::File>> received_file_promise;
std::promise<uint32_t> 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<LibFlute::File>& 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);
}
2 changes: 2 additions & 0 deletions tests/tmp/e2e_payload.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FLUTE end-to-end payload
1234567890abcdefghijklmnopqrstuvwxyz