Files
threadx/test/freertos/regression/txfr_test_harness.c
T
Frédéric Desbiens f3df5f9dde Added a regression suite for the FreeRTOS compatibility layer (#583)
* Fixed the resource leaks on the xQueueCreate error paths

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

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

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

Fixes https://github.com/eclipse-threadx/threadx/issues/570

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

* Added a regression suite for the FreeRTOS compatibility layer

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

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

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

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

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-08-09 08:03:16 -04:00

367 lines
11 KiB
C

/***************************************************************************
* 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, 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, 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, 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, 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, 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);
/* 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, 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, 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, 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, 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, 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);
}
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;
}