From b661706c377bc5029b3c7291e6214d83ff1d4763 Mon Sep 17 00:00:00 2001 From: jgromes Date: Sun, 5 Jan 2025 15:25:52 +0100 Subject: [PATCH 1/3] [CI] Add basic unit testing --- .github/workflows/unit-test.yml | 27 ++ extras/test/unit/CMakeLists.txt | 29 +++ .../test/unit/include/HardwareEmulation.hpp | 71 ++++++ extras/test/unit/include/TestHal.hpp | 237 ++++++++++++++++++ extras/test/unit/test.sh | 13 + extras/test/unit/tests/TestModule.cpp | 103 ++++++++ extras/test/unit/tests/main.cpp | 5 + 7 files changed, 485 insertions(+) create mode 100644 .github/workflows/unit-test.yml create mode 100644 extras/test/unit/CMakeLists.txt create mode 100644 extras/test/unit/include/HardwareEmulation.hpp create mode 100644 extras/test/unit/include/TestHal.hpp create mode 100755 extras/test/unit/test.sh create mode 100644 extras/test/unit/tests/TestModule.cpp create mode 100644 extras/test/unit/tests/main.cpp diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 000000000..219d6ed3b --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,27 @@ +name: "Unit test" + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + unit-test: + name: Build and run unit test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install libboost + run: | + sudo apt-get update + sudo apt-get install -y libboost-all-dev + + - name: Run unit test + run: | + cd extras/test/unit + ./test.sh diff --git a/extras/test/unit/CMakeLists.txt b/extras/test/unit/CMakeLists.txt new file mode 100644 index 000000000..a907258c1 --- /dev/null +++ b/extras/test/unit/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.13) + +project(radiolib-unittest) + +# add RadioLib sources +add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../../../../RadioLib" "${CMAKE_CURRENT_BINARY_DIR}/RadioLib") + +# add test sources +file(GLOB_RECURSE TEST_SOURCES + "tests/main.cpp" + "tests/TestModule.cpp" +) + +# create the executable +add_executable(${PROJECT_NAME} ${TEST_SOURCES}) + +# include directories +target_include_directories(${PROJECT_NAME} PUBLIC include) + +# link RadioLib +target_link_libraries(${PROJECT_NAME} RadioLib fmt) + +# set target properties and options +set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 20) +target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra) + +# set RadioLib debug +#target_compile_definitions(RadioLib PUBLIC RADIOLIB_DEBUG_BASIC RADIOLIB_DEBUG_SPI) +#target_compile_definitions(RadioLib PUBLIC RADIOLIB_DEBUG_PORT=stdout) diff --git a/extras/test/unit/include/HardwareEmulation.hpp b/extras/test/unit/include/HardwareEmulation.hpp new file mode 100644 index 000000000..c2b324a84 --- /dev/null +++ b/extras/test/unit/include/HardwareEmulation.hpp @@ -0,0 +1,71 @@ +#ifndef HARDWARE_EMULATION_HPP +#define HARDWARE_EMULATION_HPP + +#include + +// value that is returned by the emualted radio class when performing SPI transfer to it +#define EMULATED_RADIO_SPI_RETURN (0xFF) + +// pin indexes +#define EMULATED_RADIO_NSS_PIN (1) +#define EMULATED_RADIO_IRQ_PIN (2) +#define EMULATED_RADIO_RST_PIN (3) +#define EMULATED_RADIO_GPIO_PIN (4) + +enum PinFunction_t { + PIN_UNASSIGNED = 0, + PIN_CS, + PIN_IRQ, + PIN_RST, + PIN_GPIO, +}; + +// structure for emulating GPIO pins +struct EmulatedPin_t { + uint32_t mode; + uint32_t value; + bool event; + PinFunction_t func; +}; + +// structure for emulating SPI registers +struct EmulatedRegister_t { + uint8_t value; + uint8_t readOnlyBitFlags; + bool bufferAccess; +}; + +// base class for emulated radio modules (SX126x etc.) +class EmulatedRadio { + public: + void connect(EmulatedPin_t* csPin, EmulatedPin_t* irqPin, EmulatedPin_t* rstPin, EmulatedPin_t* gpioPin) { + this->cs = csPin; + this->cs->func = PIN_CS; + this->irq = irqPin; + this->irq->func = PIN_IRQ; + this->rst = rstPin; + this->rst->func = PIN_RST; + this->gpio = gpioPin; + this->gpio->func = PIN_GPIO; + } + + virtual uint8_t HandleSPI(uint8_t b) { + (void)b; + // handle the SPI input and generate output here + return(EMULATED_RADIO_SPI_RETURN); + } + + virtual void HandleGPIO() { + // handle discrete GPIO signals here (e.g. reset state machine on NSS falling edge) + } + + protected: + // pointers to emulated GPIO pins + // this is done via pointers so that the same GPIO entity is shared, like with a real hardware + EmulatedPin_t* cs; + EmulatedPin_t* irq; + EmulatedPin_t* rst; + EmulatedPin_t* gpio; +}; + +#endif diff --git a/extras/test/unit/include/TestHal.hpp b/extras/test/unit/include/TestHal.hpp new file mode 100644 index 000000000..cf1faff96 --- /dev/null +++ b/extras/test/unit/include/TestHal.hpp @@ -0,0 +1,237 @@ +#ifndef TEST_HAL_HPP +#define TEST_HAL_HPP + +#include +#include +#include + +#include + +#include +#include + +#if defined(TEST_HAL_LOG) +#define HAL_LOG(...) BOOST_TEST_MESSAGE(__VA_ARGS__) +#else +#define HAL_LOG(...) {} +#endif + +#include "HardwareEmulation.hpp" + +#define TEST_HAL_INPUT (0) +#define TEST_HAL_OUTPUT (1) +#define TEST_HAL_LOW (0) +#define TEST_HAL_HIGH (1) +#define TEST_HAL_RISING (0) +#define TEST_HAL_FALLING (1) + +// number of emulated GPIO pins +#define TEST_HAL_NUM_GPIO_PINS (32) + +#define TEST_HAL_SPI_LOG_LENGTH (512) + +class TestHal : public RadioLibHal { + public: + TestHal() : RadioLibHal(TEST_HAL_INPUT, TEST_HAL_OUTPUT, TEST_HAL_LOW, TEST_HAL_HIGH, TEST_HAL_RISING, TEST_HAL_FALLING) { } + + void init() override { + HAL_LOG("TestHal::init()"); + + // save program start timestamp + start = std::chrono::high_resolution_clock::now(); + + // init emulated GPIO + for(int i = 0; i < TEST_HAL_NUM_GPIO_PINS; i++) { + this->gpio[i].mode = 0; + this->gpio[i].value = 0; + this->gpio[i].event = false; + this->gpio[i].func = PIN_UNASSIGNED; + } + } + + void term() override { + HAL_LOG("TestHal::term()"); + } + + void pinMode(uint32_t pin, uint32_t mode) override { + HAL_LOG("TestHal::pinMode(pin=" << pin << ", mode=" << mode << " [" << ((mode == TEST_HAL_INPUT) ? "INPUT" : "OUTPUT") << "])"); + + // check the range + BOOST_ASSERT_MSG(pin < TEST_HAL_NUM_GPIO_PINS, "Pin number out of range"); + + // check known modes + BOOST_ASSERT_MSG(((mode == TEST_HAL_INPUT) || (mode == TEST_HAL_OUTPUT)), "Invalid pin mode"); + + // set mode + this->gpio[pin].mode = mode; + } + + void digitalWrite(uint32_t pin, uint32_t value) override { + HAL_LOG("TestHal::digitalWrite(pin=" << pin << ", value=" << value << " [" << ((value == TEST_HAL_LOW) ? "LOW" : "HIGH") << "])"); + + // check the range + BOOST_ASSERT_MSG(pin < TEST_HAL_NUM_GPIO_PINS, "Pin number out of range"); + + // check it is output + BOOST_ASSERT_MSG(this->gpio[pin].mode == TEST_HAL_OUTPUT, "GPIO is not output!"); + + // check known values + BOOST_ASSERT_MSG(((value == TEST_HAL_LOW) || (value == TEST_HAL_HIGH)), "Invalid output value"); + + // set value + this->gpio[pin].value = value; + this->gpio[pin].event = true; + if(radio) { + this->radio->HandleGPIO(); + } + this->gpio[pin].event = false; + } + + uint32_t digitalRead(uint32_t pin) override { + HAL_LOG("TestHal::digitalRead(pin=" << pin << ")"); + + // check the range + BOOST_ASSERT_MSG(pin < TEST_HAL_NUM_GPIO_PINS, "Pin number out of range"); + + // check it is input + BOOST_ASSERT_MSG(this->gpio[pin].mode == TEST_HAL_INPUT, "GPIO is not input"); + + // read the value + uint32_t value = this->gpio[pin].value; + HAL_LOG("TestHal::digitalRead(pin=" << pin << ")=" << value << " [" << ((value == TEST_HAL_LOW) ? "LOW" : "HIGH") << "]"); + return(value); + } + + void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override { + HAL_LOG("TestHal::attachInterrupt(interruptNum=" << interruptNum << ", interruptCb=" << interruptCb << ", mode=" << mode << ")"); + } + + void detachInterrupt(uint32_t interruptNum) override { + HAL_LOG("TestHal::detachInterrupt(interruptNum=" << interruptNum << ")"); + } + + void delay(unsigned long ms) override { + HAL_LOG("TestHal::delay(ms=" << ms << ")"); + const auto start = std::chrono::high_resolution_clock::now(); + + // sleep_for is sufficient for ms-precision sleep + std::this_thread::sleep_for(std::chrono::duration(ms)); + + // measure and print + const auto end = std::chrono::high_resolution_clock::now(); + const std::chrono::duration elapsed = end - start; + HAL_LOG("TestHal::delay(ms=" << ms << ")=" << elapsed.count() << "ms"); + } + + void delayMicroseconds(unsigned long us) override { + HAL_LOG("TestHal::delayMicroseconds(us=" << us << ")"); + const auto start = std::chrono::high_resolution_clock::now(); + + // busy wait is needed for microseconds precision + const auto len = std::chrono::microseconds(us); + while(std::chrono::high_resolution_clock::now() - start < len); + + // measure and print + const auto end = std::chrono::high_resolution_clock::now(); + const std::chrono::duration elapsed = end - start; + HAL_LOG("TestHal::delayMicroseconds(us=" << us << ")=" << elapsed.count() << "us"); + } + + void yield() override { + HAL_LOG("TestHal::yield()"); + } + + unsigned long millis() override { + HAL_LOG("TestHal::millis()"); + std::chrono::time_point now = std::chrono::high_resolution_clock::now(); + auto res = std::chrono::duration_cast(now - this->start); + HAL_LOG("TestHal::millis()=" << res.count()); + return(res.count()); + } + + unsigned long micros() override { + HAL_LOG("TestHal::micros()"); + std::chrono::time_point now = std::chrono::high_resolution_clock::now(); + auto res = std::chrono::duration_cast(now - this->start); + HAL_LOG("TestHal::micros()=" << res.count()); + return(res.count()); + } + + long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override { + HAL_LOG("TestHal::pulseIn(pin=" << pin << ", state=" << state << ", timeout=" << timeout << ")"); + return(0); + } + + void spiBegin() { + HAL_LOG("TestHal::spiBegin()"); + } + + void spiBeginTransaction() { + HAL_LOG("TestHal::spiBeginTransaction()"); + + // wipe history log + memset(this->spiLog, 0x00, TEST_HAL_SPI_LOG_LENGTH); + this->spiLogPtr = this->spiLog; + } + + void spiTransfer(uint8_t* out, size_t len, uint8_t* in) { + HAL_LOG("TestHal::spiTransfer(len=" << len << ")"); + + for(size_t i = 0; i < len; i++) { + // append to log + (*this->spiLogPtr++) = out[i]; + + // process the SPI byte + in[i] = this->radio->HandleSPI(out[i]); + + // outpu debug + HAL_LOG(fmt::format("out={:#02x}, in={:#02x}", out[i], in[i])); + } + } + + void spiEndTransaction() { + HAL_LOG("TestHal::spiEndTransaction()"); + } + + void spiEnd() { + HAL_LOG("TestHal::spiEnd()"); + } + + void tone(uint32_t pin, unsigned int frequency, unsigned long duration = 0) { + HAL_LOG("TestHal::tone(pin=" << pin << ", frequency=" << frequency << ", duration=" << duration << ")"); + } + + void noTone(uint32_t pin) { + HAL_LOG("TestHal::noTone(pin=" << pin << ")"); + } + + // method to compare buffer to the internal SPI log, for verifying SPI transactions + int spiLogMemcmp(const void* in, size_t n) { + return(memcmp(this->spiLog, in, n)); + } + + // method that "connects" the emualted radio hardware to this HAL + void connectRadio(EmulatedRadio* r) { + this->radio = r; + this->radio->connect(&this->gpio[EMULATED_RADIO_NSS_PIN], + &this->gpio[EMULATED_RADIO_IRQ_PIN], + &this->gpio[EMULATED_RADIO_RST_PIN], + &this->gpio[EMULATED_RADIO_GPIO_PIN]); + } + + private: + // array of emulated GPIO pins + EmulatedPin_t gpio[TEST_HAL_NUM_GPIO_PINS]; + + // start time point + std::chrono::time_point start; + + // emulated radio hardware + EmulatedRadio* radio; + + // SPI history log + uint8_t spiLog[TEST_HAL_SPI_LOG_LENGTH]; + uint8_t* spiLogPtr; +}; + +#endif diff --git a/extras/test/unit/test.sh b/extras/test/unit/test.sh new file mode 100755 index 000000000..369690adf --- /dev/null +++ b/extras/test/unit/test.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +# build the test binary +mkdir -p build +cd build +cmake -G "CodeBlocks - Unix Makefiles" .. +make -j4 + +# run it +cd .. +./build/radiolib-unittest --log_level=message diff --git a/extras/test/unit/tests/TestModule.cpp b/extras/test/unit/tests/TestModule.cpp new file mode 100644 index 000000000..f2815d6bc --- /dev/null +++ b/extras/test/unit/tests/TestModule.cpp @@ -0,0 +1,103 @@ +// boost test header +#include + +// mock HAL +#include "TestHal.hpp" + +// testing fixture +struct ModuleFixture { + TestHal* hal = nullptr; + Module* mod = nullptr; + EmulatedRadio* radioHardware = nullptr; + + ModuleFixture() { + BOOST_TEST_MESSAGE("--- Module fixture setup ---"); + hal = new TestHal(); + radioHardware = new EmulatedRadio(); + hal->connectRadio(radioHardware); + + mod = new Module(hal, EMULATED_RADIO_NSS_PIN, EMULATED_RADIO_IRQ_PIN, EMULATED_RADIO_RST_PIN, EMULATED_RADIO_GPIO_PIN); + mod->init(); + } + + ~ModuleFixture() { + BOOST_TEST_MESSAGE("--- Module fixture teardown ---"); + mod->term(); + delete[] mod; + delete[] hal; + } +}; + +BOOST_FIXTURE_TEST_SUITE(suite_Module, ModuleFixture) + + BOOST_FIXTURE_TEST_CASE(Module_SPIgetRegValue_reg, ModuleFixture) + { + BOOST_TEST_MESSAGE("--- Test Module::SPIgetRegValue register access ---"); + int16_t ret; + + // basic register read with default config + const uint8_t address = 0x12; + const uint8_t spiTxn[] = { address, 0x00 }; + ret = mod->SPIgetRegValue(address); + + // check return code, value and history log + BOOST_TEST(ret >= RADIOLIB_ERR_NONE); + BOOST_TEST(ret == EMULATED_RADIO_SPI_RETURN); + BOOST_TEST(hal->spiLogMemcmp(spiTxn, sizeof(spiTxn)) == 0); + + // register read masking test + const uint8_t msb = 5; + const uint8_t lsb = 1; + ret = mod->SPIgetRegValue(address, msb, lsb); + BOOST_TEST(ret == 0x3E); + + // invalid mask tests (swapped MSB and LSB, out of range bit masks) + ret = mod->SPIgetRegValue(address, lsb, msb); + BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE); + ret = mod->SPIgetRegValue(address, 10, lsb); + BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE); + ret = mod->SPIgetRegValue(address, msb, 10); + BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE); + } + + BOOST_FIXTURE_TEST_CASE(Module_SPIgetRegValue_stream, ModuleFixture) + { + BOOST_TEST_MESSAGE("--- Test Module::SPIgetRegValue stream access ---"); + int16_t ret; + + // change settings to stream type + mod->spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_ADDR] = Module::BITS_16; + mod->spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_CMD] = Module::BITS_8; + mod->spiConfig.statusPos = 1; + mod->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_READ] = RADIOLIB_SX126X_CMD_READ_REGISTER; + mod->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_WRITE] = RADIOLIB_SX126X_CMD_WRITE_REGISTER; + mod->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_NOP] = RADIOLIB_SX126X_CMD_NOP; + mod->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_STATUS] = RADIOLIB_SX126X_CMD_GET_STATUS; + mod->spiConfig.stream = true; + + // basic register read + const uint8_t address = 0x12; + const uint8_t spiTxn[] = { RADIOLIB_SX126X_CMD_READ_REGISTER, 0x00, address, 0x00, 0x00 }; + ret = mod->SPIgetRegValue(address); + + // check return code, value and history log + BOOST_TEST(ret >= RADIOLIB_ERR_NONE); + BOOST_TEST(ret == EMULATED_RADIO_SPI_RETURN); + BOOST_TEST(hal->spiLogMemcmp(spiTxn, sizeof(spiTxn)) == 0); + + // register read masking test + const uint8_t msb = 5; + const uint8_t lsb = 1; + ret = mod->SPIgetRegValue(address, msb, lsb); + BOOST_TEST(ret == 0x3E); + + // invalid mask tests (swapped MSB and LSB, out of range bit masks) + ret = mod->SPIgetRegValue(address, lsb, msb); + BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE); + ret = mod->SPIgetRegValue(address, 10, lsb); + BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE); + ret = mod->SPIgetRegValue(address, msb, 10); + BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE); + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/extras/test/unit/tests/main.cpp b/extras/test/unit/tests/main.cpp new file mode 100644 index 000000000..be4b6efd8 --- /dev/null +++ b/extras/test/unit/tests/main.cpp @@ -0,0 +1,5 @@ +#define BOOST_TEST_MODULE "RadioLib Unit test" +#include + +// intentionally left blank, boost.test creates its own entrypoint + From c23287c71d9606ec6ceba564205daac96f4ba0fb Mon Sep 17 00:00:00 2001 From: jgromes Date: Sun, 5 Jan 2025 15:26:21 +0100 Subject: [PATCH 2/3] [CI] Add gitignore --- extras/test/unit/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 extras/test/unit/.gitignore diff --git a/extras/test/unit/.gitignore b/extras/test/unit/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/extras/test/unit/.gitignore @@ -0,0 +1 @@ +build/ From a27d565bc2ded0378a8b2ca95ccb26c16a809226 Mon Sep 17 00:00:00 2001 From: jgromes Date: Sun, 5 Jan 2025 15:31:24 +0100 Subject: [PATCH 3/3] [CI] Install libfmt --- .github/workflows/unit-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 219d6ed3b..58a7d7225 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -16,10 +16,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install libboost + - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libboost-all-dev + sudo apt-get install -y libboost-all-dev libfmt-dev - name: Run unit test run: |