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/tx/thread_transition/threadx_thread_transition_configuration_test.c | |
| 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/tx/thread_transition/threadx_thread_transition_configuration_test.c')
| -rw-r--r-- | test/tx/thread_transition/threadx_thread_transition_configuration_test.c | 607 |
1 files changed, 607 insertions, 0 deletions
diff --git a/test/tx/thread_transition/threadx_thread_transition_configuration_test.c b/test/tx/thread_transition/threadx_thread_transition_configuration_test.c new file mode 100644 index 00000000..fe6acb7e --- /dev/null +++ b/test/tx/thread_transition/threadx_thread_transition_configuration_test.c @@ -0,0 +1,607 @@ +/*************************************************************************** + * 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 Test */ +/** */ +/** Thread lifecycle transition configuration coverage */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +/* The two configurations that matter to this change and that neither + regression tree builds are TX_NOT_INTERRUPTABLE and + TX_DISABLE_ERROR_CHECKING. This executable is built once for each + combination of them, against the four sources that change, and drives the + completion and termination transitions directly. + + Why each one needs its own build rather than a case in the in-tree test: + + - TX_NOT_INTERRUPTABLE selects a different transition. There the exit + notification callback runs with interrupts still disabled and the + interruptable suspension path -- which is what sets and clears the + transition marker in the configurations the trees do build -- is not + used at all, so the marker is set and cleared by explicit stores that + exist only in this configuration. A marker left set there would make a + normally completed thread permanently undeletable, which is the failure + mode worth more than a compile. + + - TX_DISABLE_ERROR_CHECKING removes the _txe_ wrappers, so the public + names bind straight to the core services. The refusal has to live in + the core services for exactly that reason, and the way to demonstrate it + is to call the public names in a build that has no wrappers. + + The scheduler is not linked. _tx_thread_system_ni_suspend and + _tx_thread_system_suspend are recorders, which is what makes the marker's + value at the moment of detachment directly observable -- and, in the + TX_NOT_INTERRUPTABLE completion path, the reason the marker has to be + cleared before the call rather than after it: on a real port that call + returns to the scheduler and never comes back. + + Everything here is straight-line: there are no threads, no timers and no + waits, so there is nothing that can hang. */ + +#include <stdio.h> +#include <string.h> + +/* tx_api.h is included without TX_SOURCE_CODE, so that the public service names + are mapped the way an application sees them: to the _txe_ wrappers with error + checking on, and straight to the core services with it off. Letting the header + make that choice is the point -- the refusal has to survive the choice. Only + then is TX_SOURCE_CODE defined, for the internal declarations the recorders + below implement. */ + +#include "tx_api.h" + +/* tx_thread_entry_exit_notify is both a service name and a TX_THREAD field name, + and it is the only name in this file that is both, so the mapping above would + rewrite the field assignment further down into a member that does not exist. + Dropping just that one mapping keeps the mapping that matters -- the one on the + three services this test calls -- while leaving the field reachable. The test + never calls the notify service; it plants the callback in the control block + directly, because the create path that would otherwise do it is not linked. */ + +#undef tx_thread_entry_exit_notify + +#define TX_SOURCE_CODE +#include "tx_thread.h" + + +/* Counters the force-included port shim's macros drive. */ + +unsigned int test_interrupt_disable_depth = 0; +unsigned int test_interrupt_disable_max_depth = 0; +unsigned int test_interrupt_restore_underflows = 0; +unsigned int test_delete_port_completion_count = 0; +unsigned int test_reset_port_completion_count = 0; + + +#define TEST_STACK_BYTES 512 + +static TX_THREAD victim; +static TX_THREAD other; +static UCHAR victim_stack[TEST_STACK_BYTES]; + +/* What the recorders saw. */ + +static unsigned int ni_suspend_calls = 0; +static UINT ni_suspend_suspending = 0xFFFFFFFFU; +static UINT ni_suspend_state = 0xFFFFFFFFU; +static unsigned int ni_suspend_depth = 0xFFFFFFFFU; + +static unsigned int suspend_calls = 0; +static UINT suspend_suspending = 0xFFFFFFFFU; + +static unsigned int stack_build_calls = 0; +static unsigned int preempt_check_calls = 0; +static unsigned int timer_deactivate_calls = 0; + +/* What the exit callback saw. */ + +static unsigned int callback_calls = 0; +static UINT callback_state = 0xFFFFFFFFU; +static UINT callback_suspending = 0xFFFFFFFFU; +static unsigned int callback_depth = 0xFFFFFFFFU; +static UINT callback_delete_status = 0xFFFFFFFFU; +static UINT callback_reset_status = 0xFFFFFFFFU; +static UINT callback_delete_completions = 0xFFFFFFFFU; +static UINT callback_reset_completions = 0xFFFFFFFFU; + +static unsigned int entry_calls = 0; + +static unsigned long expectations_checked = 0; +static unsigned long expectations_failed = 0; + + +static void expect(int condition, const char *what) +{ + + expectations_checked++; + + if (condition == 0) + { + expectations_failed++; + printf(" FAILED: %s\n", what); + } +} + + +static void expect_uint(UINT actual, UINT wanted, const char *what) +{ + + expectations_checked++; + + if (actual != wanted) + { + expectations_failed++; + printf(" FAILED: %s (got %lu, wanted %lu)\n", what, + (unsigned long) actual, (unsigned long) wanted); + } +} + + +/* ------------------------------------------------------------------------ */ +/* Recorders standing in for the scheduler. */ +/* ------------------------------------------------------------------------ */ + +VOID _tx_thread_system_ni_suspend(TX_THREAD *thread_ptr, ULONG wait_option) +{ + + (void) wait_option; + + ni_suspend_calls++; + ni_suspend_suspending = thread_ptr -> tx_thread_suspending; + ni_suspend_state = thread_ptr -> tx_thread_state; + ni_suspend_depth = test_interrupt_disable_depth; +} + + +VOID _tx_thread_system_suspend(TX_THREAD *thread_ptr) +{ + + suspend_calls++; + suspend_suspending = thread_ptr -> tx_thread_suspending; + + /* The interruptable suspension path is where the marker is cleared in the + configurations the regression trees build, so the recorder clears it as + the real one does, on the same condition. */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + thread_ptr -> tx_thread_suspending = TX_FALSE; + } + + /* It also decrements the preemption disable count, which its callers have + raised on its behalf. The recorder has to do the same, or the balance + this test asserts across the whole transition would be off by one for a + reason that belongs to the recorder rather than to the code under + test. */ + _tx_thread_preempt_disable--; +} + + +VOID _tx_thread_stack_build(TX_THREAD *thread_ptr, VOID (*function_ptr)(VOID)) +{ + + (void) thread_ptr; + (void) function_ptr; + + stack_build_calls++; +} + + +VOID _tx_thread_system_preempt_check(VOID) +{ + preempt_check_calls++; +} + + +/* _tx_thread_terminate deactivates the target's timeout timer, and + _txe_thread_reset refuses a call made from the timer thread. Neither the + timer component nor the timer thread is linked, so both are supplied here: + the deactivation as a recorder, and the timer thread as a control block that + is never the current thread, so the wrapper's check is exercised and takes + the not-the-timer-thread branch. */ + +VOID _tx_timer_system_deactivate(TX_TIMER_INTERNAL *timer_ptr) +{ + + (void) timer_ptr; + + timer_deactivate_calls++; +} + + +TX_THREAD _tx_timer_thread; + + +/* ------------------------------------------------------------------------ */ +/* The thread under test. */ +/* ------------------------------------------------------------------------ */ + +static void reset_records(void) +{ + + ni_suspend_calls = 0; + ni_suspend_suspending = 0xFFFFFFFFU; + ni_suspend_state = 0xFFFFFFFFU; + ni_suspend_depth = 0xFFFFFFFFU; + suspend_calls = 0; + suspend_suspending = 0xFFFFFFFFU; + stack_build_calls = 0; + preempt_check_calls = 0; + timer_deactivate_calls = 0; + callback_calls = 0; + callback_state = 0xFFFFFFFFU; + callback_suspending = 0xFFFFFFFFU; + callback_depth = 0xFFFFFFFFU; + callback_delete_status = 0xFFFFFFFFU; + callback_reset_status = 0xFFFFFFFFU; + callback_delete_completions = 0xFFFFFFFFU; + callback_reset_completions = 0xFFFFFFFFU; + entry_calls = 0; + + test_interrupt_disable_depth = 0; + test_interrupt_disable_max_depth = 0; + test_interrupt_restore_underflows = 0; + test_delete_port_completion_count = 0; + test_reset_port_completion_count = 0; +} + + +static VOID victim_entry(ULONG input) +{ + (void) input; + entry_calls++; +} + + +static VOID exit_notify(TX_THREAD *thread_ptr, UINT type) +{ + + if (type != TX_THREAD_EXIT) + { + return; + } + + callback_calls++; + callback_state = thread_ptr -> tx_thread_state; + callback_suspending = thread_ptr -> tx_thread_suspending; + callback_depth = test_interrupt_disable_depth; + + /* The public names. With error checking on these are the _txe_ wrappers; + with it off they are the core services. Either way the refusal has to + come back. */ + callback_delete_status = tx_thread_delete(thread_ptr); + callback_reset_status = tx_thread_reset(thread_ptr); + + /* A refused delete or reset must not have run the port's completion + processing, which on a real port tears the thread's port resources + down. */ + callback_delete_completions = test_delete_port_completion_count; + callback_reset_completions = test_reset_port_completion_count; +} + + +/* Put the victim's control block into the shape the kernel would have left it + in after tx_thread_create, for the fields the four sources under test read. + tx_thread_create itself is not linked, because the scheduler it resumes into + is not linked either. */ + +static void make_victim(UINT state) +{ + + memset(&victim, 0, sizeof(victim)); + memset(victim_stack, 0xEF, sizeof(victim_stack)); + + victim.tx_thread_id = TX_THREAD_ID; + victim.tx_thread_state = state; + victim.tx_thread_suspending = TX_FALSE; + victim.tx_thread_entry = victim_entry; + victim.tx_thread_entry_parameter = 0; +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* The field itself does not exist when notification is compiled out. */ + victim.tx_thread_entry_exit_notify = exit_notify; +#endif + victim.tx_thread_stack_start = victim_stack; + victim.tx_thread_stack_size = sizeof(victim_stack); + victim.tx_thread_priority = 16; + victim.tx_thread_preempt_threshold = 16; + victim.tx_thread_suspend_cleanup = TX_NULL; + + /* A created list with the victim and one other thread on it, so that + delete takes the link-up-the-neighbours branch rather than the + only-thread branch. */ + memset(&other, 0, sizeof(other)); + other.tx_thread_id = TX_THREAD_ID; + other.tx_thread_state = TX_SUSPENDED; + other.tx_thread_created_next = &victim; + other.tx_thread_created_previous = &victim; + + victim.tx_thread_created_next = &other; + victim.tx_thread_created_previous = &other; + + _tx_thread_created_ptr = &victim; + _tx_thread_created_count = 2; + + _tx_thread_current_ptr = &victim; + _tx_thread_execute_ptr = &victim; + _tx_thread_preempt_disable = 0; + _tx_thread_system_state = 0; +} + + +static void scenario_completion(void) +{ + + printf("Completion transition:\n"); + + reset_records(); + make_victim(TX_SUSPENDED); + + /* _tx_thread_shell_entry picks the thread up from the current pointer, + calls its entry function, and then runs the completion transition. */ + _tx_thread_shell_entry(); + + expect_uint((UINT) entry_calls, 1, "the entry function ran exactly once"); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect_uint((UINT) callback_calls, 1, "the exit callback ran exactly once"); + + /* The terminal state is published before the callback. */ + expect_uint(callback_state, TX_COMPLETED, "the callback saw TX_COMPLETED"); + + /* And the transition is marked while the callback runs, in this + configuration as in the interruptable one. */ + expect_uint(callback_suspending, TX_TRUE, "the callback saw the transition marked"); + +#ifdef TX_NOT_INTERRUPTABLE + + /* In this configuration the callback runs inside the interrupt lock. */ + expect(callback_depth > 0, "the callback ran with interrupts disabled"); +#else + expect_uint((UINT) callback_depth, 0, "the callback ran with interrupts enabled"); +#endif + + /* The two refusals, from whichever of the wrapper and the core service + this build binds the public name to. */ + expect_uint(callback_delete_status, TX_DELETE_ERROR, "the callback's delete was refused"); + expect_uint(callback_reset_status, TX_NOT_DONE, "the callback's reset was refused"); + + /* A refused delete or reset ran no port completion processing. */ + expect_uint(callback_delete_completions, 0, "the refused delete ran no port completion"); + expect_uint(callback_reset_completions, 0, "the refused reset ran no port completion"); +#endif + + /* The detachment saw the marker in the state that keeps the thread + deletable afterwards. This is the failure mode the configuration exists + to catch: on a real port the suspension call below returns to the + scheduler and does not come back, so a marker cleared after it rather + than before it would stay set for ever and the completed thread could + never be deleted. */ +#ifdef TX_NOT_INTERRUPTABLE + + expect_uint((UINT) ni_suspend_calls, 1, "the non-interruptable suspension ran once"); + expect_uint(ni_suspend_state, TX_COMPLETED, "it was entered with the terminal state"); + expect_uint(ni_suspend_suspending, TX_FALSE, + "the marker was already cleared when the suspension was entered"); + expect(ni_suspend_depth > 0, "the suspension was entered with interrupts disabled"); + expect_uint((UINT) suspend_calls, 0, "the interruptable suspension was not used"); +#else + + expect_uint((UINT) suspend_calls, 1, "the interruptable suspension ran once"); + expect_uint(suspend_suspending, TX_TRUE, + "the marker was still set when the suspension was entered, for it to clear"); + expect_uint((UINT) ni_suspend_calls, 0, "the non-interruptable suspension was not used"); +#endif + + /* Either way the thread is deletable once the transition has finished. */ + expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear afterwards"); + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "the completed thread deletes afterwards"); + expect_uint((UINT) test_delete_port_completion_count, 1, + "the successful delete ran the port completion once"); + + /* And the interrupt lock is balanced across the whole transition. */ + expect_uint((UINT) test_interrupt_disable_depth, 0, "the interrupt lock is balanced"); + expect_uint((UINT) test_interrupt_restore_underflows, 0, "no unmatched interrupt restore"); +} + + +static void scenario_terminate_ready(void) +{ + + printf("Termination of a ready thread:\n"); + + reset_records(); + make_victim(TX_READY); + + /* Terminate a target that is not the current thread, so that the reset the + callback attempts is refused by the transition check alone rather than + by the current-thread rule. */ + _tx_thread_current_ptr = &other; + + expect_uint(tx_thread_terminate(&victim), TX_SUCCESS, "the termination succeeded"); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect_uint((UINT) callback_calls, 1, "the exit callback ran exactly once"); + expect_uint(callback_state, TX_TERMINATED, "the callback saw TX_TERMINATED"); + expect_uint(callback_suspending, TX_TRUE, "the callback saw the transition marked"); + expect_uint(callback_delete_status, TX_DELETE_ERROR, "the callback's delete was refused"); + expect_uint(callback_reset_status, TX_NOT_DONE, "the callback's reset was refused"); + expect_uint(callback_reset_completions, 0, "the refused reset ran no port completion"); + expect_uint((UINT) stack_build_calls, 0, "the refused reset rebuilt no stack"); +#endif + +#ifdef TX_NOT_INTERRUPTABLE + + expect_uint((UINT) ni_suspend_calls, 1, "the non-interruptable suspension ran once"); + expect(callback_depth > 0, "the callback ran with interrupts disabled"); +#else + + expect_uint((UINT) suspend_calls, 1, "the interruptable suspension ran once"); +#endif + + /* The marker is cleared in the common tail, after the callback and after + the mutex-release processing. */ + expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear afterwards"); + expect_uint(victim.tx_thread_state, TX_TERMINATED, "the state is terminal afterwards"); + expect_uint((UINT) _tx_thread_preempt_disable, 0, "the preempt disable count is balanced"); + expect_uint((UINT) test_interrupt_disable_depth, 0, "the interrupt lock is balanced"); + expect_uint((UINT) test_interrupt_restore_underflows, 0, "no unmatched interrupt restore"); + + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "the terminated thread deletes afterwards"); +} + + +static void scenario_terminate_suspended(void) +{ + +UINT reset_status; + + + printf("Termination of a suspended thread:\n"); + + reset_records(); + make_victim(TX_SUSPENDED); + _tx_thread_current_ptr = &other; + + expect_uint(tx_thread_terminate(&victim), TX_SUCCESS, "the termination succeeded"); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect_uint((UINT) callback_calls, 1, "the exit callback ran exactly once"); + expect_uint(callback_state, TX_TERMINATED, "the callback saw TX_TERMINATED"); + + /* The non-ready branch used to clear the marker before this point. */ + expect_uint(callback_suspending, TX_TRUE, "the callback saw the transition marked"); + expect_uint(callback_delete_status, TX_DELETE_ERROR, "the callback's delete was refused"); + expect_uint(callback_reset_status, TX_NOT_DONE, "the callback's reset was refused"); + expect_uint((UINT) stack_build_calls, 0, "the refused reset rebuilt no stack"); +#endif + + /* This branch never calls a suspension routine: the thread was already off + the ready list. */ + expect_uint((UINT) ni_suspend_calls, 0, "no non-interruptable suspension on this branch"); + expect_uint((UINT) suspend_calls, 0, "no interruptable suspension on this branch"); + + expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear afterwards"); + expect_uint((UINT) _tx_thread_preempt_disable, 0, "the preempt disable count is balanced"); + expect_uint((UINT) test_interrupt_disable_depth, 0, "the interrupt lock is balanced"); + expect_uint((UINT) test_interrupt_restore_underflows, 0, "no unmatched interrupt restore"); + + /* The documented lifecycle is available afterwards, on this branch too. */ + reset_status = tx_thread_reset(&victim); + expect_uint(reset_status, TX_SUCCESS, "the terminated thread resets afterwards"); + expect_uint(victim.tx_thread_state, TX_SUSPENDED, "the reset left it suspended"); + expect_uint((UINT) stack_build_calls, 1, "the successful reset rebuilt the stack once"); + expect_uint((UINT) test_reset_port_completion_count, 1, + "the successful reset ran the port completion once"); + + expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "a reset thread is not deletable"); + expect_uint(tx_thread_terminate(&victim), TX_SUCCESS, "it terminates again"); + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "and then deletes"); +} + + +static void scenario_marker_not_set_outside_transition(void) +{ + + printf("Outside a transition:\n"); + + reset_records(); + make_victim(TX_SUSPENDED); + _tx_thread_current_ptr = &other; + + /* Neither state is terminal, so both services refuse for the reason they + always did, and the marker plays no part. */ + expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear on a live thread"); + expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "delete refuses a suspended thread"); + expect_uint(tx_thread_reset(&victim), TX_NOT_DONE, "reset refuses a suspended thread"); + + /* A terminal state with the marker clear is the ordinary deletable case, + which is what the refusal must not have broken. */ + victim.tx_thread_state = TX_COMPLETED; + victim.tx_thread_suspending = TX_FALSE; + expect_uint(tx_thread_reset(&victim), TX_SUCCESS, "reset accepts a settled completed thread"); + + victim.tx_thread_state = TX_TERMINATED; + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "delete accepts a settled terminated thread"); + + /* And a terminal state with the marker set is the new refusal, tested + directly rather than through a transition, so that both services are + covered on both terminal states. */ + make_victim(TX_COMPLETED); + victim.tx_thread_suspending = TX_TRUE; + _tx_thread_current_ptr = &other; + expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "delete refuses a marked completed thread"); + expect_uint(tx_thread_reset(&victim), TX_NOT_DONE, "reset refuses a marked completed thread"); + + victim.tx_thread_state = TX_TERMINATED; + expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "delete refuses a marked terminated thread"); + expect_uint(tx_thread_reset(&victim), TX_NOT_DONE, "reset refuses a marked terminated thread"); + + /* Clearing the marker makes both available again, which is the whole + claim about the refusal being transient. */ + victim.tx_thread_suspending = TX_FALSE; + expect_uint(tx_thread_reset(&victim), TX_SUCCESS, "clearing the marker restores reset"); + victim.tx_thread_state = TX_TERMINATED; + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "clearing the marker restores delete"); +} + + +int main(void) +{ + + printf("Thread lifecycle transition configuration test: "); + +#ifdef TX_NOT_INTERRUPTABLE + printf("TX_NOT_INTERRUPTABLE "); +#else + printf("interruptable "); +#endif +#ifdef TX_DISABLE_ERROR_CHECKING + printf("TX_DISABLE_ERROR_CHECKING "); +#else + printf("error checking "); +#endif +#ifdef TX_DISABLE_NOTIFY_CALLBACKS + printf("TX_DISABLE_NOTIFY_CALLBACKS"); +#else + printf("notify callbacks"); +#endif + printf("\n"); + + scenario_completion(); + scenario_terminate_ready(); + scenario_terminate_suspended(); + scenario_marker_not_set_outside_transition(); + + printf("%lu of %lu expectations passed.\n", + expectations_checked - expectations_failed, expectations_checked); + + if (expectations_failed != 0) + { + printf("FAILED: %lu expectations.\n", expectations_failed); + return(1); + } + + printf("SUCCESS!\n"); + return(0); +} |
