Skip to content

Commit

Permalink
tests: use cmake to run unit tests
Browse files Browse the repository at this point in the history
this requires us to switch our mocking support framework from CMock to FFF, since the former also requires Ruby
  • Loading branch information
prdktntwcklr committed Aug 14, 2024
1 parent abfc306 commit 716481a
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 64 deletions.
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"vscode": {
"extensions": [
"ms-vscode.cpptools",
"numaru.vscode-ceedling-test-adapter"
"twxs.cmake",
"ms-vscode.cmake-tools"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "bash"
Expand Down
27 changes: 21 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
cmake_minimum_required(VERSION 3.20)

set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/toolchain.cmake)
message(STATUS "CMAKE_TOOLCHAIN_FILE is: ${CMAKE_TOOLCHAIN_FILE}")
set(TARGET_GROUP prod CACHE STRING "Target to build for")

if(TARGET_GROUP STREQUAL prod)
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/toolchain.cmake)
message(STATUS "CMAKE_TOOLCHAIN_FILE is: ${CMAKE_TOOLCHAIN_FILE}")
elseif(TARGET_GROUP STREQUAL test)
# do nothing here
else()
message(FATAL_ERROR "Unknown TARGET_GROUP: \"${TARGET_GROUP}\" (can be \"prod\" or \"test\")")
endif()

message(STATUS "TARGET_GROUP is: ${TARGET_GROUP}")

# export compile commands for clang-tidy
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
Expand All @@ -15,8 +25,13 @@ set(CMAKE_C_EXTENSIONS OFF)

message(STATUS "CMAKE_C_COMPILER is: ${CMAKE_C_COMPILER}")

# set serial port, works under Windows only
set(PORT_NUMBER COM6)
message(STATUS "Serial port set to: ${PORT_NUMBER}")
if(TARGET_GROUP STREQUAL prod)
# set serial port, works under Windows only
set(PORT_NUMBER COM6)
message(STATUS "Serial port set to: ${PORT_NUMBER}")

add_subdirectory(Src Bin)
add_subdirectory(Src Bin)
elseif(TARGET_GROUP STREQUAL test)
enable_testing()
add_subdirectory(Tests)
endif()
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ RUN ln -snf /usr/share/zoneinfo/$CONTAINER_TIMEZONE /etc/localtime && \
RUN apt-get update && \
xargs -a packages.txt apt-get install -y && \
pip install --no-cache-dir pre-commit && \
gem install ceedling && \
apt-get autoremove -y && \
apt-get clean

Expand Down
Binary file removed Docs/Img/run-unit-tests.gif
Binary file not shown.
Binary file added Docs/run-unit-tests.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 14 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
![Build](https://github.com/prdktntwcklr/atmega-tdd-example/workflows/build/badge.svg)

This project demonstrates the use of unit testing and test-driven development
(TDD) when writing bare-metal code for an ATmega series microcontroller.

Ceedling ([ThrowTheSwitch/Ceedling](https://github.com/ThrowTheSwitch/Ceedling))
is used as the unit testing framework, but the codebase can easily be adapted
to accommodate other testing frameworks such as CppUTest or Google Test.
(TDD) when writing bare-metal code for an ATmega series microcontroller. This
project uses the [ThrowTheSwitch/Unity](https://github.com/ThrowTheSwitch/Unity)
unit testing framework together with [meekrosoft/fff](https://github.com/meekrosoft/fff)
for mocking support in order to minimize the reliance on external dependencies
such as `Ruby`.

In this example, the unit tests are run exclusively on the developer's machine
(the "host") instead of on the microcontroller itself (the "target"). To
Expand All @@ -26,7 +26,7 @@ described below (see [running unit tests](#running-unit-tests)).
Details on how to connect these components can be found in
```Docs/schematic.pdf```.

## Running unit tests manually
## Running unit tests

This project uses a Dockerized environment from within which the unit tests can
be executed. This saves developers from having to set up the development
Expand All @@ -39,23 +39,20 @@ extension for [Visual Studio Code](https://code.visualstudio.com/).

Opening the workspace in Visual Studio Code should then prompt you with a
textbox that allows you to reopen the project in a Docker container with all
required packages already installed. From within the ```Tests/```
directory, you can then simply run:
required packages already installed. You can then simply use `CMake` to first
build for the `test` group and then use `CTest` to invoke the tests:

```bash
ceedling test:all
mkdir -p Build
cd Build
cmake -DTARGET_GROUP=test ..
cmake --build .
ctest Tests/ -V
```

All tests should pass successfully:

![All unit tests ran successfully.](Docs/Img/run-unit-tests.gif)

## VS Code Extension

As an alternative, you can also use run the unit tests directly from
within VS Code by using the [Ceedling Test Explorer](https://marketplace.visualstudio.com/items?itemName=numaru.vscode-ceedling-test-adapter) plugin. The plugin not only provides a convenient
interface to launch unit tests for individual models from, but also adds
integration of the GDB debugger.
![All unit tests ran successfully.](Docs/run-unit-tests.jpg)

## Further reading

Expand Down
39 changes: 39 additions & 0 deletions Tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
include(FetchContent)

FetchContent_Declare(
unity
GIT_REPOSITORY https://github.com/ThrowTheSwitch/Unity.git
GIT_TAG v2.6.0
)

FetchContent_Declare(
fff
GIT_REPOSITORY https://github.com/meekrosoft/fff.git
GIT_TAG v1.1
)

FetchContent_MakeAvailable(unity fff)

add_executable(test_led test_led.c ${CMAKE_SOURCE_DIR}/Src/led.c)
target_compile_definitions(test_led PUBLIC TEST)
target_include_directories(test_led PRIVATE ${CMAKE_SOURCE_DIR}/Inc support)
target_link_libraries(test_led unity)
add_test(test_led test_led)

add_executable(test_main test_main.c ${CMAKE_SOURCE_DIR}/Src/main.c)
target_compile_definitions(test_main PUBLIC TEST)
target_include_directories(test_main PRIVATE ${CMAKE_SOURCE_DIR}/Inc support ${CMAKE_BINARY_DIR}/_deps/fff-src)
target_link_libraries(test_main unity)
add_test(test_main test_main)

add_executable(test_timer test_timer.c ${CMAKE_SOURCE_DIR}/Src/timer.c)
target_compile_definitions(test_timer PUBLIC TEST)
target_include_directories(test_timer PRIVATE ${CMAKE_SOURCE_DIR}/Inc support)
target_link_libraries(test_timer unity)
add_test(test_timer test_timer)

add_executable(test_superloop test_superloop.c ${CMAKE_SOURCE_DIR}/Src/superloop.c)
target_compile_definitions(test_superloop PUBLIC TEST)
target_include_directories(test_superloop PRIVATE ${CMAKE_SOURCE_DIR}/Inc support ${CMAKE_BINARY_DIR}/_deps/fff-src)
target_link_libraries(test_superloop unity)
add_test(test_superloop test_superloop)
10 changes: 10 additions & 0 deletions Tests/test_led.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,14 @@ void test_led_shouldReportOnOffStateCorrectly(void)
TEST_ASSERT_TRUE(led_is_off());
}

int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_led_ledInitShouldSetCorrectLedAsOutputAndTurnLedOff);
RUN_TEST(test_led_shouldTurnOnAndOffCorrectly);
RUN_TEST(test_led_ledToggleShouldToggleLedCorrectly);
RUN_TEST(test_led_shouldReportOnOffStateCorrectly);
return UNITY_END();
}

#endif // TEST
13 changes: 0 additions & 13 deletions Tests/test_low_power.c

This file was deleted.

39 changes: 28 additions & 11 deletions Tests/test_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,44 @@

#ifdef TEST

#include <stdbool.h>

#include "unity.h"
#include "fff.h"

DEFINE_FFF_GLOBALS;

FAKE_VOID_FUNC(superloop_init);
FAKE_VOID_FUNC(low_power_init);
FAKE_VOID_FUNC(low_power_enter);
FAKE_VALUE_FUNC(bool, superloop_run);

#include "main.h"
#include "mock_low_power.h"
#include "mock_superloop.h"

void setUp(void) {}
void setUp(void)
{
RESET_FAKE(superloop_init);
RESET_FAKE(low_power_init);
RESET_FAKE(low_power_enter);
RESET_FAKE(superloop_run);
}

void tearDown(void) {}

void test_main_should_initPeripheralsThenRunMainLoop(void)
{
superloop_init_Expect();
low_power_init_Expect();
low_power_enter_Ignore();

superloop_run_ExpectAndReturn(true);
superloop_run_ExpectAndReturn(true);
superloop_run_ExpectAndReturn(false);
superloop_run_fake.return_val = false;
testable_main();
TEST_ASSERT_EQUAL(1, superloop_init_fake.call_count);
TEST_ASSERT_EQUAL(1, low_power_init_fake.call_count);
TEST_ASSERT_EQUAL(1, superloop_run_fake.call_count);
}

TEST_ASSERT_EQUAL(0, testable_main());
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_main_should_initPeripheralsThenRunMainLoop);
return UNITY_END();
}

#endif // TEST
50 changes: 40 additions & 10 deletions Tests/test_superloop.c
Original file line number Diff line number Diff line change
@@ -1,34 +1,64 @@
/* test_superloop.c */

#ifdef TEST

#include <stdbool.h>
#include <stdint.h>

#include "unity.h"
#include "fff.h"

DEFINE_FFF_GLOBALS;

FAKE_VOID_FUNC(led_init);
FAKE_VOID_FUNC(timer_init);
FAKE_VALUE_FUNC(bool, timer_deadline_reached);
FAKE_VOID_FUNC(led_toggle);

#include "superloop.h"
#include "mock_led.h"
#include "mock_timer.h"

void setUp(void) {}
void setUp(void)
{
RESET_FAKE(led_init);
RESET_FAKE(timer_init);
RESET_FAKE(timer_deadline_reached);
RESET_FAKE(led_toggle);
}

void tearDown(void) {}

void test_superloop_init_should_initializePeripherals(void)
{
led_init_Expect();
timer_init_Expect();

superloop_init();
TEST_ASSERT_EQUAL(1, led_init_fake.call_count);
TEST_ASSERT_EQUAL(1, timer_init_fake.call_count);
}

void test_superloop_run_should_toggleLedIfDeadlineReached(void)
{
timer_deadline_reached_IgnoreAndReturn(false);
timer_deadline_reached_fake.return_val = true;

superloop_run();

timer_deadline_reached_IgnoreAndReturn(true);
timer_get_stamp_IgnoreAndReturn(0);
led_toggle_Expect();
TEST_ASSERT_EQUAL(1, led_toggle_fake.call_count);
}

void test_superloop_run_should_notToggleLedIfDeadlineNotYetReached(void)
{
timer_deadline_reached_fake.return_val = false;

superloop_run();

TEST_ASSERT_EQUAL(0, led_toggle_fake.call_count);
}

int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_superloop_init_should_initializePeripherals);
RUN_TEST(test_superloop_run_should_toggleLedIfDeadlineReached);
RUN_TEST(test_superloop_run_should_notToggleLedIfDeadlineNotYetReached);
return UNITY_END();
}

#endif // TEST
13 changes: 11 additions & 2 deletions Tests/test_timer.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

#include "testable_mcu_registers.h"
#include "timer.h"
#include "mock_led.h"

void setUp(void)
{
Expand Down Expand Up @@ -46,7 +45,7 @@ void test_timer_set_stamp_should_setTimeStampCorrectly(void)
TEST_ASSERT_EQUAL(570, timer_get_stamp());
}

void test_timer_deadline_reached_should_returnReachedDeadlinesCorrectly(void)
void test_timer_deadline_reached_should_returnDeadlinesCorrectly(void)
{
timer_set_stamp(200);
TEST_ASSERT_FALSE(timer_deadline_reached(250));
Expand All @@ -55,4 +54,14 @@ void test_timer_deadline_reached_should_returnReachedDeadlinesCorrectly(void)
TEST_ASSERT_TRUE(timer_deadline_reached(250));
}

int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_timer_init_should_setUpRegistersCorrectly);
RUN_TEST(test_timer_overflow_isr_should_incrementTimeStampBy10);
RUN_TEST(test_timer_set_stamp_should_setTimeStampCorrectly);
RUN_TEST(test_timer_deadline_reached_should_returnDeadlinesCorrectly);
return UNITY_END();
}

#endif // TEST
3 changes: 1 addition & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ services:
- ./:/app
unit-tests:
image: atmega-tdd
command: bash -c "cd Tests && ceedling version && ceedling clobber && \
ceedling test:all"
command: bash -c "mkdir -p Build && cd Build && cmake -DTARGET_GROUP=test .. && cmake --build . && ctest Tests/ -V"
depends_on:
- build-image
volumes:
Expand Down
1 change: 0 additions & 1 deletion packages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ make
nano
python3
python3-pip
ruby

0 comments on commit 716481a

Please sign in to comment.