Skip to content

Added a regression suite for the FreeRTOS compatibility layer - #583

Merged
fdesbiens merged 2 commits into
eclipse-threadx:devfrom
fdesbiens:feature/freertos-layer-tests
Aug 9, 2026
Merged

Added a regression suite for the FreeRTOS compatibility layer#583
fdesbiens merged 2 commits into
eclipse-threadx:devfrom
fdesbiens:feature/freertos-layer-tests

Conversation

@fdesbiens

@fdesbiens fdesbiens commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Gives the FreeRTOS compatibility layer its first regression coverage in this repository.

Depends on #582. This branch is stacked on the #570 fix, which appears here as the first of two commits, because the queue tests assert the behaviour that PR corrects. Merge #582 first and I will rebase.

Why this suite exists

Every creation function in the layer takes one or two byte pool allocations for its bookkeeping and then creates one or more ThreadX kernel objects. When a kernel object cannot be created, the function returns NULL, or pdFAIL for xTaskCreate(). The caller has no handle, so it cannot call the matching delete function, and whatever the layer failed to release is gone until the system restarts.

That makes these paths invisible from the outside: a leaking version and a correct version return exactly the same thing to the caller. It is how the leak in #570 survived, and inspection is the only thing standing between us and the next one.

So the suite does not test return values alone. It counts the ThreadX primitives the layer reaches for, and checks that each error path gives back precisely what it took.

What it covers

Test Covers
txfr_queue_create_test xQueueCreate, xQueueCreateStatic, vQueueDelete, including the two paths fixed in #582
txfr_task_create_test xTaskCreate semaphore and thread failure paths, xTaskCreateStatic
txfr_sync_create_test counting and binary semaphores, mutexes, recursive mutexes, event groups, timers

25 checks across three ctest cases. This is deliberately a first course rather than full coverage of a 2,800 line layer: the infrastructure is the hard part, and coverage can now grow a test at a time.

How the injection works

A test asks the harness to fail a chosen kernel creation call, then reads back the counts. Interception uses the linker's --wrap, so tx_freertos.c is compiled exactly as it ships, with no test hooks in product code.

Two traps worth knowing, both documented in test/freertos/readme.md:

  • tx_api.h maps the public API onto the error checking entry points, so the symbols that exist at link time are the _txe_ variants. Wrapping tx_semaphore_create does nothing. Worse, --wrap on a name that does not resolve is silently ignored, so a typo produces a test that quietly never injects anything and passes for the wrong reason.
  • txfr_malloc() and txfr_free() cannot be wrapped at all, since they are defined in tx_freertos.c and called from within it, so the compiler resolves those calls internally. The byte pool counts stand in for them.

Validation

A test suite that has only ever seen correct code proves nothing, so I built it against the layer as it stands on dev, without the #582 fix. Exactly the two expected checks fail, with the leaked counts:

read_sem failure releases both allocations           FAIL
                                                       got      alloc=2 release=0 create=1 delete=0
                                                       expected alloc=2 release=2 create=1 delete=0
write_sem failure unwinds read_sem and memory        FAIL
                                                       got      alloc=2 release=0 create=2 delete=0
                                                       expected alloc=2 release=2 create=2 delete=1

Against the fixed layer, all three tests pass. Both CI scripts were run end to end locally.

Two findings from building this

Neither is fixed here, since both are in shipped code rather than in the tests:

FreeRTOS.h selects the interrupt primitives by compiler, not by target. A GNU build resolves portDISABLE_INTERRUPTS() to the bare metal __disable_interrupts() intrinsic, so hosting the layer on Linux with GCC fails to link in vPortEnterCritical(). The macros are #ifndef guarded and the header already carries a ThreadX based fallback for the #else branch, so the test config simply pre-empts the choice. A proper fix would gate the __GNUC__ branch on the target rather than the compiler.

The layer passes pointers through ULONG arguments — the task argument and the timer identifier among them — while the Linux port defines ULONG as unsigned int on x86_64. A 64 bit build truncates them, which GCC reports as -Wpointer-to-int-cast and which would crash the timer callback wrapper. The suite builds 32 bit, as the ThreadX and SMP suites do, so this is contained for now, but it is a real constraint on hosting the layer on a 64 bit target.

Notes for review

  • Linux only, because --wrap has no MSVC equivalent. The CMake configuration stops with an explicit message rather than failing later at link time.
  • Registered with ctest and wired into .github/workflows/regression_test.yml as a freertos job alongside tx and smp, with skip_coverage: true since there is no coverage build configuration yet. The deploy job's needs list is left alone, matching how the RISC-V job was configured.
  • The strict warning set is applied to the test code only. Neither ThreadX nor the layer builds clean under -Wextra today, and that is not this suite's to fix.

xQueueCreate() allocated the queue descriptor and its backing memory, then
created two ThreadX semaphores, and returned NULL on either semaphore failure
without releasing anything. Since no handle reached the caller, vQueueDelete()
could not be used to recover, so both allocations were lost. A failure on the
second semaphore additionally abandoned the read semaphore it had already
created, leaving a live ThreadX control block inside freed memory.

Release the backing memory and the descriptor on both paths, and delete the
read semaphore before returning when the write semaphore cannot be created.
This is the teardown order vQueueDelete() already uses, and it matches the
cleanup xTaskCreate() performs on its own error paths.

Verified with a fault injection harness that intercepts the ThreadX byte pool
and semaphore entry points to force tx_semaphore_create() to fail on a chosen
call. On a read semaphore failure the layer previously performed 2 allocations
and 0 releases, and on a write semaphore failure 2 allocations, 0 releases and
0 semaphore deletions. It now performs 2 releases in both cases and deletes
the read semaphore in the second, with the byte pool restored to its prior
state.

Fixes eclipse-threadx#570

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The compatibility layer had no tests in this repository, which is awkward for
its creation functions in particular. Each of them takes one or two byte pool
allocations for its bookkeeping and then creates ThreadX kernel objects, and
each returns NULL when a kernel object cannot be created. The caller is left
without a handle, so it cannot call the matching delete function, and anything
the layer failed to release is gone until the system restarts. A leaking
version and a correct version are indistinguishable from the outside, which is
how the leak in issue 570 went unnoticed.

Add a suite that counts what the layer takes and gives back. A test asks the
harness to fail a chosen kernel creation call, then checks the number of byte
pool allocations, releases, object creations and object deletions performed.
The ThreadX entry points are intercepted with the linker's --wrap so that
tx_freertos.c is compiled exactly as it ships, with no test hooks in it. Note
that tx_api.h maps the public API onto the error checking entry points, so the
_txe_ symbols are the ones wrapped. Coverage is the creation and teardown paths
of queues, tasks, semaphores, mutexes, event groups and timers, including a
regression test for the two paths fixed for issue 570.

The suite follows the layout of the existing ThreadX and SMP suites, is
registered with ctest, and runs in CI through the shared regression template.
It is built 32 bit because the Linux port defines ULONG as unsigned int on
x86_64 while the layer passes pointers through ULONG arguments, so a 64 bit
build truncates them. It is Linux only because --wrap has no MSVC equivalent,
and the CMake configuration says so rather than failing at link time.

Validated by building the suite against the layer as it stands before the
issue 570 fix, where the two expected checks fail with the leaked counts, and
against the fixed layer, where all three tests pass.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant