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
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: CI

on:
push:
branches: [ '**' ]
pull_request:
branches: [ main ]

jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-24.04

steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
submodules: recursive

- name: Install System Dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
git \
python3 \
python3-pip \
python3-venv \
libgrpc++-dev \
libprotobuf-dev \
protobuf-compiler \
protobuf-compiler-grpc \
nlohmann-json3-dev

- name: Set up Python Virtual Environment
run: |
python3 -m venv venv
./venv/bin/pip install -r generator/requirements.txt
echo "$GITHUB_WORKSPACE/venv/bin" >> $GITHUB_PATH

- name: Run Generator Parser Tests
run: |
python3 generator/test/test_parser.py

- name: Run Generator Dialect & Protoc Validation Tests
run: |
python3 generator/test/test_generator.py

- name: "Verify C++ Build & Run Tests (Dialect: common)"
run: |
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DMAVLINK_DIALECT=common
cmake --build build -j$(nproc)
cd build && ctest --output-on-failure

- name: "Verify C++ Build & Run Tests (Dialect: minimal)"
run: |
rm -rf build
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DMAVLINK_DIALECT=minimal
cmake --build build -j$(nproc)
cd build && ctest --output-on-failure
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ if(NOT GEN_RESULT EQUAL 0)
message(FATAL_ERROR "MAVLink to Protobuf generation failed!")
endif()

# Enable CTest suite
enable_testing()

# Add bridge module subdirectory
add_subdirectory(bridge)
52 changes: 51 additions & 1 deletion bridge/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,54 @@ if (CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_options(${PROJECT_NAME} PRIVATE -O0 -g)
else()
target_compile_options(${PROJECT_NAME} PRIVATE -O3)
endif()
endif()

# ============================================================================
# C++ Testing Target
# ============================================================================
enable_testing()

add_executable(test_runner
test/test_runner.cc
test/test_router.cc
test/test_converter.cc
src/service/Router.cc
src/service/Logger.cc
src/mavlink/MessageConverter.cc
$<TARGET_OBJECTS:proto_lib>
)

target_include_directories(test_runner PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${GENERATED_PROTOBUF_PATH}
${CMAKE_CURRENT_BINARY_DIR}/generated
)

target_include_directories(test_runner SYSTEM PRIVATE
${LIBMAV_INCLUDE_DIR}
)

add_dependencies(test_runner proto_lib)

target_link_libraries(test_runner PRIVATE
${Protobuf_LIBRARIES}
${GRPC_LIBRARIES}
${GRPCPP_LIBRARIES}
Threads::Threads
)

set_target_properties(test_runner PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)

target_compile_definitions(test_runner PRIVATE MAVLINK_DIALECT="${MAVLINK_DIALECT}")
target_compile_options(test_runner PRIVATE -Wall -Wextra -Werror)

if (CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_options(test_runner PRIVATE -O0 -g)
else()
target_compile_options(test_runner PRIVATE -O3)
endif()

add_test(NAME bridge_tests COMMAND test_runner)
96 changes: 96 additions & 0 deletions bridge/test/test_converter.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#include "mavlink/MessageConverter.h"
#include <mav/MessageSet.h>
#include <iostream>
#include <cstdlib>
#include <unistd.h>

#define TEST_ASSERT(cond) \
do { \
if (!(cond)) { \
std::cerr << "Assertion failed: " #cond " at " << __FILE__ << ":" << __LINE__ << std::endl; \
std::abort(); \
} \
} while (0)

using namespace mavlink2grpc;

void test_message_converter_heartbeat() {
std::cout << "Running test_message_converter_heartbeat..." << std::endl;

// Locate the message definition file
std::string xml_path = "";
std::vector<std::string> paths = {
"third_party/mavlink/message_definitions/v1.0/common.xml",
"../third_party/mavlink/message_definitions/v1.0/common.xml",
"mavlink/message_definitions/v1.0/common.xml",
"../mavlink/message_definitions/v1.0/common.xml"
};

for (const auto& p : paths) {
if (access(p.c_str(), F_OK) != -1) {
xml_path = p;
break;
}
}

if (xml_path.empty()) {
std::cerr << "Warning: Could not find common.xml definition. Skipping converter tests." << std::endl;
return;
}

// Load MessageSet
mav::MessageSet message_set(xml_path);

// 1. Create a mav::Message for HEARTBEAT
mav::Message mav_msg = message_set.create("HEARTBEAT");
TEST_ASSERT(mav_msg.name() == "HEARTBEAT");
TEST_ASSERT(mav_msg.id() == 0);

// Set header partner ID (represents system ID and component ID)
// libmav sets source system/component via setFromConnectionPartner
// Let's set some fields on the message
mav_msg.set("type", static_cast<uint8_t>(2)); // MAV_TYPE_QUADROTOR
mav_msg.set("autopilot", static_cast<uint8_t>(3)); // MAV_AUTOPILOT_ARDUPILOTMEGA
mav_msg.set("base_mode", static_cast<uint8_t>(81));
mav_msg.set("custom_mode", static_cast<uint32_t>(12345));
mav_msg.set("system_status", static_cast<uint8_t>(3)); // MAV_STATE_STANDBY
mav_msg.set("mavlink_version", static_cast<uint8_t>(3));

// 2. Convert to Protobuf
// We can pass system_id = 42 and component_id = 191 by simulating a connection partner if needed,
// but let's check what default to_proto does or if it gets them from the message source.
auto proto_opt = MessageConverter::to_proto(mav_msg, 123456789ULL);
TEST_ASSERT(proto_opt.has_value());

auto proto_msg = proto_opt.value();
TEST_ASSERT(proto_msg.message_id() == 0);
TEST_ASSERT(proto_msg.timestamp_usec() == 123456789ULL);
TEST_ASSERT(proto_msg.has_heartbeat());

const auto& heartbeat = proto_msg.heartbeat();
TEST_ASSERT(heartbeat.type() == 2);
TEST_ASSERT(heartbeat.autopilot() == 3);
TEST_ASSERT(heartbeat.base_mode() == 81);
TEST_ASSERT(heartbeat.custom_mode() == 12345);
TEST_ASSERT(heartbeat.system_status() == 3);

// 3. Convert back from Protobuf to mav::Message
auto back_opt = MessageConverter::from_proto(proto_msg, message_set);
TEST_ASSERT(back_opt.has_value());

auto back_msg = back_opt.value();
TEST_ASSERT(back_msg.name() == "HEARTBEAT");
TEST_ASSERT(back_msg.id() == 0);

TEST_ASSERT(back_msg.get<uint8_t>("type") == 2);
TEST_ASSERT(back_msg.get<uint8_t>("autopilot") == 3);
TEST_ASSERT(back_msg.get<uint8_t>("base_mode") == 81);
TEST_ASSERT(back_msg.get<uint32_t>("custom_mode") == 12345);
TEST_ASSERT(back_msg.get<uint8_t>("system_status") == 3);

std::cout << "test_message_converter_heartbeat passed!" << std::endl;
}

void run_converter_tests() {
test_message_converter_heartbeat();
}
139 changes: 139 additions & 0 deletions bridge/test/test_router.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#include "service/Router.h"
#include <iostream>
#include <cstdlib>

#define TEST_ASSERT(cond) \
do { \
if (!(cond)) { \
std::cerr << "Assertion failed: " #cond " at " << __FILE__ << ":" << __LINE__ << std::endl; \
std::abort(); \
} \
} while (0)

using namespace mavlink2grpc;

void test_router_basic() {
std::cout << "Running test_router_basic..." << std::endl;
Router router;

TEST_ASSERT(router.subscription_count() == 0);

// Subscribe client 1 with no filters (system_id=0, component_id=0)
mavlink::StreamFilter filter1;
filter1.set_system_id(0);
filter1.set_component_id(0);

size_t client1_received = 0;
uint64_t sub1 = router.subscribe(filter1, [&](const mavlink::MavlinkMessage&) {
client1_received++;
return true;
});

TEST_ASSERT(sub1 == 1);
TEST_ASSERT(router.subscription_count() == 1);

// Send a message
mavlink::MavlinkMessage msg1;
msg1.set_system_id(1);
msg1.set_component_id(1);
msg1.set_message_id(0); // HEARTBEAT

size_t routed = router.route_message(msg1);
TEST_ASSERT(routed == 1);
TEST_ASSERT(client1_received == 1);

// Unsubscribe client 1
bool unsub_ok = router.unsubscribe(sub1);
TEST_ASSERT(unsub_ok);
TEST_ASSERT(router.subscription_count() == 0);

routed = router.route_message(msg1);
TEST_ASSERT(routed == 0);
TEST_ASSERT(client1_received == 1); // should remain 1

std::cout << "test_router_basic passed!" << std::endl;
}

void test_router_filters() {
std::cout << "Running test_router_filters..." << std::endl;
Router router;

// Filter for system 1, component 1, message 30 (ATTITUDE) only
mavlink::StreamFilter filter;
filter.set_system_id(1);
filter.set_component_id(1);
filter.add_message_ids(30);

size_t received = 0;
router.subscribe(filter, [&](const mavlink::MavlinkMessage&) {
received++;
return true;
});

// Message 1: Matches all
mavlink::MavlinkMessage msg1;
msg1.set_system_id(1);
msg1.set_component_id(1);
msg1.set_message_id(30);
router.route_message(msg1);
TEST_ASSERT(received == 1);

// Message 2: Different system ID (2)
mavlink::MavlinkMessage msg2;
msg2.set_system_id(2);
msg2.set_component_id(1);
msg2.set_message_id(30);
router.route_message(msg2);
TEST_ASSERT(received == 1); // shouldn't increase

// Message 3: Different component ID (2)
mavlink::MavlinkMessage msg3;
msg3.set_system_id(1);
msg3.set_component_id(2);
msg3.set_message_id(30);
router.route_message(msg3);
TEST_ASSERT(received == 1); // shouldn't increase

// Message 4: Different message ID (0 - HEARTBEAT)
mavlink::MavlinkMessage msg4;
msg4.set_system_id(1);
msg4.set_component_id(1);
msg4.set_message_id(0);
router.route_message(msg4);
TEST_ASSERT(received == 1); // shouldn't increase

std::cout << "test_router_filters passed!" << std::endl;
}

void test_router_cleanup() {
std::cout << "Running test_router_cleanup..." << std::endl;
Router router;

mavlink::StreamFilter filter;

// Client whose write fails (simulates disconnected client)
router.subscribe(filter, [](const mavlink::MavlinkMessage&) {
return false; // write failed
});

TEST_ASSERT(router.subscription_count() == 1);

// Route a message - this will trigger write failure and mark inactive
mavlink::MavlinkMessage msg;
size_t routed = router.route_message(msg);

TEST_ASSERT(routed == 0);
TEST_ASSERT(router.subscription_count() == 0); // Active subscription count should drop to 0

// Cleanup should remove it from internal list
size_t cleaned = router.cleanup_inactive();
TEST_ASSERT(cleaned == 1);

std::cout << "test_router_cleanup passed!" << std::endl;
}

void run_router_tests() {
test_router_basic();
test_router_filters();
test_router_cleanup();
}
19 changes: 19 additions & 0 deletions bridge/test/test_runner.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <iostream>

extern void run_router_tests();
extern void run_converter_tests();

int main() {
std::cout << "========================================" << std::endl;
std::cout << "Starting C++ Unit Tests" << std::endl;
std::cout << "========================================" << std::endl;

run_router_tests();
run_converter_tests();

std::cout << "========================================" << std::endl;
std::cout << "All C++ Unit Tests Passed Successfully!" << std::endl;
std::cout << "========================================" << std::endl;

return 0;
}
Loading
Loading