diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 8fcce90..7e5e1b7 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -62,3 +62,14 @@ jobs: cmake --build cbuild working-directory: test + - name: Build stubbed tests + working-directory: stubbed_tests + run: | + mkdir build && cd build + CC=clang CXX=clang++ cmake -DCMAKE_BUILD_TYPE=RelWithDebug .. + cmake --build . -j $(nproc) + + - name: Run stubbed tests + working-directory: stubbed_tests/build + run: ./stubbed_tests + diff --git a/lib/platform/linux/platform.c b/lib/platform/linux/platform.c index c12cd23..a01289d 100644 --- a/lib/platform/linux/platform.c +++ b/lib/platform/linux/platform.c @@ -189,8 +189,6 @@ static void * poll_for_indication(void * unused) // Initially wait for 500ms before any polling uint32_t wait_before_next_polling_ms = 500; - m_polling_thread_state_request = POLLING_THREAD_RUN; - while (m_polling_thread_state_request != POLLING_THREAD_STOP) { usleep(wait_before_next_polling_ms * 1000); @@ -378,6 +376,7 @@ bool Platform_init(Platform_get_indication_f get_indication_f, } // Start a thread to poll for indication + m_polling_thread_state_request = POLLING_THREAD_RUN; if (pthread_create(&thread_polling, NULL, poll_for_indication, NULL) != 0) { LOGE("Cannot create polling thread\n"); diff --git a/stubbed_tests/CMakeLists.txt b/stubbed_tests/CMakeLists.txt new file mode 100644 index 0000000..f39d0d3 --- /dev/null +++ b/stubbed_tests/CMakeLists.txt @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 3.18) + +project(c-mesh-api-stubbed-tests LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE RelWithDebInfo) +endif() +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +set(TEST_EXTRA_CFLAGS "" CACHE STRING "Extra compiler flags for WPC library and test code. For example '--coverage'.") +set(TEST_EXTRA_LDFLAGS "" CACHE STRING "Extra linker flags for the test executable. For example '--coverage'.") +separate_arguments(TEST_EXTRA_CFLAGS) +separate_arguments(TEST_EXTRA_LDFLAGS) + +set(WPC_LIB_DIR "${CMAKE_CURRENT_LIST_DIR}/../lib/") + +include(FetchContent) +FetchContent_Declare( + wpc-lib + SOURCE_DIR ${WPC_LIB_DIR} +) +FetchContent_Declare( + fuzztest + GIT_REPOSITORY https://github.com/google/fuzztest/ + GIT_TAG 2025-08-05 +) +FetchContent_MakeAvailable(wpc-lib fuzztest) + +enable_testing() + +fuzztest_setup_fuzzing_flags() +add_executable(stubbed_tests + tests/basic_tests.cpp + tests/ul_fuzz_tests.cpp + tests/utils/queued_ul_data_handler.cpp +) +target_include_directories(stubbed_tests PRIVATE tests) + +add_subdirectory(serialstub) + +target_include_directories(serialstub PRIVATE ${WPC_LIB_DIR}/platform) + +########## +# Somewhat hacky way to suppress warnings for fuzztest library. This allows us +# to use stricter compile flags for warnings in our testing code that includes +# files from the fuzztest. +get_target_property(LIB_INCLUDE_DIRS fuzztest_fuzztest INTERFACE_INCLUDE_DIRECTORIES) +if(LIB_INCLUDE_DIRS) + set_target_properties(fuzztest_fuzztest PROPERTIES + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${LIB_INCLUDE_DIRS}" + ) +endif() +get_target_property(LIB_INCLUDE_DIRS fuzztest_fuzztest_gtest_main INTERFACE_INCLUDE_DIRECTORIES) +if(LIB_INCLUDE_DIRS) + set_target_properties(fuzztest_fuzztest_gtest_main PROPERTIES + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${LIB_INCLUDE_DIRS}" + ) +endif() +########## + +target_compile_options(wpc PRIVATE ${TEST_EXTRA_CFLAGS} -Werror -Wall -Wextra) +target_compile_options(wpc_platform PRIVATE ${TEST_EXTRA_CFLAGS} -Werror -Wall -Wextra) +target_compile_options(stubbed_tests PRIVATE ${TEST_EXTRA_CFLAGS} -Wall -Werror -Wextra) +target_link_options(stubbed_tests PRIVATE ${TEST_EXTRA_LDFLAGS}) + +# "serialstub" should come before "wpc_platform" so that the serial implementation +# from "wpc_platform" is not taken into use. +target_link_libraries(stubbed_tests PRIVATE + GTest::gmock + wpc + serialstub + wpc_platform +) + +include(GoogleTest) +link_fuzztest(stubbed_tests) +gtest_discover_tests(stubbed_tests) + diff --git a/stubbed_tests/README.md b/stubbed_tests/README.md new file mode 100644 index 0000000..6719a51 --- /dev/null +++ b/stubbed_tests/README.md @@ -0,0 +1,48 @@ +# Introduction + +These tests use a stub implementation for the serial interface and can be run +without a real serial device. See README.md under serialstub for more detils. + +There are two types of tests included; regular unit tests and fuzz tests. The +tests use [FuzzTest](https://github.com/google/fuzztest) and +[GTest](https://github.com/google/googletest). + +# Requirements +[Clang](https://clang.llvm.org/), since FuzzTest requires it. + +# Building +FuzzTest tests can be run as unit tests or in fuzzing mode. For more +information refer to FuzzTest documentation +[here](https://github.com/google/fuzztest/blob/main/doc/quickstart-cmake.md). + +Example command to build for unit testing: + +```shell +mkdir build && cd build +CC=clang CXX=clang++ cmake -DCMAKE_BUILD_TYPE=RelWithDebug .. +cmake --build . -j $(nproc) +``` + +# Running the tests + +To run unit tests together with fuzz tests in unit testing mode: +```shell +./stubbed_tests +``` + +Refer to FuzzTest documentation on how to run fuzz tests in fuzzing mode. + +## Naming conventions +Test suites that support fuzzing should start with "Fuzz". This way you can +filter tests when running, for example to run only fuzz tests: + +```shell +./stubbed_tests --gtest_filter="Fuzz*" +``` + +And to run non-fuzz tests: + +```shell +./stubbed_tests --gtest_filter="-Fuzz*" +``` + diff --git a/stubbed_tests/serialstub/CMakeLists.txt b/stubbed_tests/serialstub/CMakeLists.txt new file mode 100644 index 0000000..c687438 --- /dev/null +++ b/stubbed_tests/serialstub/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(serialstub STATIC + ${CMAKE_CURRENT_LIST_DIR}/serialstub/serial.cpp + ${CMAKE_CURRENT_LIST_DIR}/serialstub/serial_stub.cpp + ${CMAKE_CURRENT_LIST_DIR}/serialstub/wpc_frame.cpp + ${CMAKE_CURRENT_LIST_DIR}/serialstub/misc/crc.cpp +) + +target_include_directories(serialstub PUBLIC + ${PROJECT_SOURCE_DIR}/platform + ${CMAKE_CURRENT_LIST_DIR} +) + +set_target_properties(serialstub PROPERTIES LINKER_LANGUAGE CXX) +target_compile_options(serialstub PRIVATE -Wall -Werror -Wextra) + diff --git a/stubbed_tests/serialstub/README.md b/stubbed_tests/serialstub/README.md new file mode 100644 index 0000000..a09a490 --- /dev/null +++ b/stubbed_tests/serialstub/README.md @@ -0,0 +1,17 @@ +# Introduction + +This is a stub serial implementation for the c-mesh-api platform interface +(lib/platform/serial.h). Intended use is to build on top of the linux platform +implementation. Since only serial.h is implemented, the resulting object from +this stub should be linked before the default platform objects so that the +default serial implementation is not taken into use, while rest of the platform +implementation is. + +The stub has a wrapper for Dual MCU frames. A handler is assigned for each +frame which is normally sent to the serial device. The handler should return +responses for every call (these are called "confirm" or "indication" in the +DualMCU API.) + +For more information, see the Wirepas DualMCU API documentation (for example +[here](https://github.com/wirepas/wm-sdk-5g/blob/rel_1.2.0_5G/libraries/dualmcu/api/DualMcuAPI.md)) + diff --git a/stubbed_tests/serialstub/serialstub/frame_handler.h b/stubbed_tests/serialstub/serialstub/frame_handler.h new file mode 100644 index 0000000..982dcec --- /dev/null +++ b/stubbed_tests/serialstub/serialstub/frame_handler.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +class FrameHandler +{ +public: + using ptr = std::shared_ptr; + + virtual ~FrameHandler() = default; + + /** + * \brief Handle a Dual-MCU API frame + * + * Handles a request frame and returns response frames. As an example, + * multiple responses to a single request are possible when responding to + * poll requests. + * + * In this context, a "request" is a frame sent by the application and a + * "response" is a frame sent by the stub back to the application. + * + * In Dual-MCU API specification, "request" messages are the following: + * - request + * - response + * And the "response" messages are the following: + * - confirm + * - indication + * + * \param frame + * Request frame to be handled + * \return Response frames + */ + virtual std::vector Handle(const WpcFrame& frame) = 0; +}; + diff --git a/stubbed_tests/serialstub/serialstub/misc/crc.cpp b/stubbed_tests/serialstub/serialstub/misc/crc.cpp new file mode 100644 index 0000000..a0bdbee --- /dev/null +++ b/stubbed_tests/serialstub/serialstub/misc/crc.cpp @@ -0,0 +1,13 @@ +#include "crc.h" + +uint16_t Crc::FromBuffer(const uint8_t* const buffer, const size_t length) +{ + uint16_t crc = 0xffff; + for (size_t i = 0; i < length; i++) + { + const uint8_t index = buffer[i] ^ (crc >> 8); + crc = crc_ccitt_lut[index] ^ (crc << 8); + } + return crc; +} + diff --git a/stubbed_tests/serialstub/serialstub/misc/crc.h b/stubbed_tests/serialstub/serialstub/misc/crc.h new file mode 100644 index 0000000..55fe347 --- /dev/null +++ b/stubbed_tests/serialstub/serialstub/misc/crc.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +namespace Crc +{ + +static const uint16_t crc_ccitt_lut[] = +{ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 +}; + +uint16_t FromBuffer(const uint8_t* const buffer, const size_t length); + +} // namespace Crc + diff --git a/stubbed_tests/serialstub/serialstub/serial.cpp b/stubbed_tests/serialstub/serialstub/serial.cpp new file mode 100644 index 0000000..b986c2f --- /dev/null +++ b/stubbed_tests/serialstub/serialstub/serial.cpp @@ -0,0 +1,36 @@ +#include + +extern "C" { +#include + + int Serial_open([[maybe_unused]] const char * port_name, [[maybe_unused]] unsigned long bitrate) + { + return 0; + } + + int Serial_close() + { + return 0; + } + + int Serial_read(unsigned char * c, [[maybe_unused]] unsigned int timeout_ms) + { + if (const auto& byte = serial_stub.PopOutByte(); byte.has_value()) + { + *c = *byte; + return 1; + } + + return 0; + } + + int Serial_write(const unsigned char * buffer, unsigned int buffer_size) + { + // This is ok because in practice, c-mesh-api always sends a whole + // frame with a single Serial_write command. + serial_stub.ReadAndHandleFrame(buffer, buffer_size); + return buffer_size; + } + +} + diff --git a/stubbed_tests/serialstub/serialstub/serial_stub.cpp b/stubbed_tests/serialstub/serialstub/serial_stub.cpp new file mode 100644 index 0000000..626384b --- /dev/null +++ b/stubbed_tests/serialstub/serialstub/serial_stub.cpp @@ -0,0 +1,159 @@ +#include "serial_stub.h" + +std::optional SerialStub::ReadFrame(const uint8_t* const buffer, const size_t buffer_size) +{ + // Use value initialization for WpcFrame so that the underlying payload + // array is initialized with zeroes. This makes it easier to compare two + // arrays for assertions in the tests. + WpcFrame decoded {}; + size_t read_index; + size_t write_index; + bool in_escape; + for (read_index = 0, write_index = 0, in_escape = false; read_index < buffer_size; read_index++) + { + const uint8_t byte = buffer[read_index]; + if (byte == Slip::END) + { + continue; + } + + auto decoded_bytes = reinterpret_cast(&decoded); + + if (write_index >= sizeof(decoded)) + { + // error, too big frame + return std::nullopt; + } + + if (in_escape) + { + if (byte == Slip::ESC_END) + { + decoded_bytes[write_index++] = Slip::END; + } + else if (byte == Slip::ESC_ESC) + { + decoded_bytes[write_index++] = Slip::ESC; + } + else + { + // error, invalid byte + return std::nullopt; + } + in_escape = false; + } + else + { + if (byte == Slip::ESC) + { + in_escape = true; + } + else + { + decoded_bytes[write_index++] = byte; + } + } + } + + // CRC probably ends up inside the payload at this point; fix it + auto wrong_crc_location = reinterpret_cast(&decoded.payload[decoded.payload_length]); + decoded.crc = *wrong_crc_location; + *wrong_crc_location = 0; + + return decoded; +} + +void SerialStub::ReadAndHandleFrame(const uint8_t* const buffer, const size_t buffer_size) +{ + const auto frame = ReadFrame(buffer, buffer_size); + if (frame.has_value()) + { + HandleFrame(*frame); + } +} + +void SerialStub::HandleFrame(const WpcFrame& frame) +{ + FrameHandler::ptr handler = nullptr; + + { + std::lock_guard lock(message_handlers_mutex); + if (auto it = message_handlers.find(frame.primitive_id); it != message_handlers.cend()) + { + handler = it->second; + } + } + + if (handler) + { + const auto& responses = handler->Handle(frame); + for (const auto& response : responses) + { + AddToOutQueue(response); + } + } +} + +void SerialStub::AddFrameHandler(const uint8_t primitive_id, FrameHandler::ptr handler) +{ + std::lock_guard lock(message_handlers_mutex); + message_handlers[primitive_id] = handler; +} + +void SerialStub::ClearFrameHandlers() +{ + std::lock_guard lock(message_handlers_mutex); + message_handlers.clear(); +} + +void SerialStub::AddToOutQueue(const WpcFrame& frame) +{ + out_queue.push_back(Slip::END); + + auto frame_bytes = reinterpret_cast(&frame); + + static_assert(std::numeric_limits::max() <= sizeof(WpcFrame::payload)); + const size_t payload_end_offset = offsetof(WpcFrame, payload) + frame.payload_length; + for (size_t i = 0; i < payload_end_offset; i++) + { + EncodeAndAddToOutQueue(frame_bytes[i]); + } + + for (size_t i = offsetof(WpcFrame, crc); i < offsetof(WpcFrame, crc) + sizeof(WpcFrame::crc); i++) + { + EncodeAndAddToOutQueue(frame_bytes[i]); + } + + out_queue.push_back(Slip::END); +} + +void SerialStub::EncodeAndAddToOutQueue(const uint8_t byte) +{ + if (byte == Slip::END) + { + out_queue.push_back(Slip::ESC); + out_queue.push_back(Slip::ESC_END); + } + else if (byte == Slip::ESC) + { + out_queue.push_back(Slip::ESC); + out_queue.push_back(Slip::ESC_ESC); + } + else + { + out_queue.push_back(byte); + } +} + +std::optional SerialStub::PopOutByte() +{ + if (!out_queue.empty()) + { + const uint8_t byte = out_queue.front(); + out_queue.pop_front(); + return byte; + } + + return std::nullopt; +} + diff --git a/stubbed_tests/serialstub/serialstub/serial_stub.h b/stubbed_tests/serialstub/serialstub/serial_stub.h new file mode 100644 index 0000000..efb066e --- /dev/null +++ b/stubbed_tests/serialstub/serialstub/serial_stub.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +/* +* Thread safety notes: +* - Serial_write and the following Serial_read are always called from the same thread +* - Serial_write and Serial_read calls can be done from different threads (main thread and poll thread) +* - AddFrameHandler is locked because polling thread might be using a frame handler in the background +*/ +class SerialStub +{ +public: + void ReadAndHandleFrame(const uint8_t* const buffer, const size_t buffer_size); + + void AddFrameHandler(const uint8_t primitive_id, FrameHandler::ptr handler); + void ClearFrameHandlers(); + std::optional PopOutByte(); +private: + struct Slip + { + inline static const uint8_t END = 0xC0; + inline static const uint8_t ESC = 0xDB; + inline static const uint8_t ESC_ESC = 0xDD; + inline static const uint8_t ESC_END = 0xDC; + }; + + std::optional ReadFrame(const uint8_t* const buffer, const size_t buffer_size); + void HandleFrame(const WpcFrame& frame); + void AddToOutQueue(const WpcFrame& frame); + void EncodeAndAddToOutQueue(const uint8_t byte); + + std::mutex message_handlers_mutex; + std::unordered_map message_handlers; + std::deque out_queue; +}; + +inline SerialStub serial_stub; + diff --git a/stubbed_tests/serialstub/serialstub/wpc_frame.cpp b/stubbed_tests/serialstub/serialstub/wpc_frame.cpp new file mode 100644 index 0000000..10cb1e7 --- /dev/null +++ b/stubbed_tests/serialstub/serialstub/wpc_frame.cpp @@ -0,0 +1,28 @@ +#include "wpc_frame.h" + +#include +#include + +std::optional WpcFrame::Create(const uint8_t primitive_id, + const uint8_t frame_id, + const std::span payload) +{ + if (payload.size() > sizeof(WpcFrame::payload)) + { + return std::nullopt; + } + + WpcFrame frame { + .primitive_id = primitive_id, + .frame_id = frame_id, + .payload_length = static_cast(payload.size()), + .payload = {0}, + .crc = 0, + }; + + std::ranges::copy(payload, frame.payload.begin()); + frame.crc = Crc::FromBuffer(reinterpret_cast(&frame), + offsetof(WpcFrame, payload) + frame.payload_length); + + return frame; +} diff --git a/stubbed_tests/serialstub/serialstub/wpc_frame.h b/stubbed_tests/serialstub/serialstub/wpc_frame.h new file mode 100644 index 0000000..e426b85 --- /dev/null +++ b/stubbed_tests/serialstub/serialstub/wpc_frame.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +/** +* \brief A Dual-MCU API frame +*/ +struct [[gnu::packed]] WpcFrame +{ + uint8_t primitive_id; + uint8_t frame_id; + uint8_t payload_length; + std::array payload; + uint16_t crc; + + /** + * \brief Construct a frame calculating payload length and CRC automatically + * \return std::nullopt if payload size is too large, std::optional holding + * the created WpcFrame otherwise. + */ + static std::optional Create(const uint8_t primitive_id, + const uint8_t frame_id, + const std::span payload); +}; + diff --git a/stubbed_tests/tests/basic_tests.cpp b/stubbed_tests/tests/basic_tests.cpp new file mode 100644 index 0000000..5aea8a7 --- /dev/null +++ b/stubbed_tests/tests/basic_tests.cpp @@ -0,0 +1,106 @@ +#include +#include + +#include +extern "C" { + #include + #define LOG_MODULE_NAME (char*) "basic_tests" + #include +} + +using namespace testing; + +class StubbedTest : public testing::Test +{ +protected: + class MockFrameHandler : public FrameHandler + { + public: + MOCK_METHOD(std::vector, Handle, (const WpcFrame&), (override)); + }; + + void SetUp() override + { + Platform_set_log_level(NO_LOG_LEVEL); + WPC_initialize("", 0); + } + + void TearDown() override + { + // WPC_close takes some time + WPC_close(); + // Expectations on mock objects are done when they are deleted here + serial_stub.ClearFrameHandlers(); + } +}; + +TEST_F(StubbedTest, exampleVerifyingSentRequest) +{ + auto handler = std::make_shared(); + + const std::array TEST_KEY { + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, + 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, + }; + + std::array EXPECTED_REQUEST_PAYLOAD { + 13, // Attribute id + 00, 0x10, // Attribute length + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, + 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, + }; + + EXPECT_CALL(*handler, Handle(FieldsAre(0x0D, _, 19, EXPECTED_REQUEST_PAYLOAD, _))) + .Times(1); + serial_stub.AddFrameHandler(0x0D, handler); + + WPC_set_cipher_key(TEST_KEY.data()); +} + +TEST_F(StubbedTest, exampleVerifyingSentRequestWithSavedArgument) +{ + auto handler = std::make_shared(); + + const std::array TEST_KEY { + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, + 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, + }; + + std::array EXPECTED_REQUEST_PAYLOAD { + 0x0D, 0x00, // Attribute id + 0x10, // Attribute length + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, + 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, + }; + + WpcFrame sentFrame {}; + + EXPECT_CALL(*handler, Handle) + .Times(1) + .WillOnce(DoAll(SaveArg<0>(&sentFrame), + Return(std::vector()))); + serial_stub.AddFrameHandler(0x0D, handler); + + WPC_set_cipher_key(TEST_KEY.data()); + + EXPECT_EQ(sentFrame.primitive_id, 0x0D); + EXPECT_EQ(sentFrame.payload_length, 19); + EXPECT_EQ(sentFrame.payload, EXPECTED_REQUEST_PAYLOAD); +} + +TEST_F(StubbedTest, exampleUsingSameHandlerForMultiplePrimitives) +{ + auto handler = std::make_shared(); + + EXPECT_CALL(*handler, Handle) + .Times(2); + + // 0x1B: MSAP-SCRATCHPAD_CLEAR.request + // 0x38: MSAP-SINK_COST_WRITE.request + serial_stub.AddFrameHandler(0x1B, handler); + serial_stub.AddFrameHandler(0x38, handler); + + WPC_clear_local_scratchpad(); + WPC_set_sink_cost(10); +} + diff --git a/stubbed_tests/tests/ul_fuzz_tests.cpp b/stubbed_tests/tests/ul_fuzz_tests.cpp new file mode 100644 index 0000000..b4342fb --- /dev/null +++ b/stubbed_tests/tests/ul_fuzz_tests.cpp @@ -0,0 +1,116 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include + #define LOG_MODULE_NAME (char*) "test" + #include +} + +using namespace testing; +using namespace fuzztest; + +constexpr uint32_t max_supported_travel_time = (std::numeric_limits::max() / 1000) - 1; + +// Not completely valid, since the 2 bit qos part in qos_hop_count can be 0 +// or 1. StructOf didn't play well with bitfields so qos and hop count are not +// separated. Only check first bit for qos when doing assertions. +auto ValidSingleDataRx() { + return StructOf( + InRange(0, 0), // indication_status + Arbitrary(), // src_add + Arbitrary(), // src_endpoint + Arbitrary(), // dest_add + Arbitrary(), // dest_endpoint + Arbitrary(), // qos_hop_count + InRange(0, max_supported_travel_time), // travel_time + InRange(0, 102), // apdu_length + Arbitrary>() // apdu + ); +} + +class FuzzUlDataTest +{ +public: + static bool onDataReceived(const uint8_t* bytes, + size_t num_bytes, + app_addr_t src_addr, + app_addr_t dst_addr, + app_qos_e qos, + uint8_t src_ep, + uint8_t dst_ep, + uint32_t travel_time, + uint8_t hop_count, + unsigned long long timestamp_ms_epoch) + { + last_callback_args = { + .bytes { bytes, bytes + num_bytes }, + .src_addr = src_addr, + .dst_addr = dst_addr, + .qos = qos, + .src_ep = src_ep, + .dst_ep = dst_ep, + .travel_time = travel_time, + .hop_count = hop_count, + .timestamp_ms_epoch = timestamp_ms_epoch, + }; + callback_called.release(); + + return true; + } + + FuzzUlDataTest() + { + Platform_set_log_level(NO_LOG_LEVEL); + WPC_initialize("", 0); + + handler = std::make_shared(); + WPC_register_for_data(onDataReceived); + serial_stub.AddFrameHandler(0x04, handler); + } + + ~FuzzUlDataTest() + { + WPC_close(); + serial_stub.ClearFrameHandlers(); + } + + void TestSingleValidUlMessage(const DataRxIndication& dataRx) + { + handler->AddToSendQueue(dataRx); + + const uint32_t EXPECTED_TRAVEL_TIME = static_cast(dataRx.travel_time) * 1000 / 128; + const app_qos_e EXPECTED_QOS = static_cast(dataRx.qos_hop_count & 0b1); + const uint8_t EXPECTED_HOP_COUNT = dataRx.qos_hop_count >> 2; + + ASSERT_TRUE(callback_called.try_acquire_for(std::chrono::seconds(2))) << + "onDataReceived callback wasn't called within the timeout."; + ASSERT_TRUE(last_callback_args.has_value()); + const auto cb_args = last_callback_args.value(); + last_callback_args.reset(); + + ASSERT_EQ(cb_args.bytes.size(), dataRx.apdu_length); + ASSERT_EQ(cb_args.src_addr, dataRx.src_add); + ASSERT_EQ(cb_args.dst_addr, dataRx.dest_add); + ASSERT_EQ(cb_args.src_ep, dataRx.src_endpoint); + ASSERT_EQ(cb_args.dst_ep, dataRx.dest_endpoint); + ASSERT_EQ(cb_args.travel_time, EXPECTED_TRAVEL_TIME); + ASSERT_EQ(cb_args.hop_count, EXPECTED_HOP_COUNT); + ASSERT_EQ(cb_args.qos, EXPECTED_QOS); + } + + std::shared_ptr handler; + inline static std::binary_semaphore callback_called {0}; + inline static std::optional last_callback_args; +}; + + +FUZZ_TEST_F(FuzzUlDataTest, TestSingleValidUlMessage) + .WithDomains(ValidSingleDataRx()); + diff --git a/stubbed_tests/tests/utils/data_structures.h b/stubbed_tests/tests/utils/data_structures.h new file mode 100644 index 0000000..0c6c3ac --- /dev/null +++ b/stubbed_tests/tests/utils/data_structures.h @@ -0,0 +1,51 @@ +#pragma once + +extern "C" { + #include +} +#include +#include + +/** + * \brief DSAP DATA_RX.indication frame + */ +struct [[gnu::packed]] DataRxIndication +{ + uint8_t indication_status; + uint32_t src_add; + uint8_t src_endpoint; + uint32_t dest_add; + uint8_t dest_endpoint; + uint8_t qos_hop_count; + uint32_t travel_time; + uint8_t apdu_length; + std::array apdu; +}; + +/** + * \bried Arguments for onDataReceived_cb_f + */ +struct OnDataReceivedArgs +{ + std::vector bytes; + app_addr_t src_addr; + app_addr_t dst_addr; + app_qos_e qos; + uint8_t src_ep; + uint8_t dst_ep; + uint32_t travel_time; + uint8_t hop_count; + unsigned long long timestamp_ms_epoch; +}; + +/** + * \brief CSAP/DSAP/MSAP-ATTRIBUTE_READ.confirm frame + */ +struct [[gnu::packed]] AttributeReadConfirm +{ + uint8_t result; + uint16_t attribute_id; + uint8_t attribute_length; + std::array attribute_value; +}; + diff --git a/stubbed_tests/tests/utils/queued_ul_data_handler.cpp b/stubbed_tests/tests/utils/queued_ul_data_handler.cpp new file mode 100644 index 0000000..31326fe --- /dev/null +++ b/stubbed_tests/tests/utils/queued_ul_data_handler.cpp @@ -0,0 +1,42 @@ +#include "queued_ul_data_handler.h" + +#include +#include + +std::vector QueuedUlDataHandler::Handle(const WpcFrame& frame) +{ + std::vector responses; + + std::lock_guard lock {to_send_mutex}; + responses.emplace_back(getConfirm(frame.frame_id, !to_send.empty())); + + for (const auto& rxFrame : to_send) + { + responses.emplace_back(getDataRxInd(rxFrame, frame.frame_id)); + } + to_send.clear(); + + return responses; +} + +WpcFrame QueuedUlDataHandler::getConfirm(const uint8_t frame_id, bool hasIndication) +{ + const std::array payload {hasIndication}; + return *WpcFrame::Create(0x84, frame_id, payload); +} + +WpcFrame QueuedUlDataHandler::getDataRxInd(const DataRxIndication& rx_frame, const uint8_t frame_id) +{ + // Difference from max payload size + const auto size_difference = rx_frame.apdu.max_size() - rx_frame.apdu_length; + const std::span data {reinterpret_cast(&rx_frame), + sizeof(DataRxIndication) - size_difference}; + return *WpcFrame::Create(0x03, frame_id, data); +} + +void QueuedUlDataHandler::AddToSendQueue(const DataRxIndication& rxFrame) +{ + std::lock_guard lock {to_send_mutex}; + to_send.emplace_back(rxFrame); +} + diff --git a/stubbed_tests/tests/utils/queued_ul_data_handler.h b/stubbed_tests/tests/utils/queued_ul_data_handler.h new file mode 100644 index 0000000..fb76139 --- /dev/null +++ b/stubbed_tests/tests/utils/queued_ul_data_handler.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +/** + * \brief FrameHandler for testing uplink data callbacks + * + * It can be registered for MSAP-INDICATION_POLL.request + * Uplink data packet(s) can be added to the queue via AddToSendQueue(). + * Handle() function returns all of the data packets in the queue and clears the + * queue. + */ +class QueuedUlDataHandler : public FrameHandler +{ +public: + std::vector Handle(const WpcFrame& frame) override; + void AddToSendQueue(const DataRxIndication& rxFrame); +private: + WpcFrame getConfirm(const uint8_t frame_id, bool hasIndication); + WpcFrame getDataRxInd(const DataRxIndication& rx_frame, const uint8_t frame_id); + std::mutex to_send_mutex; + std::deque to_send; +}; + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 817db28..d5626aa 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,9 +3,16 @@ cmake_minimum_required(VERSION 3.18) project(meshAPItest LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) -set(CMAKE_BUILD_TYPE RelWithDebInfo) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE RelWithDebInfo) +endif() set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(TEST_EXTRA_CFLAGS "" CACHE STRING "Extra compiler flags for WPC library and test code. For example '--coverage'.") +set(TEST_EXTRA_LDFLAGS "" CACHE STRING "Extra linker flags for the test executable. For example '--coverage'.") +separate_arguments(TEST_EXTRA_CFLAGS) +separate_arguments(TEST_EXTRA_LDFLAGS) + add_compile_options(-Wall -Werror -Wextra) set(WPC_LIB_DIR "${CMAKE_CURRENT_LIST_DIR}/../lib/") @@ -42,3 +49,8 @@ target_link_libraries(${CMAKE_PROJECT_NAME} include(GoogleTest) gtest_discover_tests(${CMAKE_PROJECT_NAME}) +target_compile_options(wpc PRIVATE ${TEST_EXTRA_CFLAGS}) +target_compile_options(wpc_platform PRIVATE ${TEST_EXTRA_CFLAGS}) +target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE ${TEST_EXTRA_CFLAGS}) +target_link_options(${CMAKE_PROJECT_NAME} PRIVATE ${TEST_EXTRA_LDFLAGS}) + diff --git a/test/otap_files/dummy.otap b/test/otap_files/dummy.otap deleted file mode 100644 index 77e48ea..0000000 Binary files a/test/otap_files/dummy.otap and /dev/null differ diff --git a/test/scratchpad_tests.cpp b/test/scratchpad_tests.cpp index bbd8903..f1bc53c 100644 --- a/test/scratchpad_tests.cpp +++ b/test/scratchpad_tests.cpp @@ -1,5 +1,3 @@ -#include -#include #include #include "wpc_test.hpp" @@ -11,27 +9,30 @@ class WpcScratchpadTest : public WpcTest ASSERT_NO_FATAL_FAILURE(WpcTest::SetUpTestSuite()); ASSERT_NO_FATAL_FAILURE(WpcTest::StopStack()); ASSERT_EQ(APP_RES_OK, WPC_set_role(APP_ROLE_SINK)); - ASSERT_TRUE(std::filesystem::exists(OTAP_UPLOAD_FILE_PATH)); } protected: - inline static const std::filesystem::path OTAP_UPLOAD_FILE_PATH = "otap_files/dummy.otap"; - - std::vector ReadUploadFile() const - { - const auto size = std::filesystem::file_size(OTAP_UPLOAD_FILE_PATH); - std::vector buffer(size); - - if (std::ifstream ifs(OTAP_UPLOAD_FILE_PATH, std::ios_base::binary); ifs) { - if (ifs.read((char*)buffer.data(), size)) { - return buffer; - } - } - - ADD_FAILURE() << "Cannot open file " << OTAP_UPLOAD_FILE_PATH << ". " - << "Please update OTAP_UPLOAD_FILE_PATH to a valid OTAP image"; - return {}; - } + inline static const std::vector DUMMY_SCRATCHPAD = { + 0x53, 0x43, 0x52, 0x31, 0x9a, 0x93, 0x30, 0x82, 0xd9, 0xeb, 0x0a, + 0xfc, 0x31, 0x21, 0xe3, 0x37, 0xb0, 0x00, 0x00, 0x00, 0x33, 0x6c, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x53, 0x43, 0x52, 0x31, 0x9a, 0x93, 0x30, + 0x82, 0xd9, 0xeb, 0x0a, 0xfc, 0x31, 0x21, 0xe3, 0x37, 0x80, 0x00, + 0x00, 0x00, 0x0b, 0x27, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, + 0xff, 0xff, 0xff, 0xaf, 0x29, 0xd6, 0xc6, 0x09, 0xfd, 0x8f, 0x78, + 0xba, 0xb5, 0xc4, 0x2f, 0x43, 0x6d, 0xf1, 0xcd, 0xd4, 0x6a, 0x0d, + 0xa3, 0x1a, 0x8c, 0xcb, 0xd9, 0xf0, 0xf9, 0xb1, 0xdf, 0x4a, 0xef, + 0x1e, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0xae, 0xab, 0x5b, + 0xce, 0x4c, 0x4a, 0x45, 0xda, 0x77, 0xe2, 0x48, 0xd2, 0x03, 0x62, + 0x79, 0xf9, 0x98, 0xc2, 0xa1, 0x0a, 0x8e, 0x15, 0x5b, 0x21, 0x12, + 0xbe, 0x2e, 0x5e, 0x79, 0x16, 0xd4, 0x58, 0xaf, 0x83, 0x74, 0x6e, + 0x9a, 0x7c, 0x0e, 0x26, 0xcd, 0x5d, 0xf6, 0xc7, 0xa5, 0x40, 0x4e, + 0xb9, 0xc3, 0x5c, 0x02, 0x62, 0xab, 0xdb, 0x21, 0xc3, 0x7e, 0x64, + 0xe9, 0xcc, 0x95, 0xb2, 0x34, 0x5e, 0xab, 0xd0, 0x7f, 0x5e, 0xe2, + 0x26, 0x8c, 0x3f, 0x6b, 0xc7, 0xc6, 0x33, 0x04, 0x83, 0x6e + }; void UploadScratchpadAndVerify(const std::vector& buffer, const uint8_t seq_number) const { @@ -77,8 +78,7 @@ class WpcScratchpadTest : public WpcTest TEST_F(WpcScratchpadTest, testUploadAndClearScratchpad) { - const auto buffer = ReadUploadFile(); - ASSERT_NO_FATAL_FAILURE(UploadScratchpadAndVerify(buffer, 101)); + ASSERT_NO_FATAL_FAILURE(UploadScratchpadAndVerify(DUMMY_SCRATCHPAD, 101)); ASSERT_EQ(APP_RES_OK, WPC_clear_local_scratchpad()); @@ -97,7 +97,7 @@ TEST_F(WpcScratchpadTest, testUploadAndClearScratchpad) TEST_F(WpcScratchpadTest, testUploadAndDownloadScratchpadInBlocks) { - const auto upload_buffer = ReadUploadFile(); + const auto& upload_buffer = DUMMY_SCRATCHPAD; const uint32_t MAX_BLOCK_SIZE = 32; ASSERT_LT(MAX_BLOCK_SIZE, upload_buffer.size()); @@ -139,7 +139,7 @@ TEST_F(WpcScratchpadTest, testUploadAndDownloadScratchpadInBlocks) TEST_F(WpcScratchpadTest, testUploadAndDownloadScratchpad) { - auto upload_buffer = ReadUploadFile(); + const auto& upload_buffer = DUMMY_SCRATCHPAD; ASSERT_NO_FATAL_FAILURE(UploadScratchpadAndVerify(upload_buffer, 201)); std::vector download_buffer(upload_buffer.size());