diff options
| author | Frédéric Desbiens <[email protected]> | 2026-10-02 10:42:44 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-10-02 10:42:44 -0400 |
| commit | 93387b0a60382a29579bbad5045c68328e3751df (patch) | |
| tree | 42be790d602cb76f55090e5a7c418df7ea2b3428 /test/freertos | |
| parent | 44d7c95c582d415c4ad84527180b29c93c3bf664 (diff) | |
| parent | 28281cd5647d4671236ca08d19bd18ed15b2265a (diff) | |
Merge pull request #783 from eclipse-threadx/devv6.5.2.202603_rel
Merged changes ahead of the v6.5.2.202603 release
Diffstat (limited to 'test/freertos')
| -rw-r--r-- | test/freertos/cmake/CMakeLists.txt | 78 | ||||
| -rw-r--r-- | test/freertos/cmake/regression/CMakeLists.txt | 84 | ||||
| -rwxr-xr-x | test/freertos/cmake/run.sh | 135 | ||||
| -rw-r--r-- | test/freertos/fixtures/FreeRTOSConfig.h | 66 | ||||
| -rw-r--r-- | test/freertos/fixtures/tx_user.h | 35 | ||||
| -rw-r--r-- | test/freertos/readme.md | 86 | ||||
| -rw-r--r-- | test/freertos/regression/txfr_queue_create_test.c | 132 | ||||
| -rw-r--r-- | test/freertos/regression/txfr_sync_create_test.c | 129 | ||||
| -rw-r--r-- | test/freertos/regression/txfr_task_create_test.c | 145 | ||||
| -rw-r--r-- | test/freertos/regression/txfr_test_harness.c | 384 | ||||
| -rw-r--r-- | test/freertos/regression/txfr_test_harness.h | 103 |
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 */ |
