Added a regression suite for the FreeRTOS compatibility layer (#583)

* Fixed the resource leaks on the xQueueCreate error paths

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 https://github.com/eclipse-threadx/threadx/issues/570

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Added a regression suite for the FreeRTOS compatibility layer

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>
This commit is contained in:
Frédéric Desbiens
2026-08-09 08:03:16 -04:00
committed by GitHub
parent 5481fc75a0
commit f3df5f9dde
14 changed files with 1326 additions and 0 deletions
+18
View File
@@ -41,6 +41,24 @@ jobs:
cmake_path: ./test/smp/cmake
result_affix: SMP
skip_deploy: true
freertos:
permissions:
contents: read
issues: read
checks: write
pull-requests: write
pages: write
id-token: write
uses: ./.github/workflows/regression_template.yml
with:
build_script: ./scripts/build_freertos.sh
test_script: ./scripts/test_freertos.sh
cmake_path: ./test/freertos/cmake
result_affix: FreeRTOS
skip_deploy: true
# No coverage build configuration yet; the suite covers the creation
# paths of the layer rather than all of it.
skip_coverage: true
# riscv: disabled — re-enable when RISC-V CI is ready
# riscv:
# permissions:
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
##############################################################################
# Copyright (C) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
##############################################################################
$(dirname `realpath $0`)/../test/freertos/cmake/run.sh build all
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
##############################################################################
# Copyright (C) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
##############################################################################
CTEST_PARALLEL_LEVEL=4 $(dirname `realpath $0`)/../test/freertos/cmake/run.sh test all
+78
View File
@@ -0,0 +1,78 @@
##############################################################################
# Copyright (C) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
##############################################################################
cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
cmake_policy(SET CMP0054 NEW)
cmake_policy(SET CMP0057 NEW)
project(threadx_freertos_test LANGUAGES C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
# The suite intercepts the ThreadX entry points with the linker's --wrap, which
# is a GNU ld and lld feature. There is no MSVC equivalent, so the suite is
# built on Linux only and the build stops early elsewhere rather than failing
# later with confusing link errors.
if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
message(FATAL_ERROR
"The FreeRTOS compatibility layer tests require GNU ld or lld for "
"--wrap support and are supported on Linux only.")
endif()
set(BUILD_CONFIGURATIONS default_build)
set(CMAKE_CONFIGURATION_TYPES
${BUILD_CONFIGURATIONS}
CACHE STRING "list of supported configuration types" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
${CMAKE_CONFIGURATION_TYPES})
list(GET CMAKE_CONFIGURATION_TYPES 0 BUILD_TYPE)
if((NOT CMAKE_BUILD_TYPE) OR (NOT ("${CMAKE_BUILD_TYPE}" IN_LIST
CMAKE_CONFIGURATION_TYPES)))
set(CMAKE_BUILD_TYPE
"${BUILD_TYPE}"
CACHE STRING "Build Type of the project" FORCE)
endif()
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS "Using toolchain file: ${CMAKE_TOOLCHAIN_FILE}.")
# Built 32 bit, as the ThreadX and SMP suites are. This is not incidental for
# this suite: the Linux port defines ULONG as unsigned int on x86_64, while the
# compatibility layer passes pointers through ULONG arguments, for instance the
# task argument and the timer identifier. A 64 bit build therefore truncates
# those pointers, which the compiler reports as -Wpointer-to-int-cast and which
# would crash the timer callback wrapper.
add_compile_options(-m32)
add_link_options(-m32)
# Applied to everything, including the ThreadX build pulled in below. The
# strict warning set is applied to the test code alone, in the regression
# CMakeLists, since neither ThreadX nor the layer under test builds clean
# under -Wextra today and that is not this suite's to fix.
add_compile_options(-ggdb -g3 -fdiagnostics-color)
enable_testing()
# The compatibility layer stores a back pointer in each thread control block,
# so ThreadX has to be built with the matching user extension.
set(TX_USER_FILE
${CMAKE_CURRENT_LIST_DIR}/../fixtures/tx_user.h
CACHE FILEPATH "tx_user.h used for the FreeRTOS layer tests" FORCE)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../.. threadx)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/regression)
@@ -0,0 +1,83 @@
##############################################################################
# Copyright (C) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
##############################################################################
cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
cmake_policy(SET CMP0057 NEW)
project(freertos_regression_test LANGUAGES C)
set(SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/../../regression)
set(FIXTURE_DIR ${CMAKE_CURRENT_LIST_DIR}/../../fixtures)
set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..)
set(LAYER_SOURCE ${REPO_ROOT}/utility/rtos_compatibility_layers/FreeRTOS/tx_freertos.c)
set(LAYER_INCLUDE_DIR ${REPO_ROOT}/utility/rtos_compatibility_layers/FreeRTOS)
set(freertos_test_cases
${SOURCE_DIR}/txfr_queue_create_test.c
${SOURCE_DIR}/txfr_task_create_test.c
${SOURCE_DIR}/txfr_sync_create_test.c)
# tx_api.h maps the public API onto the error checking entry points, so those
# are the symbols that exist at link time and the ones the harness wraps. A
# name that does not resolve is silently ignored by the linker, so this list
# must be kept in step with the __wrap_ functions in txfr_test_harness.c.
set(wrapped_symbols
_txe_byte_allocate
_txe_byte_release
_txe_semaphore_create
_txe_semaphore_delete
_txe_mutex_create
_txe_mutex_delete
_txe_event_flags_create
_txe_event_flags_delete
_txe_timer_create
_txe_timer_delete
_txe_thread_create
_txe_thread_delete)
set(wrap_link_options "")
foreach(symbol ${wrapped_symbols})
list(APPEND wrap_link_options "-Wl,--wrap=${symbol}")
endforeach()
# The layer is compiled from source into each test rather than linked from the
# freertos-threadx target, so that the test's FreeRTOSConfig.h applies.
add_library(freertos_layer_under_test OBJECT ${LAYER_SOURCE})
target_include_directories(freertos_layer_under_test
PUBLIC ${LAYER_INCLUDE_DIR} ${FIXTURE_DIR})
target_link_libraries(freertos_layer_under_test PUBLIC azrtos::threadx)
set(test_warning_options -Wall -Wextra -Werror)
add_library(freertos_test_harness OBJECT ${SOURCE_DIR}/txfr_test_harness.c)
target_include_directories(freertos_test_harness
PUBLIC ${SOURCE_DIR} ${LAYER_INCLUDE_DIR} ${FIXTURE_DIR})
target_link_libraries(freertos_test_harness PUBLIC azrtos::threadx)
target_compile_options(freertos_test_harness PRIVATE ${test_warning_options})
foreach(test_case ${freertos_test_cases})
get_filename_component(test_name ${test_case} NAME_WE)
add_executable(${test_name} ${test_case}
$<TARGET_OBJECTS:freertos_test_harness>
$<TARGET_OBJECTS:freertos_layer_under_test>)
target_include_directories(${test_name}
PRIVATE ${SOURCE_DIR} ${LAYER_INCLUDE_DIR} ${FIXTURE_DIR})
target_link_libraries(${test_name} PRIVATE azrtos::threadx)
target_link_options(${test_name} PRIVATE ${wrap_link_options})
target_compile_options(${test_name} PRIVATE ${test_warning_options})
add_test(${CMAKE_BUILD_TYPE}::${test_name} ${test_name})
endforeach()
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
##############################################################################
# Copyright (C) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
##############################################################################
# Builds and runs the FreeRTOS compatibility layer regression suite. The
# interface matches test/tx/cmake/run.sh so that the shared CI template can
# drive it: "run.sh build all" then "run.sh test all".
set -e
function help() {
echo "Usage: $0 [build|test] [all|<build_configuration> <build_configuration>...]"
echo "Available build_configuration:"
for build in ${build_configurations[*]}; do
echo " $build"
done
exit 1
}
function validate() {
for build in ${build_configurations[*]}; do
if [ "$1" == "$build" ]; then
return
fi
done
help
}
function generate() {
build=$1
cmake -Bbuild/$build -GNinja -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../../../cmake/linux.cmake -DCMAKE_BUILD_TYPE=$build .
}
function build() {
cmake --build build/$1
}
function test() {
pushd build/$1
[ -z "${CTEST_PARALLEL_LEVEL}" ] && parallel="-j$2"
if [ -z "${CTEST_REPEAT_FAIL}" ];
then
repeat_fail=2
else
repeat_fail=${CTEST_REPEAT_FAIL}
fi
ctest $parallel --timeout 300 -O $1.txt -T test --no-compress-output --test-output-size-passed 4194304 --test-output-size-failed 4194304 --output-on-failure --repeat until-pass:${repeat_fail} --output-junit $1.xml
popd
grep -E "^(\s*[0-9]+|Total)" build/$1/$1.txt >build/$1.txt
sed -i "s/\x1B\[[0-9;]*[JKmsu]//g" build/$1.txt
}
cd $(dirname $0)
result=$(sed -n "/(BUILD_CONFIGURATIONS/,/)/p" CMakeLists.txt|sed ':label;N;s/\n/ /;b label'|grep -Pzo "[a-zA-Z0-9_]*build[a-zA-Z0-9_]*\s*"| tr -d '\0')
IFS=' '
read -ra build_configurations <<< "$result"
if [ $# -lt 1 ]; then
help
fi
command=$1
shift
if [ "$#" == "0" ]; then
builds=${build_configurations[0]}
elif [ "$*" == "all" ]; then
builds=${build_configurations[@]}
else
for item in $*; do
validate $item
done
builds=$*
fi
if [ "$command" == "build" ]; then
for item in $builds; do
generate $item
echo ""
done
for item in $builds; do
echo "Building $item"
build $item
echo ""
done
elif [ "$command" == "test" ]; then
cores=$(nproc)
if [ -z "${CTEST_PARALLEL_LEVEL}" ];
then
parallel_jobs=$(($cores + 2))
fi
for item in $builds; do
echo "Testing $item"
test $item $parallel_jobs
done
else
help
fi
+66
View File
@@ -0,0 +1,66 @@
/***************************************************************************
* Copyright (C) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** FreeRTOS compatibility layer regression test configuration */
/** */
/**************************************************************************/
/* Configuration used to build the compatibility layer under test. It follows
the shipped template in utility/rtos_compatibility_layers/FreeRTOS/
config_template, with the choices a test build needs called out below. */
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
#define configTICK_RATE_HZ (1000u)
#define configMAX_PRIORITIES (32u)
#define configMINIMAL_STACK_SIZE (512u)
/* Large enough that a handful of leaked descriptors cannot exhaust it. The
tests therefore never infer a leak from an allocation failure; they count
the byte pool operations directly. */
#define configTOTAL_HEAP_SIZE (1024u * 128u)
#define configUSE_16_BIT_TICKS 0
#define configSTACK_DEPTH_TYPE uint32_t
/* Task deletion is included so that the idle task, and with it the teardown
path the tests exercise, is built. */
#define INCLUDE_vTaskDelete 1
/* The tests drive the error paths deliberately, so neither assertion may halt
the run. A failed expectation is reported by the harness instead. */
#define configASSERT(x)
#define TX_FREERTOS_ASSERT_FAIL()
/* The harness calls tx_freertos_init() from tx_application_define(), so the
automatic initialization path is not wanted here. */
#define TX_FREERTOS_AUTO_INIT 0
/* FreeRTOS.h selects portDISABLE_INTERRUPTS() by compiler rather than by
target, so a GNU build resolves it to the bare metal __disable_interrupts()
intrinsic, which does not exist when the layer is hosted on Linux. Both
macros are guarded with #ifndef, so the definitions below pre-empt that
choice with the ThreadX primitives the header's own fallback branch uses. */
UINT _tx_thread_interrupt_disable(VOID);
VOID _tx_thread_interrupt_restore(UINT previous_posture);
#define portDISABLE_INTERRUPTS() _tx_thread_interrupt_disable()
#define portENABLE_INTERRUPTS() _tx_thread_interrupt_restore(TX_INT_ENABLE)
#endif /* #ifndef FREERTOS_CONFIG_H */
+35
View File
@@ -0,0 +1,35 @@
/***************************************************************************
* Copyright (C) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** FreeRTOS compatibility layer regression test ThreadX settings */
/** */
/**************************************************************************/
/* Every other ThreadX setting is left at its default; only the extension the
compatibility layer requires is defined here. */
#ifndef TX_USER_H
#define TX_USER_H
/* The layer stores a back pointer to its own task structure in each thread
control block, as documented in the layer's readme.md. Without this the
layer does not compile. */
#define TX_THREAD_USER_EXTENSION VOID *txfr_thread_ptr;
#endif /* #ifndef TX_USER_H */
+86
View File
@@ -0,0 +1,86 @@
# FreeRTOS compatibility layer regression tests
Regression tests for `utility/rtos_compatibility_layers/FreeRTOS/tx_freertos.c`,
built against the real ThreadX Linux port.
## Running them
```bash
./scripts/build_freertos.sh
./scripts/test_freertos.sh
```
Or directly, which is the same thing:
```bash
test/freertos/cmake/run.sh build all
test/freertos/cmake/run.sh test all
```
CI runs both scripts through `.github/workflows/regression_test.yml`.
## What they cover
Every function in the layer that creates an object takes one or two byte pool
allocations for its bookkeeping and then creates one or more ThreadX kernel
objects. When one of those kernel objects cannot be created, the function
returns `NULL`, or `pdFAIL` for `xTaskCreate()`, and the caller is left with no
handle and therefore no way to call the matching delete function. Anything the
layer fails to release on the way out is lost until the system restarts.
That makes these error paths invisible from the outside: a leaking version and
a correct version return exactly the same thing to the caller. The suite
therefore counts the ThreadX primitives the layer reaches for, and checks that
each error path gives back precisely what it took.
| Test | Covers |
| --- | --- |
| `txfr_queue_create_test` | `xQueueCreate`, `xQueueCreateStatic`, `vQueueDelete` |
| `txfr_task_create_test` | `xTaskCreate`, `xTaskCreateStatic` |
| `txfr_sync_create_test` | semaphores, mutexes, event groups and timers |
## How the fault injection works
A test asks the harness to fail a chosen kernel creation call, then reads back
how many allocations, releases, object creations and object deletions the layer
performed. The interception is done with the linker's `--wrap` option, so
`tx_freertos.c` is compiled exactly as it ships, with no test hooks in it.
Two things are worth knowing before adding tests:
- `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, and those are what
the wrap list in `cmake/regression/CMakeLists.txt` names. A `--wrap` for a
name that does not resolve is silently ignored, so a typo there produces a
test that quietly never injects anything.
- `txfr_malloc()` and `txfr_free()` cannot be wrapped, because 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.
Because `--wrap` is a GNU ld and lld feature with no MSVC equivalent, this
suite is Linux only. The CMake configuration stops with a clear message rather
than failing later with confusing link errors.
## Fixtures
`fixtures/FreeRTOSConfig.h` configures the layer for the tests. Two of its
settings are not arbitrary:
- `configASSERT()` and `TX_FREERTOS_ASSERT_FAIL()` are empty, since the tests
drive error paths deliberately and neither may halt the run.
- `portDISABLE_INTERRUPTS()` and `portENABLE_INTERRUPTS()` are defined up front.
`FreeRTOS.h` picks those by compiler rather than by target, so a GNU build
otherwise resolves them to the bare metal `__disable_interrupts()` intrinsic,
which does not exist when the layer is hosted on Linux.
`fixtures/tx_user.h` supplies `TX_THREAD_USER_EXTENSION`, which the layer
requires, as documented in the layer's own `readme.md`.
## Why the build is 32 bit
The Linux port defines `ULONG` as `unsigned int` on x86_64, while the layer
passes pointers through `ULONG` arguments, such as the task argument and the
timer identifier. A 64 bit build truncates those pointers, which the compiler
reports as `-Wpointer-to-int-cast` and which crashes the timer callback
wrapper. The ThreadX and SMP suites build 32 bit for their own reasons; this
suite has to.
@@ -0,0 +1,116 @@
/***************************************************************************
* Copyright (C) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** FreeRTOS compatibility layer, queue creation resource accounting */
/** */
/**************************************************************************/
/* xQueueCreate() takes two byte pool allocations, the descriptor and the
backing storage, and then creates two semaphores. Since a failure returns
NULL, the caller has no handle and cannot call vQueueDelete(), so anything
the layer fails to undo on the way out is lost for good. These checks pin
down the accounting on each of those paths.
The failure paths here are the ones reported in issue 570. */
#include "txfr_test_harness.h"
#include "queue.h"
#define QUEUE_LENGTH 8u
#define QUEUE_ITEM_SIZE 32u
/* Storage for the static variant. */
static StaticQueue_t queue_control_block;
static uint8_t queue_storage[QUEUE_LENGTH * QUEUE_ITEM_SIZE];
void txfr_test_body(void)
{
QueueHandle_t queue;
TXFR_COUNTERS counters;
int i;
int cycles;
/* A successful creation takes both allocations and creates both
semaphores, and releases nothing. */
txfr_test_account_start(TXFR_INJECT_NONE, 0);
queue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
txfr_test_account_stop(&counters);
txfr_test_check("xQueueCreate returns a handle", queue != NULL);
txfr_test_check_counts("xQueueCreate accounting", &counters, 2, 0, 2, 0);
/* Deleting it gives both allocations back and deletes both semaphores. */
if(queue != NULL)
{
txfr_test_account_start(TXFR_INJECT_NONE, 0);
vQueueDelete(queue);
txfr_test_account_stop(&counters);
txfr_test_check_counts("vQueueDelete accounting", &counters, 0, 2, 0, 2);
}
/* The read semaphore fails. Nothing was created that needs deleting, but
both allocations must be released. */
txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
queue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
txfr_test_account_stop(&counters);
txfr_test_check("xQueueCreate returns NULL on read_sem failure", queue == NULL);
txfr_test_check_counts("read_sem failure releases both allocations", &counters, 2, 2, 1, 0);
/* The write semaphore fails. The read semaphore already exists by then, so
it must be deleted as well as both allocations released. */
txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 2);
queue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
txfr_test_account_stop(&counters);
txfr_test_check("xQueueCreate returns NULL on write_sem failure", queue == NULL);
txfr_test_check_counts("write_sem failure unwinds read_sem and memory", &counters, 2, 2, 2, 1);
/* The static variant takes no allocations and creates both semaphores. */
txfr_test_account_start(TXFR_INJECT_NONE, 0);
queue = xQueueCreateStatic(QUEUE_LENGTH, QUEUE_ITEM_SIZE, queue_storage, &queue_control_block);
txfr_test_account_stop(&counters);
txfr_test_check("xQueueCreateStatic returns a handle", queue != NULL);
txfr_test_check_counts("xQueueCreateStatic accounting", &counters, 0, 0, 2, 0);
if(queue != NULL)
{
txfr_test_account_start(TXFR_INJECT_NONE, 0);
vQueueDelete(queue);
txfr_test_account_stop(&counters);
txfr_test_check_counts("vQueueDelete on static queue", &counters, 0, 0, 0, 2);
}
/* Repeated create and delete cycles must leave the pool able to serve the
same request. This would not catch a small leak on its own, which is why
the counts above are checked directly, but it does catch a descriptor
that is released to the wrong pool or released twice. */
cycles = 0;
for(i = 0; i < 64; i++)
{
queue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
if(queue == NULL)
{
break;
}
cycles++;
vQueueDelete(queue);
}
txfr_test_check("64 create and delete cycles succeed", cycles == 64);
}
@@ -0,0 +1,129 @@
/***************************************************************************
* Copyright (C) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** FreeRTOS compatibility layer, synchronisation object accounting */
/** */
/**************************************************************************/
/* Semaphores, mutexes, event groups and timers each take a single descriptor
allocation and then create one kernel object. Every one of them must give
the descriptor back when that kernel object cannot be created. */
#include "txfr_test_harness.h"
#include "semphr.h"
#include "event_groups.h"
#include "timers.h"
static void test_timer_callback(TimerHandle_t timer)
{
(void)timer;
}
void txfr_test_body(void)
{
SemaphoreHandle_t semaphore;
EventGroupHandle_t event_group;
TimerHandle_t timer;
TXFR_COUNTERS counters;
/* Counting semaphore. */
txfr_test_account_start(TXFR_INJECT_NONE, 0);
semaphore = xSemaphoreCreateCounting(4u, 0u);
txfr_test_account_stop(&counters);
txfr_test_check("xSemaphoreCreateCounting returns a handle", semaphore != NULL);
txfr_test_check_counts("xSemaphoreCreateCounting accounting", &counters, 1, 0, 1, 0);
if(semaphore != NULL)
{
vSemaphoreDelete(semaphore);
}
txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
semaphore = xSemaphoreCreateCounting(4u, 0u);
txfr_test_account_stop(&counters);
txfr_test_check("xSemaphoreCreateCounting returns NULL on failure", semaphore == NULL);
txfr_test_check_counts("counting semaphore failure releases descriptor", &counters, 1, 1, 1, 0);
/* Binary semaphore, which shares the same descriptor path. */
txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
semaphore = xSemaphoreCreateBinary();
txfr_test_account_stop(&counters);
txfr_test_check("xSemaphoreCreateBinary returns NULL on failure", semaphore == NULL);
txfr_test_check_counts("binary semaphore failure releases descriptor", &counters, 1, 1, 1, 0);
/* Mutex. */
txfr_test_account_start(TXFR_INJECT_NONE, 0);
semaphore = xSemaphoreCreateMutex();
txfr_test_account_stop(&counters);
txfr_test_check("xSemaphoreCreateMutex returns a handle", semaphore != NULL);
txfr_test_check_counts("xSemaphoreCreateMutex accounting", &counters, 1, 0, 1, 0);
if(semaphore != NULL)
{
vSemaphoreDelete(semaphore);
}
txfr_test_account_start(TXFR_INJECT_MUTEX_CREATE, 1);
semaphore = xSemaphoreCreateMutex();
txfr_test_account_stop(&counters);
txfr_test_check("xSemaphoreCreateMutex returns NULL on failure", semaphore == NULL);
txfr_test_check_counts("mutex failure releases descriptor", &counters, 1, 1, 1, 0);
/* Recursive mutex, which differs only in the inheritance argument. */
txfr_test_account_start(TXFR_INJECT_MUTEX_CREATE, 1);
semaphore = xSemaphoreCreateRecursiveMutex();
txfr_test_account_stop(&counters);
txfr_test_check("xSemaphoreCreateRecursiveMutex returns NULL on failure", semaphore == NULL);
txfr_test_check_counts("recursive mutex failure releases descriptor", &counters, 1, 1, 1, 0);
/* Event group. */
txfr_test_account_start(TXFR_INJECT_NONE, 0);
event_group = xEventGroupCreate();
txfr_test_account_stop(&counters);
txfr_test_check("xEventGroupCreate returns a handle", event_group != NULL);
txfr_test_check_counts("xEventGroupCreate accounting", &counters, 1, 0, 1, 0);
if(event_group != NULL)
{
vEventGroupDelete(event_group);
}
txfr_test_account_start(TXFR_INJECT_EVENT_FLAGS_CREATE, 1);
event_group = xEventGroupCreate();
txfr_test_account_stop(&counters);
txfr_test_check("xEventGroupCreate returns NULL on failure", event_group == NULL);
txfr_test_check_counts("event group failure releases descriptor", &counters, 1, 1, 1, 0);
/* Timer. */
txfr_test_account_start(TXFR_INJECT_NONE, 0);
timer = xTimerCreate("t", 10u, pdFALSE, NULL, test_timer_callback);
txfr_test_account_stop(&counters);
txfr_test_check("xTimerCreate returns a handle", timer != NULL);
txfr_test_check_counts("xTimerCreate accounting", &counters, 1, 0, 1, 0);
if(timer != NULL)
{
(void)xTimerDelete(timer, 0u);
}
txfr_test_account_start(TXFR_INJECT_TIMER_CREATE, 1);
timer = xTimerCreate("t", 10u, pdFALSE, NULL, test_timer_callback);
txfr_test_account_stop(&counters);
txfr_test_check("xTimerCreate returns NULL on failure", timer == NULL);
txfr_test_check_counts("timer failure releases descriptor", &counters, 1, 1, 1, 0);
}
@@ -0,0 +1,105 @@
/***************************************************************************
* Copyright (C) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** FreeRTOS compatibility layer, task creation resource accounting */
/** */
/**************************************************************************/
/* xTaskCreate() takes two byte pool allocations, the stack and the task
structure, then creates a notification semaphore and a thread. It is the
function the rest of the layer's error handling is modelled on, so these
checks also serve as the reference for what correct unwinding looks like. */
#include "txfr_test_harness.h"
#include "task.h"
#define TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE)
#define TASK_PRIORITY (3u)
static StaticTask_t task_control_block;
static StackType_t task_stack[TASK_STACK_DEPTH];
static void test_task_entry(void *p_arg)
{
(void)p_arg;
/* Never scheduled at this priority while the test thread runs, but a task
entry point must not return. */
for(;;)
{
vTaskDelay(portMAX_DELAY);
}
}
void txfr_test_body(void)
{
BaseType_t result;
TaskHandle_t task;
TXFR_COUNTERS counters;
/* A successful creation takes both allocations, creates the notification
semaphore and the thread, and releases nothing. */
task = NULL;
txfr_test_account_start(TXFR_INJECT_NONE, 0);
result = xTaskCreate(test_task_entry, "ok", TASK_STACK_DEPTH, NULL, TASK_PRIORITY, &task);
txfr_test_account_stop(&counters);
txfr_test_check("xTaskCreate reports success", result == pdPASS);
txfr_test_check("xTaskCreate returns a handle", task != NULL);
txfr_test_check_counts("xTaskCreate accounting", &counters, 2, 0, 2, 0);
if(task != NULL)
{
vTaskDelete(task);
}
/* The notification semaphore fails before anything else is created, so
both allocations must come back and nothing needs deleting. */
task = NULL;
txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
result = xTaskCreate(test_task_entry, "sem", TASK_STACK_DEPTH, NULL, TASK_PRIORITY, &task);
txfr_test_account_stop(&counters);
txfr_test_check("xTaskCreate fails when the semaphore fails", result != pdPASS);
txfr_test_check_counts("semaphore failure releases stack and task", &counters, 2, 2, 1, 0);
/* The thread creation fails with the semaphore already in place, so that
semaphore must be deleted and both allocations released. */
task = NULL;
txfr_test_account_start(TXFR_INJECT_THREAD_CREATE, 1);
result = xTaskCreate(test_task_entry, "thread", TASK_STACK_DEPTH, NULL, TASK_PRIORITY, &task);
txfr_test_account_stop(&counters);
txfr_test_check("xTaskCreate fails when the thread fails", result != pdPASS);
txfr_test_check_counts("thread failure unwinds the semaphore too", &counters, 2, 2, 2, 1);
/* The static variant takes no allocations, and creates the semaphore and
the thread from the caller's buffers. */
txfr_test_account_start(TXFR_INJECT_NONE, 0);
task = xTaskCreateStatic(test_task_entry, "static", TASK_STACK_DEPTH, NULL, TASK_PRIORITY,
task_stack, &task_control_block);
txfr_test_account_stop(&counters);
txfr_test_check("xTaskCreateStatic returns a handle", task != NULL);
txfr_test_check_counts("xTaskCreateStatic accounting", &counters, 0, 0, 2, 0);
if(task != NULL)
{
vTaskDelete(task);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
/***************************************************************************
* Copyright (C) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** FreeRTOS compatibility layer test harness */
/** */
/**************************************************************************/
/* This harness lets a test force a ThreadX object creation call to fail and
then account for what the compatibility layer did with the resources it had
already taken. The creation entry points of the layer take a byte pool
allocation for the descriptor, sometimes a second one for backing storage,
and then create one or more kernel objects; an error path that forgets to
undo any of that is invisible to the caller, which only ever sees NULL.
Interception is done with the linker's --wrap option rather than with hooks
in the layer itself, so tx_freertos.c is compiled exactly as it ships. Note
that tx_api.h maps the public API onto the error checking entry points, so
the symbols that exist at link time, and therefore the ones wrapped, are the
_txe_ variants. The wrap options live in the test CMakeLists.
This mechanism is specific to GNU ld and lld, so the suite is Linux only. */
#ifndef TXFR_TEST_HARNESS_H
#define TXFR_TEST_HARNESS_H
#include "FreeRTOS.h"
/* Identifies the kernel creation call a test wants to fail. */
typedef enum TXFR_INJECT_TARGET_ENUM
{
TXFR_INJECT_NONE = 0,
TXFR_INJECT_SEMAPHORE_CREATE,
TXFR_INJECT_MUTEX_CREATE,
TXFR_INJECT_EVENT_FLAGS_CREATE,
TXFR_INJECT_TIMER_CREATE,
TXFR_INJECT_THREAD_CREATE
} TXFR_INJECT_TARGET;
/* Counts of the ThreadX primitives the layer reached for while accounting was
active. The byte pool counts stand in for txfr_malloc() and txfr_free(),
which cannot be wrapped: they are defined in tx_freertos.c and called from
within it, so the compiler resolves those calls internally. */
typedef struct TXFR_COUNTERS_STRUCT
{
int byte_allocate;
int byte_release;
int semaphore_create;
int semaphore_delete;
int mutex_create;
int mutex_delete;
int event_flags_create;
int event_flags_delete;
int timer_create;
int timer_delete;
int thread_create;
int thread_delete;
} TXFR_COUNTERS;
/* Start accounting. Pass TXFR_INJECT_NONE to only count, or a target together
with the 1 based index of the call to fail, counted from this call onward. */
void txfr_test_account_start(TXFR_INJECT_TARGET target, int fail_on_call);
/* Stop accounting and copy out what was counted. */
void txfr_test_account_stop(TXFR_COUNTERS *p_counters);
/* Record the outcome of one expectation. Prints a line per check and remembers
whether anything failed. */
void txfr_test_check(const char *label, int condition);
/* As above, with the observed and expected counts printed on a failure. */
void txfr_test_check_counts(const char *label, const TXFR_COUNTERS *p_counters,
int expect_allocate, int expect_release,
int expect_object_create, int expect_object_delete);
/* Number of failed checks so far. */
int txfr_test_failures(void);
/* Each test file defines this. It runs in thread context, since the byte pool
operations under test are not callable from initialization. */
void txfr_test_body(void);
#endif /* TXFR_TEST_HARNESS_H */