summaryrefslogtreecommitdiff
path: root/test/freertos
diff options
context:
space:
mode:
Diffstat (limited to 'test/freertos')
-rw-r--r--test/freertos/cmake/CMakeLists.txt78
-rw-r--r--test/freertos/cmake/regression/CMakeLists.txt84
-rwxr-xr-xtest/freertos/cmake/run.sh135
-rw-r--r--test/freertos/fixtures/FreeRTOSConfig.h66
-rw-r--r--test/freertos/fixtures/tx_user.h35
-rw-r--r--test/freertos/readme.md86
-rw-r--r--test/freertos/regression/txfr_queue_create_test.c132
-rw-r--r--test/freertos/regression/txfr_sync_create_test.c129
-rw-r--r--test/freertos/regression/txfr_task_create_test.c145
-rw-r--r--test/freertos/regression/txfr_test_harness.c384
-rw-r--r--test/freertos/regression/txfr_test_harness.h103
11 files changed, 1377 insertions, 0 deletions
diff --git a/test/freertos/cmake/CMakeLists.txt b/test/freertos/cmake/CMakeLists.txt
new file mode 100644
index 00000000..a8c07d8d
--- /dev/null
+++ b/test/freertos/cmake/CMakeLists.txt
@@ -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)
diff --git a/test/freertos/cmake/regression/CMakeLists.txt b/test/freertos/cmake/regression/CMakeLists.txt
new file mode 100644
index 00000000..21da2a6d
--- /dev/null
+++ b/test/freertos/cmake/regression/CMakeLists.txt
@@ -0,0 +1,84 @@
+##############################################################################
+# 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
+ _txe_thread_resume)
+
+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()
diff --git a/test/freertos/cmake/run.sh b/test/freertos/cmake/run.sh
new file mode 100755
index 00000000..896a5c37
--- /dev/null
+++ b/test/freertos/cmake/run.sh
@@ -0,0 +1,135 @@
+#!/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() {
+ # -k 0 keeps Ninja going after a target fails, so one broken target no
+ # longer decides whether the targets after it exist. ctest reports a
+ # missing binary as a failing test, which turned a single link error into
+ # a failure count that varied with build scheduling order.
+ cmake --build build/$1 -- -k 0
+}
+
+function test() {
+ # Guard the pushd: with the caller capturing this function's status, set -e
+ # no longer aborts here, so a missing build directory would otherwise let
+ # ctest run in the source tree and report "no tests" as success.
+ pushd build/$1 || return 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's status is captured and returned past the popd and the summary
+ # steps, which all succeed. Without this the function would return 0 and a
+ # failing configuration would be reported as a pass.
+ local status=0
+ 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 || status=$?
+ popd
+ # Tolerated: this is a summary for humans, and a ctest that died early
+ # enough to leave no matching line must not mask its own status.
+ grep -E "^(\s*[0-9]+|Total)" build/$1/$1.txt >build/$1.txt || true
+ sed -i "s/\x1B\[[0-9;]*[JKmsu]//g" build/$1.txt
+ return $status
+}
+
+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
+
+ # A failing configuration must not stop the ones after it: under set -e
+ # the loop would abort and leave them unbuilt, which then reads as a wall
+ # of missing-binary test failures. The status is accumulated and returned.
+ build_status=0
+ for item in $builds; do
+ echo "Building $item"
+ build $item || build_status=$?
+ echo ""
+ done
+ [ $build_status -eq 0 ] || exit $build_status
+elif [ "$command" == "test" ]; then
+ cores=$(nproc)
+ if [ -z "${CTEST_PARALLEL_LEVEL}" ];
+ then
+ parallel_jobs=$(($cores + 2))
+ fi
+ # ctest's status is accumulated rather than allowed to abort the loop, so
+ # a failing configuration does not leave the ones after it untested.
+ exit_code=0
+ for item in $builds; do
+ echo "Testing $item"
+ test $item $parallel_jobs || exit_code=$?
+ done
+ exit $exit_code
+else
+ help
+fi
diff --git a/test/freertos/fixtures/FreeRTOSConfig.h b/test/freertos/fixtures/FreeRTOSConfig.h
new file mode 100644
index 00000000..017b7171
--- /dev/null
+++ b/test/freertos/fixtures/FreeRTOSConfig.h
@@ -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 */
diff --git a/test/freertos/fixtures/tx_user.h b/test/freertos/fixtures/tx_user.h
new file mode 100644
index 00000000..834e711e
--- /dev/null
+++ b/test/freertos/fixtures/tx_user.h
@@ -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 */
diff --git a/test/freertos/readme.md b/test/freertos/readme.md
new file mode 100644
index 00000000..0bb1b650
--- /dev/null
+++ b/test/freertos/readme.md
@@ -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.
diff --git a/test/freertos/regression/txfr_queue_create_test.c b/test/freertos/regression/txfr_queue_create_test.c
new file mode 100644
index 00000000..aa996070
--- /dev/null
+++ b/test/freertos/regression/txfr_queue_create_test.c
@@ -0,0 +1,132 @@
+/***************************************************************************
+ * 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);
+ }
+
+ /* The static variant owns no memory, but it still creates two semaphores
+ and still hands back nothing on failure, so the caller cannot call
+ vQueueDelete(). A read semaphore left behind here would stay registered
+ in the kernel, pointing into the caller's StaticQueue_t. */
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
+ queue = xQueueCreateStatic(QUEUE_LENGTH, QUEUE_ITEM_SIZE, queue_storage, &queue_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xQueueCreateStatic returns NULL on read_sem failure", queue == NULL);
+ txfr_test_check_counts("static read_sem failure leaves nothing behind", &counters, 0, 0, 1, 0);
+
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 2);
+ queue = xQueueCreateStatic(QUEUE_LENGTH, QUEUE_ITEM_SIZE, queue_storage, &queue_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xQueueCreateStatic returns NULL on write_sem failure", queue == NULL);
+ txfr_test_check_counts("static write_sem failure unwinds read_sem", &counters, 0, 0, 2, 1);
+
+ /* 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);
+}
diff --git a/test/freertos/regression/txfr_sync_create_test.c b/test/freertos/regression/txfr_sync_create_test.c
new file mode 100644
index 00000000..14bd53d8
--- /dev/null
+++ b/test/freertos/regression/txfr_sync_create_test.c
@@ -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);
+}
diff --git a/test/freertos/regression/txfr_task_create_test.c b/test/freertos/regression/txfr_task_create_test.c
new file mode 100644
index 00000000..2371bcc0
--- /dev/null
+++ b/test/freertos/regression/txfr_task_create_test.c
@@ -0,0 +1,145 @@
+/***************************************************************************
+ * 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];
+
+/* Each failure case gets its own buffers, for two reasons. vTaskDelete() only
+ queues a task for the idle task to reap, so the control block of a task
+ deleted above is still live while this test thread runs. And if one of these
+ paths ever regresses and leaves an object behind, a shared buffer would
+ carry that damage into the next case and report misleading counts there. */
+static StaticTask_t semaphore_failure_control_block;
+static StackType_t semaphore_failure_stack[TASK_STACK_DEPTH];
+static StaticTask_t thread_failure_control_block;
+static StackType_t thread_failure_stack[TASK_STACK_DEPTH];
+static StaticTask_t resume_failure_control_block;
+static StackType_t resume_failure_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);
+ }
+
+ /* As with the queues, the static variant owns no memory but still creates
+ kernel objects and still returns nothing on failure. A semaphore or a
+ thread left behind here stays registered in the kernel, pointing into
+ the caller's StaticTask_t. */
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
+ task = xTaskCreateStatic(test_task_entry, "s_sem", TASK_STACK_DEPTH, NULL, TASK_PRIORITY,
+ semaphore_failure_stack, &semaphore_failure_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreateStatic returns NULL on semaphore failure", task == NULL);
+ txfr_test_check_counts("static semaphore failure leaves nothing behind", &counters, 0, 0, 1, 0);
+
+ txfr_test_account_start(TXFR_INJECT_THREAD_CREATE, 1);
+ task = xTaskCreateStatic(test_task_entry, "s_thr", TASK_STACK_DEPTH, NULL, TASK_PRIORITY,
+ thread_failure_stack, &thread_failure_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreateStatic returns NULL on thread failure", task == NULL);
+ txfr_test_check_counts("static thread failure unwinds the semaphore", &counters, 0, 0, 2, 1);
+
+ /* The resume is the last step, so by then both kernel objects exist. The
+ thread has to be terminated before it can be deleted, which is the order
+ the idle task uses when it reaps a deleted task. */
+ txfr_test_account_start(TXFR_INJECT_THREAD_RESUME, 1);
+ task = xTaskCreateStatic(test_task_entry, "s_res", TASK_STACK_DEPTH, NULL, TASK_PRIORITY,
+ resume_failure_stack, &resume_failure_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreateStatic returns NULL on resume failure", task == NULL);
+ txfr_test_check_counts("static resume failure unwinds thread and semaphore", &counters, 0, 0, 2, 2);
+}
diff --git a/test/freertos/regression/txfr_test_harness.c b/test/freertos/regression/txfr_test_harness.c
new file mode 100644
index 00000000..b5bd45de
--- /dev/null
+++ b/test/freertos/regression/txfr_test_harness.c
@@ -0,0 +1,384 @@
+/***************************************************************************
+ * 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 */
+/** */
+/**************************************************************************/
+
+#include "txfr_test_harness.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+/* Accounting state. Only the single test thread touches these. */
+static int txfr_accounting;
+static TXFR_INJECT_TARGET txfr_inject_target;
+static int txfr_inject_fail_on;
+static int txfr_inject_call_index;
+static TXFR_COUNTERS txfr_counters;
+static int txfr_failures;
+
+/* The real entry points, supplied by the linker in response to --wrap. */
+UINT __real__txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option);
+UINT __real__txe_byte_release(VOID *memory_ptr);
+UINT __real__txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size);
+UINT __real__txe_semaphore_delete(TX_SEMAPHORE *semaphore_ptr);
+UINT __real__txe_mutex_create(TX_MUTEX *mutex_ptr, TX_NAME_CONST CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size);
+UINT __real__txe_mutex_delete(TX_MUTEX *mutex_ptr);
+UINT __real__txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, TX_NAME_CONST CHAR *name_ptr, UINT event_control_block_size);
+UINT __real__txe_event_flags_delete(TX_EVENT_FLAGS_GROUP *group_ptr);
+UINT __real__txe_timer_create(TX_TIMER *timer_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*expiration_function)(ULONG input),
+ ULONG expiration_input, ULONG initial_ticks, ULONG reschedule_ticks,
+ UINT auto_activate, UINT timer_control_block_size);
+UINT __real__txe_timer_delete(TX_TIMER *timer_ptr);
+UINT __real__txe_thread_create(TX_THREAD *thread_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*entry_function)(ULONG entry_input),
+ ULONG entry_input, VOID *stack_start, ULONG stack_size, UINT priority,
+ UINT preempt_threshold, ULONG time_slice, UINT auto_start,
+ UINT thread_control_block_size);
+UINT __real__txe_thread_delete(TX_THREAD *thread_ptr);
+UINT __real__txe_thread_resume(TX_THREAD *thread_ptr);
+
+
+/* Determine whether the call now being made is the one the test asked to fail.
+ Only calls made while accounting is active are considered, which keeps the
+ objects created by tx_freertos_init() and by the harness out of the count. */
+static int txfr_should_fail(TXFR_INJECT_TARGET target)
+{
+ if((txfr_accounting == 0) || (txfr_inject_target != target))
+ {
+ return 0;
+ }
+
+ txfr_inject_call_index++;
+
+ return (txfr_inject_call_index == txfr_inject_fail_on) ? 1 : 0;
+}
+
+
+UINT __wrap__txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.byte_allocate++;
+ }
+
+ return __real__txe_byte_allocate(pool_ptr, memory_ptr, memory_size, wait_option);
+}
+
+
+UINT __wrap__txe_byte_release(VOID *memory_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.byte_release++;
+ }
+
+ return __real__txe_byte_release(memory_ptr);
+}
+
+
+UINT __wrap__txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_SEMAPHORE_CREATE) != 0)
+ {
+ txfr_counters.semaphore_create++;
+ return TX_SEMAPHORE_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.semaphore_create++;
+ }
+
+ return __real__txe_semaphore_create(semaphore_ptr, name_ptr, initial_count, semaphore_control_block_size);
+}
+
+
+UINT __wrap__txe_semaphore_delete(TX_SEMAPHORE *semaphore_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.semaphore_delete++;
+ }
+
+ return __real__txe_semaphore_delete(semaphore_ptr);
+}
+
+
+UINT __wrap__txe_mutex_create(TX_MUTEX *mutex_ptr, TX_NAME_CONST CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_MUTEX_CREATE) != 0)
+ {
+ txfr_counters.mutex_create++;
+ return TX_MUTEX_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.mutex_create++;
+ }
+
+ return __real__txe_mutex_create(mutex_ptr, name_ptr, inherit, mutex_control_block_size);
+}
+
+
+UINT __wrap__txe_mutex_delete(TX_MUTEX *mutex_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.mutex_delete++;
+ }
+
+ return __real__txe_mutex_delete(mutex_ptr);
+}
+
+
+UINT __wrap__txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, TX_NAME_CONST CHAR *name_ptr, UINT event_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_EVENT_FLAGS_CREATE) != 0)
+ {
+ txfr_counters.event_flags_create++;
+ return TX_GROUP_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.event_flags_create++;
+ }
+
+ return __real__txe_event_flags_create(group_ptr, name_ptr, event_control_block_size);
+}
+
+
+UINT __wrap__txe_event_flags_delete(TX_EVENT_FLAGS_GROUP *group_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.event_flags_delete++;
+ }
+
+ return __real__txe_event_flags_delete(group_ptr);
+}
+
+
+UINT __wrap__txe_timer_create(TX_TIMER *timer_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*expiration_function)(ULONG input),
+ ULONG expiration_input, ULONG initial_ticks, ULONG reschedule_ticks,
+ UINT auto_activate, UINT timer_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_TIMER_CREATE) != 0)
+ {
+ txfr_counters.timer_create++;
+ return TX_TIMER_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.timer_create++;
+ }
+
+ return __real__txe_timer_create(timer_ptr, name_ptr, expiration_function, expiration_input,
+ initial_ticks, reschedule_ticks, auto_activate, timer_control_block_size);
+}
+
+
+UINT __wrap__txe_timer_delete(TX_TIMER *timer_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.timer_delete++;
+ }
+
+ return __real__txe_timer_delete(timer_ptr);
+}
+
+
+UINT __wrap__txe_thread_create(TX_THREAD *thread_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*entry_function)(ULONG entry_input),
+ ULONG entry_input, VOID *stack_start, ULONG stack_size, UINT priority,
+ UINT preempt_threshold, ULONG time_slice, UINT auto_start,
+ UINT thread_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_THREAD_CREATE) != 0)
+ {
+ txfr_counters.thread_create++;
+ return TX_THREAD_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.thread_create++;
+ }
+
+ return __real__txe_thread_create(thread_ptr, name_ptr, entry_function, entry_input, stack_start,
+ stack_size, priority, preempt_threshold, time_slice, auto_start,
+ thread_control_block_size);
+}
+
+
+UINT __wrap__txe_thread_delete(TX_THREAD *thread_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.thread_delete++;
+ }
+
+ return __real__txe_thread_delete(thread_ptr);
+}
+
+
+UINT __wrap__txe_thread_resume(TX_THREAD *thread_ptr)
+{
+ if(txfr_should_fail(TXFR_INJECT_THREAD_RESUME) != 0)
+ {
+ txfr_counters.thread_resume++;
+ return TX_RESUME_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.thread_resume++;
+ }
+
+ return __real__txe_thread_resume(thread_ptr);
+}
+
+
+void txfr_test_account_start(TXFR_INJECT_TARGET target, int fail_on_call)
+{
+ TX_MEMSET(&txfr_counters, 0, sizeof(txfr_counters));
+ txfr_inject_target = target;
+ txfr_inject_fail_on = fail_on_call;
+ txfr_inject_call_index = 0;
+ txfr_accounting = 1;
+}
+
+
+void txfr_test_account_stop(TXFR_COUNTERS *p_counters)
+{
+ txfr_accounting = 0;
+ txfr_inject_target = TXFR_INJECT_NONE;
+
+ if(p_counters != NULL)
+ {
+ *p_counters = txfr_counters;
+ }
+}
+
+
+void txfr_test_check(const char *label, int condition)
+{
+ printf("%-52s %s\n", label, (condition != 0) ? "PASS" : "FAIL");
+
+ if(condition == 0)
+ {
+ txfr_failures++;
+ }
+}
+
+
+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)
+{
+ int object_create;
+ int object_delete;
+ int ok;
+
+ /* Only one kind of kernel object is exercised per check, so the totals
+ identify it unambiguously. */
+ object_create = p_counters->semaphore_create + p_counters->mutex_create +
+ p_counters->event_flags_create + p_counters->timer_create +
+ p_counters->thread_create;
+ object_delete = p_counters->semaphore_delete + p_counters->mutex_delete +
+ p_counters->event_flags_delete + p_counters->timer_delete +
+ p_counters->thread_delete;
+
+ ok = ((p_counters->byte_allocate == expect_allocate) &&
+ (p_counters->byte_release == expect_release) &&
+ (object_create == expect_object_create) &&
+ (object_delete == expect_object_delete)) ? 1 : 0;
+
+ printf("%-52s %s\n", label, (ok != 0) ? "PASS" : "FAIL");
+
+ if(ok == 0)
+ {
+ printf("%-52s got alloc=%d release=%d create=%d delete=%d\n", "",
+ p_counters->byte_allocate, p_counters->byte_release, object_create, object_delete);
+ printf("%-52s expected alloc=%d release=%d create=%d delete=%d\n", "",
+ expect_allocate, expect_release, expect_object_create, expect_object_delete);
+ txfr_failures++;
+ }
+}
+
+
+int txfr_test_failures(void)
+{
+ return txfr_failures;
+}
+
+
+/* Test thread. The byte pool operations under test require thread context. */
+
+static TX_THREAD txfr_test_thread;
+static ULONG txfr_test_stack[4096];
+
+
+static void txfr_test_thread_entry(ULONG id)
+{
+ (void)id;
+
+ txfr_test_body();
+
+ printf("\n%s\n", (txfr_failures == 0) ? "ALL CHECKS PASSED" : "THERE WERE FAILED CHECKS");
+ fflush(stdout);
+
+ /* The kernel owns the calling thread, so returning here would simply idle.
+ Leave the process with a status ctest can read. */
+ exit((txfr_failures == 0) ? 0 : 1);
+}
+
+
+void tx_application_define(void *first_unused_memory)
+{
+ UINT ret;
+
+ (void)first_unused_memory;
+
+ ret = tx_freertos_init();
+ if(ret != TX_SUCCESS)
+ {
+ printf("tx_freertos_init() failed with %u\n", ret);
+ exit(2);
+ }
+
+ ret = tx_thread_create(&txfr_test_thread, "txfr_test", txfr_test_thread_entry, 0u,
+ txfr_test_stack, sizeof(txfr_test_stack), 16u, 16u, 0u, TX_AUTO_START);
+ if(ret != TX_SUCCESS)
+ {
+ printf("tx_thread_create() failed with %u\n", ret);
+ exit(2);
+ }
+}
+
+
+int main(void)
+{
+ tx_kernel_enter();
+
+ return 0;
+}
diff --git a/test/freertos/regression/txfr_test_harness.h b/test/freertos/regression/txfr_test_harness.h
new file mode 100644
index 00000000..ffb6db0e
--- /dev/null
+++ b/test/freertos/regression/txfr_test_harness.h
@@ -0,0 +1,103 @@
+/***************************************************************************
+ * 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_THREAD_RESUME
+} 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;
+
+ /* Counted, but not part of the object totals checked below, since a
+ resume creates nothing. */
+ int thread_resume;
+} 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 */