From 78f46d9cb6e3593b6166129e67a40cd7682823ab Mon Sep 17 00:00:00 2001 From: Euripedes Rocha Filho Date: Thu, 1 Oct 2026 09:48:19 +0200 Subject: [PATCH] ci: Adds testing to the message library --- .gitlab/ci/test.yml | 2 + test/mqtt_msg_host_test/CMakeLists.txt | 9 + test/mqtt_msg_host_test/README.md | 13 ++ test/mqtt_msg_host_test/main/CMakeLists.txt | 24 +++ .../mqtt_msg_host_test/main/idf_component.yml | 3 + test/mqtt_msg_host_test/main/test_main.cpp | 23 +++ .../mqtt_msg_host_test/main/test_mqtt_msg.cpp | 155 ++++++++++++++++++ test/mqtt_msg_host_test/sdkconfig.ci.coverage | 1 + test/mqtt_msg_host_test/sdkconfig.defaults | 8 + 9 files changed, 238 insertions(+) create mode 100644 test/mqtt_msg_host_test/CMakeLists.txt create mode 100644 test/mqtt_msg_host_test/README.md create mode 100644 test/mqtt_msg_host_test/main/CMakeLists.txt create mode 100644 test/mqtt_msg_host_test/main/idf_component.yml create mode 100644 test/mqtt_msg_host_test/main/test_main.cpp create mode 100644 test/mqtt_msg_host_test/main/test_mqtt_msg.cpp create mode 100644 test/mqtt_msg_host_test/sdkconfig.ci.coverage create mode 100644 test/mqtt_msg_host_test/sdkconfig.defaults diff --git a/.gitlab/ci/test.yml b/.gitlab/ci/test.yml index 0bc5895..7294977 100644 --- a/.gitlab/ci/test.yml +++ b/.gitlab/ci/test.yml @@ -39,5 +39,7 @@ host_tests: ./build_linux_default/mqtt_utils_host_test.elf cd ../mqtt_outbox_host_test ./build_linux_coverage/mqtt_outbox_host_test.elf + cd ../mqtt_msg_host_test + ./build_linux_coverage/mqtt_msg_host_test.elf cd ../.. gcovr --gcov-ignore-parse-errors -k -r . --exclude '.*/managed_components/.*' --print-summary --html coverage.html -x coverage.xml diff --git a/test/mqtt_msg_host_test/CMakeLists.txt b/test/mqtt_msg_host_test/CMakeLists.txt new file mode 100644 index 0000000..c36168e --- /dev/null +++ b/test/mqtt_msg_host_test/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) + +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/CPM.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/RapidCheck.cmake) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) + +project(mqtt_msg_host_test) diff --git a/test/mqtt_msg_host_test/README.md b/test/mqtt_msg_host_test/README.md new file mode 100644 index 0000000..78dceba --- /dev/null +++ b/test/mqtt_msg_host_test/README.md @@ -0,0 +1,13 @@ +# mqtt_msg host tests + +Isolated host tests for `lib/mqtt_msg`. Tests call the message API directly — +no outbox, MQTT client, transport, or FreeRTOS scheduler required. + +## Build and run + +```bash +cd test/mqtt_msg_host_test +idf.py --preview set-target linux +idf.py build +./build/mqtt_msg_host_test.elf +``` diff --git a/test/mqtt_msg_host_test/main/CMakeLists.txt b/test/mqtt_msg_host_test/main/CMakeLists.txt new file mode 100644 index 0000000..3e5d69d --- /dev/null +++ b/test/mqtt_msg_host_test/main/CMakeLists.txt @@ -0,0 +1,24 @@ +idf_component_register(SRCS "test_main.cpp" "test_mqtt_msg.cpp" + INCLUDE_DIRS "." + REQUIRES heap log + WHOLE_ARCHIVE) + +set(SANITIZER_FLAGS -fsanitize=address,undefined -fno-sanitize-recover=undefined) +target_compile_options(${COMPONENT_LIB} PRIVATE ${SANITIZER_FLAGS} -Wno-missing-field-initializers) +target_link_options(${COMPONENT_LIB} INTERFACE ${SANITIZER_FLAGS}) + +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../../lib/mqtt_msg + ${CMAKE_CURRENT_BINARY_DIR}/mqtt_msg) + +target_compile_options(mqtt_msg_lib PRIVATE ${SANITIZER_FLAGS}) + +target_link_libraries(${COMPONENT_LIB} PUBLIC + idf::mqtt::msg + Catch2::Catch2WithMain + rapidcheck + rapidcheck_catch) + +if(CONFIG_GCOV_ENABLED) + target_compile_options(mqtt_msg_lib PUBLIC --coverage -fprofile-arcs -ftest-coverage) + target_link_options(mqtt_msg_lib PUBLIC --coverage -fprofile-arcs -ftest-coverage) +endif() diff --git a/test/mqtt_msg_host_test/main/idf_component.yml b/test/mqtt_msg_host_test/main/idf_component.yml new file mode 100644 index 0000000..6898760 --- /dev/null +++ b/test/mqtt_msg_host_test/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + espressif/catch2: + version: "*" diff --git a/test/mqtt_msg_host_test/main/test_main.cpp b/test/mqtt_msg_host_test/main/test_main.cpp new file mode 100644 index 0000000..8c8f9bc --- /dev/null +++ b/test/mqtt_msg_host_test/main/test_main.cpp @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include + +extern "C" void app_main(void) +{ + int argc = 1; + const char *argv[2] = {"mqtt_msg_host_test", nullptr}; + int result = Catch::Session().run(argc, argv); + + if (result != 0) { + printf("Test failed with result %d\n", result); + exit(1); + } else { + printf("Test passed.\n"); + exit(0); + } +} diff --git a/test/mqtt_msg_host_test/main/test_mqtt_msg.cpp b/test/mqtt_msg_host_test/main/test_mqtt_msg.cpp new file mode 100644 index 0000000..829cbf2 --- /dev/null +++ b/test/mqtt_msg_host_test/main/test_mqtt_msg.cpp @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "mqtt_msg.h" + + int platform_random(int max) + { + if (max <= 0) { + return 0; + } + + static std::mt19937 engine{std::random_device{}()}; + std::uniform_int_distribution distribution(0, max - 1); + return distribution(engine); + } +} + +namespace +{ + +constexpr size_t mqtt_fixed_header_size = 5; + +struct MessageDeleter { + void operator()(mqtt_message_t *message) const + { + mqtt_msg_destroy(message); + } +}; + +using MessagePtr = std::unique_ptr; + +MessagePtr make_message(size_t buffer_size) +{ + return MessagePtr(mqtt_msg_create(buffer_size)); +} + +std::string generated_bytes(int length, int minimum, int maximum) +{ + std::string value(static_cast(length), '\0'); + + for (char &byte : value) { + byte = static_cast(*rc::gen::inRange(minimum, maximum)); + } + + return value; +} + +} // namespace + +TEST_CASE("mqtt_msg_pingreq encodes the fixed MQTT control packet") +{ + MessagePtr message = make_message(mqtt_fixed_header_size); + REQUIRE(message != nullptr); + mqtt_message_t *encoded = mqtt_msg_pingreq(message.get()); + REQUIRE(encoded == message.get()); + REQUIRE(encoded->length == 2); + REQUIRE(mqtt_get_type(encoded->data) == MQTT_MSG_TYPE_PINGREQ); + REQUIRE(encoded->data[0] == 0xC0); + REQUIRE(encoded->data[1] == 0x00); + REQUIRE(mqtt_has_valid_msg_hdr(encoded->data, encoded->length) == 1); +} + +TEST_CASE("mqtt_msg_publish rejects an empty topic") +{ + MessagePtr message = make_message(32); + REQUIRE(message != nullptr); + uint16_t message_id = 7; + mqtt_message_t *encoded = mqtt_msg_publish(message.get(), "", "payload", 7, 0, 0, &message_id); + REQUIRE(encoded == message.get()); + REQUIRE(encoded->length == 0); +} + +TEST_CASE("mqtt_msg_publish round-trips an empty payload") +{ + const int qos_values[] = {0, 1}; + + for (const int qos : qos_values) { + const size_t buffer_size = mqtt_fixed_header_size + 2 + 1 + (qos > 0 ? 2 : 0); + MessagePtr message = make_message(buffer_size); + REQUIRE(message != nullptr); + uint16_t message_id = 0; + mqtt_message_t *encoded = mqtt_msg_publish(message.get(), "t", nullptr, 0, qos, 0, &message_id); + REQUIRE(encoded == message.get()); + REQUIRE(encoded->length > 0); + size_t payload_length = encoded->length; + const char *payload = mqtt_get_publish_data(encoded->data, &payload_length); + REQUIRE(payload != nullptr); + REQUIRE(payload_length == 0); + + if (qos == 0) { + REQUIRE(message_id == 0); + } else { + REQUIRE(message_id != 0); + REQUIRE(mqtt_get_id(encoded->data, encoded->length) == message_id); + } + } +} + +TEST_CASE("mqtt_msg_publish round-trips a complete packet") +{ + rc::prop("encoded publish recovers topic, payload, flags, and packet id", []() { + const int qos = *rc::gen::inRange(0, 3); + const int retain = *rc::gen::inRange(0, 2); + const std::string topic = generated_bytes(*rc::gen::inRange(1, 65), 1, 127); + const std::string payload = generated_bytes(*rc::gen::inRange(0, 129), 0, 256); + const size_t buffer_size = mqtt_fixed_header_size + 2 + topic.size() + + (qos > 0 ? 2 : 0) + payload.size(); + MessagePtr message = make_message(buffer_size); + RC_ASSERT(message != nullptr); + uint16_t message_id = 0; + mqtt_message_t *encoded = mqtt_msg_publish(message.get(), topic.c_str(), payload.data(), + static_cast(payload.size()), qos, retain, &message_id); + RC_ASSERT(encoded == message.get()); + RC_ASSERT(encoded->length > 0); + RC_ASSERT(encoded->length <= buffer_size); + int fixed_header_size = 0; + const size_t total_length = mqtt_get_total_length(encoded->data, encoded->length, &fixed_header_size); + RC_ASSERT(total_length == encoded->length); + RC_ASSERT(fixed_header_size >= 2); + RC_ASSERT(mqtt_get_type(encoded->data) == MQTT_MSG_TYPE_PUBLISH); + RC_ASSERT(mqtt_get_qos(encoded->data) == qos); + RC_ASSERT(mqtt_get_retain(encoded->data) == retain); + RC_ASSERT(mqtt_get_dup(encoded->data) == 0); + RC_ASSERT(mqtt_has_valid_msg_hdr(encoded->data, encoded->length) == 1); + size_t topic_length = encoded->length; + const char *encoded_topic = mqtt_get_publish_topic(encoded->data, &topic_length); + RC_ASSERT(encoded_topic != nullptr); + RC_ASSERT(topic_length == topic.size()); + RC_ASSERT(std::memcmp(encoded_topic, topic.data(), topic.size()) == 0); + size_t payload_length = encoded->length; + const char *encoded_payload = mqtt_get_publish_data(encoded->data, &payload_length); + RC_ASSERT(encoded_payload != nullptr); + RC_ASSERT(payload_length == payload.size()); + RC_ASSERT(std::memcmp(encoded_payload, payload.data(), payload.size()) == 0); + + if (qos == 0) { + RC_ASSERT(message_id == 0); + } else { + RC_ASSERT(message_id != 0); + RC_ASSERT(mqtt_get_id(encoded->data, encoded->length) == message_id); + } + }); +} diff --git a/test/mqtt_msg_host_test/sdkconfig.ci.coverage b/test/mqtt_msg_host_test/sdkconfig.ci.coverage new file mode 100644 index 0000000..3c490c0 --- /dev/null +++ b/test/mqtt_msg_host_test/sdkconfig.ci.coverage @@ -0,0 +1 @@ +CONFIG_GCOV_ENABLED=y diff --git a/test/mqtt_msg_host_test/sdkconfig.defaults b/test/mqtt_msg_host_test/sdkconfig.defaults new file mode 100644 index 0000000..d695075 --- /dev/null +++ b/test/mqtt_msg_host_test/sdkconfig.defaults @@ -0,0 +1,8 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_COMPILER_CXX_RTTI=y +CONFIG_COMPILER_CXX_EXCEPTIONS_EMG_POOL_SIZE=0 +CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=y +CONFIG_LOG_COLORS=n +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=10000