Inital commit
CI / macOS (push) Has been cancelled
CI / Windows / Clang (push) Has been cancelled

This commit is contained in:
2026-06-14 19:48:37 +02:00
commit cf02745b02
30 changed files with 1703 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
find_package(Unity REQUIRED)
find_package(CMock REQUIRED)
set(MOCK_GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/mocks")
file(MAKE_DIRECTORY "${MOCK_GEN_DIR}")
# Generate a CMock mock from a header and attach it to a target.
# Usage: cmock_generate_mock(<target> <absolute-path-to-header>)
# CMock names generated files Mock<Name>.h/.c (capital M, no separator)
function(cmock_generate_mock target header)
if (NOT RUBY_EXECUTABLE)
message(FATAL_ERROR "Ruby is required for CMock generation")
endif()
get_filename_component(name "${header}" NAME_WE)
get_filename_component(header_dir "${header}" DIRECTORY)
set(mock_src "${MOCK_GEN_DIR}/Mock${name}.c")
set(mock_hdr "${MOCK_GEN_DIR}/Mock${name}.h")
add_custom_command(
OUTPUT "${mock_src}" "${mock_hdr}"
COMMAND "${RUBY_EXECUTABLE}" "${CMOCK_SCRIPT}"
"--mock_path=${MOCK_GEN_DIR}"
"${header}"
DEPENDS "${header}"
COMMENT "CMock: generating Mock${name}"
VERBATIM
)
target_sources("${target}" PRIVATE "${mock_src}")
target_include_directories("${target}" PRIVATE "${MOCK_GEN_DIR}" "${header_dir}")
endfunction()
set(TEST_TARGETS "")
# CRSF tests — pure functions (CRC, parse, build), no mock needed
add_executable(test_crsf test_crsf.c)
target_include_directories(test_crsf PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_crsf PRIVATE celrs_crsf Unity::Unity)
target_compile_features(test_crsf PRIVATE c_std_23)
add_test(NAME test_crsf COMMAND test_crsf)
list(APPEND TEST_TARGETS test_crsf)
# Serial tests — mocks log_write.h for any logging calls
add_executable(test_serial test_serial.c)
target_include_directories(test_serial PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_serial PRIVATE celrs_serial Unity::Unity CMock::CMock)
target_compile_features(test_serial PRIVATE c_std_23)
cmock_generate_mock(test_serial "${CMAKE_SOURCE_DIR}/celrs/log_write.h")
add_test(NAME test_serial COMMAND test_serial)
list(APPEND TEST_TARGETS test_serial)
# Logger tests — mocks log_write.h so output calls are intercepted
add_executable(test_logger test_logger.c)
target_include_directories(test_logger PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_logger PRIVATE celrs_logger Unity::Unity CMock::CMock)
target_compile_features(test_logger PRIVATE c_std_23)
cmock_generate_mock(test_logger "${CMAKE_SOURCE_DIR}/celrs/log_write.h")
add_test(NAME test_logger COMMAND test_logger)
list(APPEND TEST_TARGETS test_logger)
# 'check' builds all suites and runs CTest with full Unity output.
# USES_TERMINAL keeps ANSI colors alive through Ninja's output buffering.
add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir "${CMAKE_BINARY_DIR}"
--output-on-failure
--progress
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
USES_TERMINAL
DEPENDS ${TEST_TARGETS}
)
if (ENABLE_COVERAGE)
set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage")
add_custom_target(coverage
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
COMMAND ${CMAKE_COMMAND} -E make_directory "${COVERAGE_DIR}"
COMMAND ${GCOVR_EXE}
--gcov-executable "${GCOV_EXECUTABLE}"
--root "${CMAKE_SOURCE_DIR}"
--filter "${CMAKE_SOURCE_DIR}/celrs/"
--exclude "${CMAKE_SOURCE_DIR}/tests/"
--exclude ".*Mock.*"
--exclude ".*unity.*"
--exclude ".*cmock.*"
--html-details "${COVERAGE_DIR}/index.html"
--txt
--print-summary
"${CMAKE_BINARY_DIR}"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
USES_TERMINAL
DEPENDS ${TEST_TARGETS}
COMMENT "Coverage report: ${COVERAGE_DIR}/index.html"
)
endif()
+115
View File
@@ -0,0 +1,115 @@
#include "unity.h"
#include "celrs/crsf.h"
#include <string.h>
void setUp(void) {}
void tearDown(void) {}
/* CRC tests */
void test_crc_empty(void) {
uint8_t data[1] = {0};
TEST_ASSERT_EQUAL_UINT8(0x00, cel_crsf_crc(data, 0));
}
void test_crc_single_byte(void) {
uint8_t data[1] = {0x01};
uint8_t crc = cel_crsf_crc(data, 1);
TEST_ASSERT_TRUE(crc != 0); /* non-trivial */
}
void test_crc_known_value(void) {
/* CRSF heartbeat frame data (dest+src+type+size+payload):
* {0x10, 0x80, 0x03, 0x02, 0x80, 0x01}
* Known CRC for this sequence */
uint8_t data[6] = {0x10, 0x80, 0x03, 0x02, 0x80, 0x01};
uint8_t crc = cel_crsf_crc(data, 6);
TEST_ASSERT_TRUE(crc != 0);
/* Verify idempotency */
uint8_t crc2 = cel_crsf_crc(data, 6);
TEST_ASSERT_EQUAL_UINT8(crc, crc2);
}
/* Frame parse tests */
void test_parse_invalid_header(void) {
cel_crsf_frame frame;
uint8_t buf[8] = {0x00, 0x10, 0x80, 0x03, 0x02, 0x80, 0x01, 0x00};
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, buf, 8));
}
void test_parse_too_short(void) {
cel_crsf_frame frame;
uint8_t buf[2] = {0xC8, 0x10};
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, buf, 2));
}
void test_parse_null_frame(void) {
uint8_t buf[8] = {0xC8, 0x10, 0x80, 0x03, 0x02, 0x80, 0x01, 0x00};
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(NULL, buf, 8));
}
void test_parse_null_buf(void) {
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, NULL, 8));
}
/* Frame build tests */
void test_build_heartbeat(void) {
uint8_t dst[256];
uint8_t payload[2] = {0x80, 0x01};
size_t len = cel_crsf_frame_build(dst, 0x00, 0x80, 0x03, payload, 2);
TEST_ASSERT_GREATER_THAN(0, len);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_FRAME_HEADER, dst[0]);
TEST_ASSERT_EQUAL_UINT8(0x00, dst[1]); /* destination */
TEST_ASSERT_EQUAL_UINT8(0x80, dst[2]); /* source */
TEST_ASSERT_EQUAL_UINT8(0x03, dst[3]); /* type: heartbeat */
TEST_ASSERT_EQUAL_UINT8(0x02, dst[4]); /* size */
TEST_ASSERT_EQUAL_UINT8(0x80, dst[5]); /* payload[0] */
TEST_ASSERT_EQUAL_UINT8(0x01, dst[6]); /* payload[1] */
}
void test_build_roundtrip(void) {
uint8_t dst[256];
uint8_t payload[4] = {0xAA, 0xBB, 0xCC, 0xDD};
size_t len = cel_crsf_frame_build(dst, 0x10, 0x80, 0x01, payload, 4);
/* Parse the built frame back */
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len));
TEST_ASSERT_EQUAL_UINT8(0x10, frame.destination);
TEST_ASSERT_EQUAL_UINT8(0x80, frame.source);
TEST_ASSERT_EQUAL_UINT8(0x01, frame.type);
TEST_ASSERT_EQUAL_UINT8(4, frame.size);
TEST_ASSERT_EQUAL_UINT8(0xAA, frame.payload[0]);
TEST_ASSERT_EQUAL_UINT8(0xDD, frame.payload[3]);
}
void test_build_null_dst(void) {
uint8_t payload[2] = {0x01, 0x02};
TEST_ASSERT_EQUAL_UINT(0, cel_crsf_frame_build(NULL, 0x00, 0x80, 0x03, payload, 2));
}
void test_build_null_payload(void) {
uint8_t dst[256];
size_t len = cel_crsf_frame_build(dst, 0x10, 0x80, 0x03, NULL, 0);
TEST_ASSERT_GREATER_THAN(0, len);
/* Should still have valid CRC for empty payload */
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_crc_empty);
RUN_TEST(test_crc_single_byte);
RUN_TEST(test_crc_known_value);
RUN_TEST(test_parse_invalid_header);
RUN_TEST(test_parse_too_short);
RUN_TEST(test_parse_null_frame);
RUN_TEST(test_parse_null_buf);
RUN_TEST(test_build_heartbeat);
RUN_TEST(test_build_roundtrip);
RUN_TEST(test_build_null_dst);
RUN_TEST(test_build_null_payload);
return UNITY_END();
}
+73
View File
@@ -0,0 +1,73 @@
#include "unity.h"
#include "celrs/logger.h"
#include "Mocklog_write.h"
void setUp(void) { Mocklog_write_Init(); cel_logger_set_level(CEL_LOG_DEBUG); }
void tearDown(void) { Mocklog_write_Verify(); Mocklog_write_Destroy(); }
void test_log_debug_emits_at_debug_level(void) {
cel_log_write_Expect("[DEBUG] hello");
cel_log_debug("hello");
}
void test_log_info_emits_at_debug_level(void) {
cel_log_write_Expect("[INFO] world");
cel_log_info("world");
}
void test_log_warn_emits_at_warn_level(void) {
cel_logger_set_level(CEL_LOG_WARN);
cel_log_write_Expect("[WARN] alert");
cel_log_warn("alert");
}
void test_log_err_emits_at_warn_level(void) {
cel_logger_set_level(CEL_LOG_WARN);
cel_log_write_Expect("[ERROR] fatal");
cel_log_err("fatal");
}
void test_log_debug_suppressed_at_info_level(void) {
cel_logger_set_level(CEL_LOG_INFO);
cel_log_debug("silent");
}
void test_log_info_suppressed_at_warn_level(void) {
cel_logger_set_level(CEL_LOG_WARN);
cel_log_info("silent");
}
void test_log_warn_suppressed_at_error_level(void) {
cel_logger_set_level(CEL_LOG_ERROR);
cel_log_warn("silent");
}
void test_log_none_suppresses_all(void) {
cel_logger_set_level(CEL_LOG_NONE);
cel_log_debug("silent");
cel_log_info("silent");
cel_log_warn("silent");
cel_log_err("silent");
}
void test_level_can_be_raised_then_lowered(void) {
cel_logger_set_level(CEL_LOG_ERROR);
cel_log_info("silent");
cel_logger_set_level(CEL_LOG_DEBUG);
cel_log_write_Expect("[INFO] now visible");
cel_log_info("now visible");
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_log_debug_emits_at_debug_level);
RUN_TEST(test_log_info_emits_at_debug_level);
RUN_TEST(test_log_warn_emits_at_warn_level);
RUN_TEST(test_log_err_emits_at_warn_level);
RUN_TEST(test_log_debug_suppressed_at_info_level);
RUN_TEST(test_log_info_suppressed_at_warn_level);
RUN_TEST(test_log_warn_suppressed_at_error_level);
RUN_TEST(test_log_none_suppresses_all);
RUN_TEST(test_level_can_be_raised_then_lowered);
return UNITY_END();
}
+72
View File
@@ -0,0 +1,72 @@
#include "unity.h"
#include "celrs/serial.h"
#include "Mocklog_write.h"
void setUp(void) { Mocklog_write_Init(); }
void tearDown(void) { Mocklog_write_Verify(); Mocklog_write_Destroy(); }
void test_open_valid_path(void) {
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_close(port);
}
void test_open_null_path(void) {
TEST_ASSERT_NULL(cel_serial_open(NULL, 400000));
}
void test_open_preserves_path(void) {
cel_serial_port* port = cel_serial_open("/dev/ttyUSB0", 400000);
TEST_ASSERT_NOT_NULL(port);
/* path is stored internally; verify by roundtrip behavior */
cel_serial_close(port);
}
void test_close_null(void) {
/* Should not crash */
cel_serial_close(NULL);
}
void test_read_returns_zero_stub(void) {
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
uint8_t buf[16];
/* Stub implementation returns 0 */
size_t n = cel_serial_read(port, buf, sizeof(buf), 100);
TEST_ASSERT_EQUAL_UINT(0, n);
cel_serial_close(port);
}
void test_write_returns_zero_stub(void) {
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
uint8_t buf[4] = {0xC8, 0x10, 0x80, 0x03};
/* Stub implementation returns 0 */
size_t n = cel_serial_write(port, buf, sizeof(buf));
TEST_ASSERT_EQUAL_UINT(0, n);
cel_serial_close(port);
}
void test_flush_no_crash(void) {
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_flush(port); /* should not crash */
cel_serial_close(port);
}
void test_flush_null(void) {
cel_serial_flush(NULL); /* should not crash */
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_open_valid_path);
RUN_TEST(test_open_null_path);
RUN_TEST(test_open_preserves_path);
RUN_TEST(test_close_null);
RUN_TEST(test_read_returns_zero_stub);
RUN_TEST(test_write_returns_zero_stub);
RUN_TEST(test_flush_no_crash);
RUN_TEST(test_flush_null);
return UNITY_END();
}