summaryrefslogtreecommitdiff
path: root/test/freertos/regression
diff options
context:
space:
mode:
Diffstat (limited to 'test/freertos/regression')
-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
5 files changed, 893 insertions, 0 deletions
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 */