Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CI] Unit test #1373

Merged
merged 3 commits into from
Jan 5, 2025
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
27 changes: 27 additions & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
@@ -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 dependencies
run: |
sudo apt-get update
sudo apt-get install -y libboost-all-dev libfmt-dev

- name: Run unit test
run: |
cd extras/test/unit
./test.sh
1 change: 1 addition & 0 deletions extras/test/unit/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
29 changes: 29 additions & 0 deletions extras/test/unit/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
71 changes: 71 additions & 0 deletions extras/test/unit/include/HardwareEmulation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#ifndef HARDWARE_EMULATION_HPP
#define HARDWARE_EMULATION_HPP

#include <stdint.h>

// 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
237 changes: 237 additions & 0 deletions extras/test/unit/include/TestHal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#ifndef TEST_HAL_HPP
#define TEST_HAL_HPP

#include <chrono>
#include <thread>
#include <fmt/format.h>

#include <RadioLib.h>

#include <boost/log/trivial.hpp>
#include <boost/format.hpp>

#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<unsigned long, std::milli>(ms));

// measure and print
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration<double, std::milli> 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<double, std::micro> 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<std::chrono::milliseconds>(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<std::chrono::microseconds>(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<std::chrono::high_resolution_clock> start;

// emulated radio hardware
EmulatedRadio* radio;

// SPI history log
uint8_t spiLog[TEST_HAL_SPI_LOG_LENGTH];
uint8_t* spiLogPtr;
};

#endif
13 changes: 13 additions & 0 deletions extras/test/unit/test.sh
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading