Skip to content
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
14 changes: 14 additions & 0 deletions .github/.cSpellWords.txt
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,20 @@ pxhandle
pylint
pytest
pyyaml
reentrant
gcno
gcov
gcda
ebr
dylib
rpath
ggdb
fprofile
ftest
genhtml
tracefile
tracefiles
zerocounters
sddisk
sdtype
sinclude
Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,25 @@ on:
workflow_dispatch:

jobs:
# Currently no unit tests
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Dependencies
run: sudo apt-get install -y cmake build-essential lcov ruby
- name: Build unit tests
run: |
cmake -S test/unit-test -B test/unit-test/build/
make -C test/unit-test/build/ all
- name: Run unit tests
run: ctest --test-dir test/unit-test/build/ -E system --output-on-failure
- name: Coverage
run: |
make -C test/unit-test/build/ coverage
lcov --list --rc branch_coverage=1 test/unit-test/build/coverage.info

formatting:
runs-on: ubuntu-latest
steps:
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "test/unit-test/CMock"]
path = test/unit-test/CMock
url = https://github.com/ThrowTheSwitch/CMock.git
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,40 @@ If you already have FreeRTOS in your project, you may skip the fetch content by
It is recommended to use this repository as a submodule. Please refer to
[Git Tools — Submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules).

## Running the unit tests

Host-based unit tests live in [`test/unit-test`](test/unit-test) and are built
with [Unity](https://github.com/ThrowTheSwitch/Unity) and
[CMock](https://github.com/ThrowTheSwitch/CMock).

Prerequisites: CMake, a C compiler, `make`, Ruby (used by CMock and the Unity
test-runner generator), and `lcov` for coverage.

The tests rely on the CMock submodule. Initialise submodules first (the build
will also attempt to clone it automatically if missing):

```sh
git submodule update --init --recursive
```

Configure, build, and run the tests:

```sh
cmake -S test/unit-test -B test/unit-test/build/
make -C test/unit-test/build/ all
ctest --test-dir test/unit-test/build/ -E system --output-on-failure
```

Optionally generate a coverage report (writes `coverage.info`):

```sh
make -C test/unit-test/build/ coverage
lcov --list --rc branch_coverage=1 test/unit-test/build/coverage.info
```

See [`test/unit-test/README.md`](test/unit-test/README.md) for details on the
test layout and how to add new tests.

## Notes

This project is undergoing optimizations or refactoring to improve memory usage,
Expand Down
21 changes: 16 additions & 5 deletions ff_ioman.c
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,14 @@ static FF_Error_t FF_ParseExtended( FF_IOManager_t * pxIOManager,

while( xTryCount-- )
{
/* Stop following the extended-partition (EBR) chain once the
* caller's partition array is full, otherwise the loop below
* could write past pPartsFound->pxPartitions[ ffconfigMAX_PARTITIONS ]. */
if( pPartsFound->iCount >= ffconfigMAX_PARTITIONS )
{
break;
}

if( ( pxBuffer == NULL ) || ( pxBuffer->ulSector != ulThisSector ) )
{
/* Moving to a different sector. Release the
Expand Down Expand Up @@ -1027,6 +1035,14 @@ static FF_Error_t FF_ParseExtended( FF_IOManager_t * pxIOManager,
continue;
}

/* Make sure there is room to store the partition before
* writing it. The increment below must never index past the
* end of pPartsFound->pxPartitions[ ffconfigMAX_PARTITIONS ]. */
if( pPartsFound->iCount >= ffconfigMAX_PARTITIONS )
{
break;
}

{
/* Store this partition for the caller */
FF_Part_t * p = &pPartsFound->pxPartitions[ pPartsFound->iCount++ ];
Expand All @@ -1039,11 +1055,6 @@ static FF_Error_t FF_ParseExtended( FF_IOManager_t * pxIOManager,
p->bIsExtended = pdTRUE;
}

if( pPartsFound->iCount >= ffconfigMAX_PARTITIONS )
{
break;
}

xTryCount = 100;
} /* for( xPartNr = 0; xPartNr < 4; xPartNr++ ) */

Expand Down
2 changes: 2 additions & 0 deletions test/unit-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# CMake build output for the unit tests.
build/
120 changes: 120 additions & 0 deletions test/unit-test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# ------------------------------------------------------------------------------
# FreeRTOS+FAT unit tests (CMock / Unity)
#
# Host-based unit tests for the library's algorithmic code. The module under
# test (ff_ioman.c) is compiled as a "real" library and linked against CMock
# generated mocks of its dependencies (locking, FAT and CRC layers). Lightweight
# kernel type/macro stubs (under include/) stand in for the FreeRTOS kernel.
#
# Usage (matches .github/workflows/release.yml):
# cmake -S test/unit-test -B test/unit-test/build/
# make -C test/unit-test/build/ all
# ctest --test-dir test/unit-test/build/ -E system --output-on-failure
# make -C test/unit-test/build/ coverage
# ------------------------------------------------------------------------------
cmake_minimum_required( VERSION 3.13 )

project( freertos_plus_fat_utest
VERSION 1.0.0
LANGUAGES C )

# Repository root: test/unit-test/ -> test/ -> <root>
get_filename_component( MODULE_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE )
set( UNIT_TEST_DIR "${CMAKE_CURRENT_LIST_DIR}" )
set( CMOCK_DIR "${UNIT_TEST_DIR}/CMock" )

# C90 is the library's baseline; the tests/mocks need a couple of C99 niceties,
# so build everything as C99 without GNU extensions disabled.
set( CMAKE_C_STANDARD 99 )
set( CMAKE_C_STANDARD_REQUIRED ON )

enable_testing()

# ------------------------------------------------------------------------------
# CMock / Unity
# ------------------------------------------------------------------------------
include( ${MODULE_ROOT_DIR}/tools/cmock/create_test.cmake )
include( ${UNIT_TEST_DIR}/cmock_build.cmake )

if( NOT EXISTS ${CMOCK_DIR}/src/cmock.c )
clone_cmock()
endif()

add_cmock_targets()

# ------------------------------------------------------------------------------
# Shared include directories.
# - config/ : test FreeRTOSFATConfig.h
# - include/ : minimal FreeRTOS kernel type/macro stubs (FreeRTOS.h, task.h ...)
# - <root>/include : the real FreeRTOS+FAT public headers
# The kernel stubs MUST precede the real include dir so they shadow the absent
# kernel headers.
# ------------------------------------------------------------------------------
set( FAT_TEST_INCLUDE_DIRS
${UNIT_TEST_DIR}/include
${UNIT_TEST_DIR}/config
${MODULE_ROOT_DIR}/include )

set( project_name "ff_ioman" )

# ===================== Mocks ================================================
# Dependencies of ff_ioman.c that are satisfied by mocks at link time.
list( APPEND mock_list
${MODULE_ROOT_DIR}/include/ff_locking.h
${MODULE_ROOT_DIR}/include/ff_fat.h
${MODULE_ROOT_DIR}/include/ff_crc.h )

list( APPEND mock_include_list ${FAT_TEST_INCLUDE_DIRS} )
list( APPEND mock_define_list "" )

# ================== Module under test =======================================
# ff_ioman.c is the unit under test; ff_memory.c supplies the (non-inlined) byte
# accessors it calls and is compiled for real rather than mocked.
list( APPEND real_source_files
${MODULE_ROOT_DIR}/ff_ioman.c
${MODULE_ROOT_DIR}/ff_memory.c )

list( APPEND real_include_directories ${FAT_TEST_INCLUDE_DIRS} )

# ===================== Test code ============================================
list( APPEND test_include_directories ${FAT_TEST_INCLUDE_DIRS} )

# ==============================================================================
set( mock_name "${project_name}_mock" )
set( real_name "${project_name}_real" )

create_mock_list( ${mock_name}
"${mock_list}"
"${MODULE_ROOT_DIR}/tools/cmock/project.yml"
"${mock_include_list}"
"${mock_define_list}" )

create_real_library( ${real_name}
"${real_source_files}"
"${real_include_directories}"
"${mock_name}" )

list( APPEND utest_dep_list ${real_name} )

list( APPEND utest_link_list
lib${real_name}.a
-l${mock_name} )

set( utest_name "${project_name}_utest" )
set( utest_source "${UNIT_TEST_DIR}/ff_ioman_utest.c" )

create_test( ${utest_name}
${utest_source}
"${utest_link_list}"
"${utest_dep_list}"
"${test_include_directories}" )

# ------------------------------------------------------------------------------
# `coverage` target: run the tests and collect lcov data into coverage.info.
# ------------------------------------------------------------------------------
add_custom_target( coverage
COMMAND ${CMAKE_COMMAND} -DCMAKE_BINARY_DIR=${CMAKE_BINARY_DIR}
-P ${MODULE_ROOT_DIR}/tools/cmock/coverage.cmake
DEPENDS ${utest_name}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Running unit tests and collecting coverage" )
1 change: 1 addition & 0 deletions test/unit-test/CMock
Submodule CMock added at d482f5
79 changes: 79 additions & 0 deletions test/unit-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# FreeRTOS+FAT unit tests

Host-based unit tests built with [Unity] and [CMock], following the same
pattern as the FreeRTOS core libraries (for example [coreMQTT]).

The module under test (`ff_ioman.c`) is compiled as a "real" library and linked
against CMock-generated mocks of its dependencies (the locking, FAT and CRC
layers). Lightweight kernel type/macro stubs (under `include/`) stand in for the
FreeRTOS kernel, and the FreeRTOS+FAT byte accessors and buffer cache run for
real, fed by an in-memory block device.

[Unity]: https://github.com/ThrowTheSwitch/Unity
[CMock]: https://github.com/ThrowTheSwitch/CMock
[coreMQTT]: https://github.com/FreeRTOS/coreMQTT/tree/main/test/unit-test

## Layout

| Path | Purpose |
| --- | --- |
| `CMock/` | CMock submodule (vendors Unity + CException). |
| `CMakeLists.txt` | Top-level test build: sets up CMock/Unity, declares the mocks, the module under test, and the `coverage` target. |
| `cmock_build.cmake` | Clones CMock and builds the `unity` / `cmock` libraries. |
| `config/FreeRTOSFATConfig.h` | Test configuration. `ffconfigMAX_PARTITIONS` is 4 so the partition-enumeration bounds checks are reachable with a compact disk image. |
| `include/` | Minimal `FreeRTOS.h`, `task.h`, `semphr.h`, `event_groups.h` stubs (types/macros only), shadowing the absent kernel headers. |
| `ff_ioman_utest.c` | Unity tests for partition-table parsing in `ff_ioman.c`. |

Shared CMake helpers live at the repository root under
[`tools/cmock/`](../../tools/cmock): `create_test.cmake` (the
`create_mock_list` / `create_real_library` / `create_test` functions),
`project.yml` (CMock configuration) and `coverage.cmake`.

## Prerequisites

- CMake, a C compiler, and `make`
- Ruby (CMock and the Unity test-runner generator are Ruby scripts)
- `lcov` for the coverage target
- The `CMock` submodule (`git submodule update --init --recursive`). The build
will attempt to clone it automatically if missing.

## Running

```sh
cmake -S test/unit-test -B test/unit-test/build/
make -C test/unit-test/build/ all
ctest --test-dir test/unit-test/build/ -E system --output-on-failure
make -C test/unit-test/build/ coverage # writes build/coverage.info
```

These are the same steps the release workflow (`.github/workflows/release.yml`)
runs.

## What `ff_ioman_utest` covers

The suite drives the public `FF_PartitionSearch()`, which walks the extended /
logical partition (EBR) chain via the internal `FF_ParseExtended()`. It focuses
on the bounds checks that keep `FF_SPartFound_t::pxPartitions[]` from being
written past `ffconfigMAX_PARTITIONS`:

- **Overflowing chain is clamped** — a disk advertising more logical partitions
than configured stops at the limit (and does not overrun the array).
- **Chain at the limit is recorded** — exactly `ffconfigMAX_PARTITIONS` logical
partitions are all enumerated.
- **Self-referential chain terminates** — an EBR that links to itself is capped
rather than looping forever and overrunning the array.

Each test wraps `FF_SPartFound_t` in a guard structure and asserts the guard
bytes are untouched, so an out-of-bounds write is detected as a test failure.

The locking layer is mocked and ignored (`FF_PendSemaphore_Ignore()` etc.);
`FF_CreateEvents_IgnoreAndReturn( pdTRUE )` lets the I/O manager be created.

## Adding more tests

1. Add the test source and declare it in `CMakeLists.txt` via `create_test`.
2. If the unit under test pulls in new dependencies, add their headers to
`mock_list` so CMock generates mocks for them, and add any extra real
sources to `real_source_files`.
3. Write `test_*` functions using Unity assertions plus `setUp`/`tearDown`; the
test runner is generated automatically.
Loading
Loading