diff --git a/common/src/tx_thread_delete.c b/common/src/tx_thread_delete.c index 510f6565e..e73fdd319 100644 --- a/common/src/tx_thread_delete.c +++ b/common/src/tx_thread_delete.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -95,6 +97,31 @@ UINT status; } } + /* The state is terminal, but a terminal state on its own is not authorization to + release the control block. Both paths that produce one -- thread completion in + _tx_thread_shell_entry and thread termination in _tx_thread_terminate -- publish + TX_COMPLETED or TX_TERMINATED, and then run this thread's exit notification + callback, before the thread has been detached from the ready list and before + those services have finished with the pointer they hold to it. The suspending + flag is set for exactly that interval, so a thread whose flag is still set is + part-way through the transition. */ + if (status == TX_SUCCESS) + { + + /* Is the completion or termination transition still in progress? */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + + /* Restore interrupts. */ + TX_RESTORE + + /* Yes, refuse the delete rather than unlink a thread the scheduler is + still holding. The condition is transient: the caller may retry once + the transition has finished. */ + status = TX_DELETE_ERROR; + } + } + /* Determine if the delete operation is okay. */ if (status == TX_SUCCESS) { diff --git a/common/src/tx_thread_reset.c b/common/src/tx_thread_reset.c index da56daf57..e87de26be 100644 --- a/common/src/tx_thread_reset.c +++ b/common/src/tx_thread_reset.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -104,6 +106,28 @@ UINT status; status = TX_NOT_DONE; } } + + /* The state is terminal, but a terminal state on its own is not authorization + to rebuild the stack and move the thread back to TX_SUSPENDED. Both paths + that produce one -- thread completion in _tx_thread_shell_entry and thread + termination in _tx_thread_terminate -- publish TX_COMPLETED or + TX_TERMINATED, and then run this thread's exit notification callback, before + the thread has been detached from the ready list. The suspending flag is + set for exactly that interval. Resetting inside it would also defeat the + protection _tx_thread_system_resume relies on, which refuses to cancel a + suspension only while the state is still terminal. */ + if (status == TX_SUCCESS) + { + + /* Is the completion or termination transition still in progress? */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + + /* Yes, refuse the reset. The condition is transient: the caller may + retry once the transition has finished. */ + status = TX_NOT_DONE; + } + } } /* Is the request valid? */ diff --git a/common/src/tx_thread_shell_entry.c b/common/src/tx_thread_shell_entry.c index 45fe9d6da..c079f18e1 100644 --- a/common/src/tx_thread_shell_entry.c +++ b/common/src/tx_thread_shell_entry.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -134,6 +136,17 @@ VOID (*entry_exit_notify)(TX_THREAD *notify_thread_ptr, UINT type); #ifdef TX_NOT_INTERRUPTABLE + /* Set the suspending flag, so that the completion transition is marked as in + progress for the same interval it is marked in the interruptable + configuration below. Nothing in this configuration needs the flag to cancel + an interrupted suspension -- interrupts stay disabled through the whole + transition -- but the notification callback and the completion extension + below are application code, reached by a direct call, and interrupt lockout + does not stop either of them from calling a thread lifecycle service on this + same control block. The flag is what _tx_thread_delete and _tx_thread_reset + test in order to refuse one. */ + thread_ptr -> tx_thread_suspending = TX_TRUE; + #ifndef TX_DISABLE_NOTIFY_CALLBACKS /* Determine if an application callback routine is specified. */ @@ -148,6 +161,15 @@ VOID (*entry_exit_notify)(TX_THREAD *notify_thread_ptr, UINT type); /* Perform any additional activities for tool or user purpose. */ TX_THREAD_COMPLETED_EXTENSION(thread_ptr) + /* Clear the suspending flag. It has to be cleared here rather than after the + call below, because _tx_thread_system_ni_suspend returns to the scheduler for + a thread that is the current thread, which this one is, and so does not come + back. Clearing it afterwards would leave a normally completed thread marked + as transitioning for ever and therefore permanently undeletable. Clearing it + here loses nothing: everything from this point to the detachment runs with + interrupts disabled and calls no application code. */ + thread_ptr -> tx_thread_suspending = TX_FALSE; + /* Call actual non-interruptable thread suspension routine. */ _tx_thread_system_ni_suspend(thread_ptr, ((ULONG) 0)); diff --git a/common/src/tx_thread_terminate.c b/common/src/tx_thread_terminate.c index 86e34baeb..05ec91a1f 100644 --- a/common/src/tx_thread_terminate.c +++ b/common/src/tx_thread_terminate.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -133,6 +135,16 @@ ULONG suspension_sequence; #ifdef TX_NOT_INTERRUPTABLE + /* Set the suspending flag, so that the termination transition is marked + as in progress for the same interval it is marked in the interruptable + configuration below. Nothing in this configuration needs the flag to + cancel an interrupted suspension -- interrupts stay disabled through + the whole transition -- but the notification callback below is + application code, reached by a direct call, and interrupt lockout does + not stop it from calling a thread lifecycle service on this same + control block. The flag is cleared in the common tail below. */ + thread_ptr -> tx_thread_suspending = TX_TRUE; + #ifndef TX_DISABLE_NOTIFY_CALLBACKS /* Determine if an application callback routine is specified. */ @@ -225,20 +237,13 @@ ULONG suspension_sequence; (suspend_cleanup)(thread_ptr, suspension_sequence); } -#ifndef TX_NOT_INTERRUPTABLE - - /* Disable interrupts. */ - TX_DISABLE -#endif - - /* Clear the suspending flag. */ - thread_ptr -> tx_thread_suspending = TX_FALSE; - -#ifndef TX_NOT_INTERRUPTABLE - - /* Restore interrupts. */ - TX_RESTORE -#endif + /* The suspending flag deliberately stays set here. It used to be + cleared at this point, which left the terminated extension and the + notification callback below -- both application code -- free to delete + or reset this control block while this service still held a pointer to + it and still had mutex-release processing to do. It is now cleared + once, in the common tail below, after the last dereference of the + target. */ /* Perform any additional activities for tool or user purpose. */ TX_THREAD_TERMINATED_EXTENSION(thread_ptr) @@ -282,6 +287,20 @@ ULONG suspension_sequence; TX_DISABLE #endif + /* Clear the suspending flag. This is the one place the end of the + termination transition is published, and it is reached from both branches + above, after the notification callback and after the mutex-release + processing, which is the last thing in this service to dereference the + target. Until this store, _tx_thread_delete and _tx_thread_reset refuse + the target. In the interruptable ready branch the flag is already false, + because _tx_thread_system_suspend cleared it when it detached the thread, + so this is a second store of a value the flag already holds; that is + cheaper than testing for it and it keeps the transition to a single clear + site. No suspension-initiating service can have set it again in between: + every one of them acts on a thread that is ready or suspended, and this + thread is terminated. */ + thread_ptr -> tx_thread_suspending = TX_FALSE; + /* Enable preemption. */ _tx_thread_preempt_disable--; diff --git a/common_smp/src/tx_thread_delete.c b/common_smp/src/tx_thread_delete.c index 510f6565e..e73fdd319 100644 --- a/common_smp/src/tx_thread_delete.c +++ b/common_smp/src/tx_thread_delete.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -95,6 +97,31 @@ UINT status; } } + /* The state is terminal, but a terminal state on its own is not authorization to + release the control block. Both paths that produce one -- thread completion in + _tx_thread_shell_entry and thread termination in _tx_thread_terminate -- publish + TX_COMPLETED or TX_TERMINATED, and then run this thread's exit notification + callback, before the thread has been detached from the ready list and before + those services have finished with the pointer they hold to it. The suspending + flag is set for exactly that interval, so a thread whose flag is still set is + part-way through the transition. */ + if (status == TX_SUCCESS) + { + + /* Is the completion or termination transition still in progress? */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + + /* Restore interrupts. */ + TX_RESTORE + + /* Yes, refuse the delete rather than unlink a thread the scheduler is + still holding. The condition is transient: the caller may retry once + the transition has finished. */ + status = TX_DELETE_ERROR; + } + } + /* Determine if the delete operation is okay. */ if (status == TX_SUCCESS) { diff --git a/common_smp/src/tx_thread_reset.c b/common_smp/src/tx_thread_reset.c index da56daf57..e87de26be 100644 --- a/common_smp/src/tx_thread_reset.c +++ b/common_smp/src/tx_thread_reset.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -104,6 +106,28 @@ UINT status; status = TX_NOT_DONE; } } + + /* The state is terminal, but a terminal state on its own is not authorization + to rebuild the stack and move the thread back to TX_SUSPENDED. Both paths + that produce one -- thread completion in _tx_thread_shell_entry and thread + termination in _tx_thread_terminate -- publish TX_COMPLETED or + TX_TERMINATED, and then run this thread's exit notification callback, before + the thread has been detached from the ready list. The suspending flag is + set for exactly that interval. Resetting inside it would also defeat the + protection _tx_thread_system_resume relies on, which refuses to cancel a + suspension only while the state is still terminal. */ + if (status == TX_SUCCESS) + { + + /* Is the completion or termination transition still in progress? */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + + /* Yes, refuse the reset. The condition is transient: the caller may + retry once the transition has finished. */ + status = TX_NOT_DONE; + } + } } /* Is the request valid? */ diff --git a/common_smp/src/tx_thread_shell_entry.c b/common_smp/src/tx_thread_shell_entry.c index 45fe9d6da..c079f18e1 100644 --- a/common_smp/src/tx_thread_shell_entry.c +++ b/common_smp/src/tx_thread_shell_entry.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -134,6 +136,17 @@ VOID (*entry_exit_notify)(TX_THREAD *notify_thread_ptr, UINT type); #ifdef TX_NOT_INTERRUPTABLE + /* Set the suspending flag, so that the completion transition is marked as in + progress for the same interval it is marked in the interruptable + configuration below. Nothing in this configuration needs the flag to cancel + an interrupted suspension -- interrupts stay disabled through the whole + transition -- but the notification callback and the completion extension + below are application code, reached by a direct call, and interrupt lockout + does not stop either of them from calling a thread lifecycle service on this + same control block. The flag is what _tx_thread_delete and _tx_thread_reset + test in order to refuse one. */ + thread_ptr -> tx_thread_suspending = TX_TRUE; + #ifndef TX_DISABLE_NOTIFY_CALLBACKS /* Determine if an application callback routine is specified. */ @@ -148,6 +161,15 @@ VOID (*entry_exit_notify)(TX_THREAD *notify_thread_ptr, UINT type); /* Perform any additional activities for tool or user purpose. */ TX_THREAD_COMPLETED_EXTENSION(thread_ptr) + /* Clear the suspending flag. It has to be cleared here rather than after the + call below, because _tx_thread_system_ni_suspend returns to the scheduler for + a thread that is the current thread, which this one is, and so does not come + back. Clearing it afterwards would leave a normally completed thread marked + as transitioning for ever and therefore permanently undeletable. Clearing it + here loses nothing: everything from this point to the detachment runs with + interrupts disabled and calls no application code. */ + thread_ptr -> tx_thread_suspending = TX_FALSE; + /* Call actual non-interruptable thread suspension routine. */ _tx_thread_system_ni_suspend(thread_ptr, ((ULONG) 0)); diff --git a/common_smp/src/tx_thread_terminate.c b/common_smp/src/tx_thread_terminate.c index 86e34baeb..05ec91a1f 100644 --- a/common_smp/src/tx_thread_terminate.c +++ b/common_smp/src/tx_thread_terminate.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -133,6 +135,16 @@ ULONG suspension_sequence; #ifdef TX_NOT_INTERRUPTABLE + /* Set the suspending flag, so that the termination transition is marked + as in progress for the same interval it is marked in the interruptable + configuration below. Nothing in this configuration needs the flag to + cancel an interrupted suspension -- interrupts stay disabled through + the whole transition -- but the notification callback below is + application code, reached by a direct call, and interrupt lockout does + not stop it from calling a thread lifecycle service on this same + control block. The flag is cleared in the common tail below. */ + thread_ptr -> tx_thread_suspending = TX_TRUE; + #ifndef TX_DISABLE_NOTIFY_CALLBACKS /* Determine if an application callback routine is specified. */ @@ -225,20 +237,13 @@ ULONG suspension_sequence; (suspend_cleanup)(thread_ptr, suspension_sequence); } -#ifndef TX_NOT_INTERRUPTABLE - - /* Disable interrupts. */ - TX_DISABLE -#endif - - /* Clear the suspending flag. */ - thread_ptr -> tx_thread_suspending = TX_FALSE; - -#ifndef TX_NOT_INTERRUPTABLE - - /* Restore interrupts. */ - TX_RESTORE -#endif + /* The suspending flag deliberately stays set here. It used to be + cleared at this point, which left the terminated extension and the + notification callback below -- both application code -- free to delete + or reset this control block while this service still held a pointer to + it and still had mutex-release processing to do. It is now cleared + once, in the common tail below, after the last dereference of the + target. */ /* Perform any additional activities for tool or user purpose. */ TX_THREAD_TERMINATED_EXTENSION(thread_ptr) @@ -282,6 +287,20 @@ ULONG suspension_sequence; TX_DISABLE #endif + /* Clear the suspending flag. This is the one place the end of the + termination transition is published, and it is reached from both branches + above, after the notification callback and after the mutex-release + processing, which is the last thing in this service to dereference the + target. Until this store, _tx_thread_delete and _tx_thread_reset refuse + the target. In the interruptable ready branch the flag is already false, + because _tx_thread_system_suspend cleared it when it detached the thread, + so this is a second store of a value the flag already holds; that is + cheaper than testing for it and it keeps the transition to a single clear + site. No suspension-initiating service can have set it again in between: + every one of them acts on a thread that is ready or suspended, and this + thread is terminated. */ + thread_ptr -> tx_thread_suspending = TX_FALSE; + /* Enable preemption. */ _tx_thread_preempt_disable--; diff --git a/test/smp/cmake/regression/CMakeLists.txt b/test/smp/cmake/regression/CMakeLists.txt index af33ca3f0..dd1d48a30 100644 --- a/test/smp/cmake/regression/CMakeLists.txt +++ b/test/smp/cmake/regression/CMakeLists.txt @@ -115,6 +115,8 @@ set(regression_test_cases ${SOURCE_DIR}/threadx_thread_sleep_terminate_test.c ${SOURCE_DIR}/threadx_thread_stack_checking_test.c ${SOURCE_DIR}/threadx_thread_terminate_delete_test.c + ${SOURCE_DIR}/threadx_thread_exit_callback_transition_test.c + ${SOURCE_DIR}/threadx_smp_thread_exit_callback_remote_test.c ${SOURCE_DIR}/threadx_thread_time_slice_change_test.c ${SOURCE_DIR}/threadx_thread_wait_abort_and_isr_test.c ${SOURCE_DIR}/threadx_thread_wait_abort_test.c diff --git a/test/smp/regression/testcontrol.c b/test/smp/regression/testcontrol.c index c0ba34636..59d8e69a7 100644 --- a/test/smp/regression/testcontrol.c +++ b/test/smp/regression/testcontrol.c @@ -240,6 +240,8 @@ void threadx_thread_simple_sleep_non_clear_application_define(void *); void threadx_thread_sleep_for_100ticks_application_define(void *); void threadx_thread_multiple_sleep_application_define(void *); void threadx_thread_terminate_delete_application_define(void *); +void threadx_thread_exit_callback_transition_application_define(void *); +void threadx_smp_thread_exit_callback_remote_application_define(void *); void threadx_thread_preemption_change_application_define(void *); void threadx_thread_priority_change_application_define(void *); void threadx_thread_time_slice_change_application_define(void *); @@ -384,6 +386,8 @@ TEST_ENTRY test_control_tests[] = threadx_thread_sleep_for_100ticks_application_define, threadx_thread_multiple_sleep_application_define, threadx_thread_terminate_delete_application_define, + threadx_thread_exit_callback_transition_application_define, + threadx_smp_thread_exit_callback_remote_application_define, threadx_thread_priority_change_application_define, diff --git a/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c b/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c new file mode 100644 index 000000000..488c28226 --- /dev/null +++ b/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c @@ -0,0 +1,514 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* Define the ThreadX SMP thread exit callback remote-core termination test. + + This is the SMP-specific half of the exit-callback transition coverage. The + deterministic cases are in threadx_thread_exit_callback_transition_test, + which confines every thread it drives to core 0 precisely so that they + cannot be made flaky by concurrency; this test supplies the case that needs + it. + + The victim is excluded from every core but core 1 and spins there without + relinquishing, so it is genuinely executing on another core when the + controller -- excluded to core 0 -- terminates it. _tx_thread_terminate + publishes TX_TERMINATED, releases the global SMP protection at TX_RESTORE, + and calls the victim's exit notification callback in the controller's + context on core 0. On an unfixed kernel a callback-side reset of the victim + succeeds at that point, and _tx_thread_reset both memsets the victim's stack + to the fill pattern and rebuilds its initial frame -- over the stack a live + core is running on. + + What is measured: + + - that the victim really was mapped to and executing on another core when + termination began, which is the precondition; if it cannot be + established within a bounded wait the test says so and drops only the + assertions that depend on it, rather than reporting a pass it did not + earn or spinning until something changes, + - that the callback-side delete and reset are refused, from the wrapper + and from the core service alike, + - that the victim's stack is untouched, checked with a sentinel written + into the unused low end of the stack, which a reset's fill would erase, + - that after the service returns the victim is in no core's execute slot + and on no ready list. + + Every wait is bounded and every list walk is bounded, so neither a failing + kernel nor an unlucky schedule can turn this into a hang. */ + +#include +#include "tx_api.h" +#include "tx_thread.h" + + +#define CONTROLLER_PRIORITY 10 +#define VICTIM_PRIORITY 20 + +/* The core the victim is pinned to, and the exclusion maps that pin it there + and the controller to core 0. */ + +#define VICTIM_CORE 1 +#define EXCLUDE_ALL_BUT_CORE_0 0xE +#define EXCLUDE_ALL_BUT_CORE_1 0xD + +/* Bound on the wait for the victim to be mapped to its core. */ + +#define REMOTE_WAIT_TICKS 200 + +/* Sentinel written into the unused low end of the victim's stack. A reset + memsets the whole stack to the fill pattern before rebuilding the frame, so + the sentinel is gone if a reset ran. Its offset keeps it clear of both the + frame a downward-growing stack builds at the top and the stack-checking + pattern the kernel inspects at the very bottom. */ + +#define SENTINEL_OFFSET 64 +#define SENTINEL_WORDS 8 +#define SENTINEL_VALUE 0xA5A5A5A5UL + +#define READY_RING_LIMIT 32 + +#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL + +#define FAILED_TAG_LIMIT 16 + + +static TX_THREAD controller; +static TX_THREAD victim; + +static CHAR *victim_stack; + +static volatile ULONG victim_spins = 0; +static volatile UINT victim_run = TX_FALSE; + +static UINT probe_state; +static UINT probe_suspending; +static UINT probe_core_mapped; +static UINT probe_delete_status = PROBE_NOT_ATTEMPTED; +static UINT probe_core_delete_status = PROBE_NOT_ATTEMPTED; +static UINT probe_reset_status = PROBE_NOT_ATTEMPTED; +static UINT probe_core_reset_status = PROBE_NOT_ATTEMPTED; +static UINT probe_ran = TX_FALSE; +static UINT remote_confirmed = TX_FALSE; +static UINT sentinel_intact_after = TX_FALSE; + +static unsigned long expectations_checked = 0; +static unsigned long expectations_failed = 0; +static UINT first_failure = 0; +static UINT failed_tags[FAILED_TAG_LIMIT]; +static UINT failed_tag_count = 0; + + +/* Define thread prototypes. */ + +static void controller_entry(ULONG thread_input); +static void victim_entry(ULONG thread_input); + + +/* Prototype for test control return. */ + +void test_control_return(UINT status); + + +static void expect(UINT condition, UINT tag) +{ + + expectations_checked++; + + if (condition != TX_TRUE) + { + + expectations_failed++; + + if (first_failure == 0) + { + first_failure = tag; + } + + if (failed_tag_count < FAILED_TAG_LIMIT) + { + failed_tags[failed_tag_count] = tag; + failed_tag_count++; + } + } +} + + +static void expect_status(UINT actual, UINT wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void write_sentinel(void) +{ + +ULONG *words; +UINT index; + + + words = (ULONG *) (victim_stack + SENTINEL_OFFSET); + + for (index = 0; index < SENTINEL_WORDS; index++) + { + words[index] = SENTINEL_VALUE; + } +} + + +static UINT sentinel_intact(void) +{ + +ULONG *words; +UINT index; +UINT intact; + + + words = (ULONG *) (victim_stack + SENTINEL_OFFSET); + intact = TX_TRUE; + + for (index = 0; index < SENTINEL_WORDS; index++) + { + if (words[index] != SENTINEL_VALUE) + { + intact = TX_FALSE; + } + } + + return(intact); +} + + +/* Report whether thread_ptr appears in any core's execute slot or on any ready + list. Both walks are bounded. */ + +static UINT fully_detached(TX_THREAD *thread_ptr) +{ + +TX_INTERRUPT_SAVE_AREA + +TX_THREAD *walk_ptr; +TX_THREAD *head_ptr; +UINT priority; +UINT steps; +UINT core; +UINT detached; + + + detached = TX_TRUE; + + TX_DISABLE + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + if (_tx_thread_execute_ptr[core] == thread_ptr) + { + detached = TX_FALSE; + } + } + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + + head_ptr = _tx_thread_priority_list[priority]; + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT)) + { + + if (walk_ptr == thread_ptr) + { + detached = TX_FALSE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + } + + TX_RESTORE + + return(detached); +} + + +static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type) +{ + + if (thread_ptr != &victim) + { + return; + } + + if (type != TX_THREAD_EXIT) + { + return; + } + + probe_ran = TX_TRUE; + probe_state = thread_ptr -> tx_thread_state; + probe_suspending = thread_ptr -> tx_thread_suspending; + probe_core_mapped = thread_ptr -> tx_thread_smp_core_mapped; + + /* The delete first, then the reset. Each core service is called only if + its wrapper already refused, so that a kernel which allows the operation + is not asked to perform it twice. */ + + probe_delete_status = tx_thread_delete(thread_ptr); + + if (probe_delete_status != TX_SUCCESS) + { + + probe_core_delete_status = _tx_thread_delete(thread_ptr); + + if (probe_core_delete_status != TX_SUCCESS) + { + + probe_reset_status = tx_thread_reset(thread_ptr); + + if (probe_reset_status != TX_SUCCESS) + { + probe_core_reset_status = _tx_thread_reset(thread_ptr); + } + } + } +} + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_smp_thread_exit_callback_remote_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + + pointer = (CHAR *) first_unused_memory; + + status = tx_thread_create(&controller, "controller", controller_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + CONTROLLER_PRIORITY, CONTROLLER_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + victim_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_smp_core_exclude(&controller, EXCLUDE_ALL_BUT_CORE_0); + + if (status != TX_SUCCESS) + { + + printf("Running SMP Thread Exit Callback Remote Test............... ERROR #1\n"); + test_control_return(1); + } +} + + +static void controller_entry(ULONG thread_input) +{ + +UINT status; +UINT notify_status; +UINT index; +ULONG waited; +ULONG spins_before; + + + (void) thread_input; + + printf("Running SMP Thread Exit Callback Remote Test............... "); + + victim_run = TX_TRUE; + + status = tx_thread_create(&victim, "victim", victim_entry, 0, + victim_stack, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + + status += tx_thread_smp_core_exclude(&victim, EXCLUDE_ALL_BUT_CORE_1); + + notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + if (notify_status != TX_SUCCESS) + { + status = notify_status; + } +#else + + if (notify_status != TX_FEATURE_NOT_ENABLED) + { + status = notify_status; + } +#endif + + expect_status(status, TX_SUCCESS, 10); + + /* The sentinel goes in after the create, because the create fills the whole + stack with the pattern the stack-checking configurations inspect. */ + write_sentinel(); + expect(sentinel_intact(), 11); + + if (expectations_failed == 0) + { + expect_status(tx_thread_resume(&victim), TX_SUCCESS, 12); + } + + /* Wait, bounded, for the victim to be both mapped to its core and selected + to execute there, and for it to have actually spun. Those three + together are the precondition this test exists for. */ + + waited = 0; + + while ((waited < REMOTE_WAIT_TICKS) && (remote_confirmed == TX_FALSE)) + { + + if ((_tx_thread_execute_ptr[VICTIM_CORE] == &victim) && + (victim.tx_thread_smp_core_mapped == ((UINT) VICTIM_CORE)) && + (victim_spins > 0)) + { + remote_confirmed = TX_TRUE; + } + else + { + tx_thread_sleep(1); + waited++; + } + } + + /* Confirm it is still spinning, so that the termination below really does + start against a running remote thread rather than a stalled one. */ + + spins_before = victim_spins; + + if (remote_confirmed == TX_TRUE) + { + tx_thread_sleep(2); + expect(((victim_spins > spins_before) ? TX_TRUE : TX_FALSE), 13); + } + else + { + + /* Say so rather than assert it. The refusal assertions below do not + depend on the victim being remote, so they still run; the ones that + do are dropped, and this line records that the environment did not + provide the precondition. */ + printf("(remote precondition not established in %u ticks; remote-only checks skipped) ", + (UINT) REMOTE_WAIT_TICKS); + } + + if (expectations_failed == 0) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 20); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 21); + expect_status(probe_state, TX_TERMINATED, 22); + + /* The transition marker is set across the callback, which is what the + two refusals below rest on. */ + expect_status(probe_suspending, TX_TRUE, 23); + + expect_status(probe_delete_status, TX_DELETE_ERROR, 24); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 25); + expect_status(probe_reset_status, TX_NOT_DONE, 26); + expect_status(probe_core_reset_status, TX_NOT_DONE, 27); + + if (remote_confirmed == TX_TRUE) + { + + /* The callback saw the victim still mapped to the remote core. */ + expect_status(probe_core_mapped, (UINT) VICTIM_CORE, 28); + } +#endif + + /* The reset was refused, so nothing memset the stack a live core was + running on. */ + sentinel_intact_after = sentinel_intact(); + expect_status(sentinel_intact_after, TX_TRUE, 29); + + /* And the termination completed: the marker is clear, and the victim is + in no core's execute slot and on no ready list. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 30); + expect_status(victim.tx_thread_suspending, TX_FALSE, 31); + expect(fully_detached(&victim), 32); + } + + /* The victim's loop exits on this, for the case where it is still between + the terminate and its core acting on the preemption. */ + victim_run = TX_FALSE; + + if (expectations_failed == 0) + { + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 40); + } + + if (expectations_failed != 0) + { + + printf("ERROR #%u (%lu of %lu expectations failed:", + first_failure, expectations_failed, expectations_checked); + + for (index = 0; index < failed_tag_count; index++) + { + printf(" %u", failed_tags[index]); + } + + printf(")\n"); + fflush(stdout); + test_control_return(1); + } + else + { + printf("SUCCESS!\n"); + test_control_return(0); + } +} + + +static void victim_entry(ULONG thread_input) +{ + + (void) thread_input; + + /* Spin without relinquishing, so this thread occupies its core rather than + yielding it. */ + while (victim_run == TX_TRUE) + { + victim_spins++; + } +} diff --git a/test/smp/regression/threadx_thread_exit_callback_transition_test.c b/test/smp/regression/threadx_thread_exit_callback_transition_test.c new file mode 100644 index 000000000..c931085c5 --- /dev/null +++ b/test/smp/regression/threadx_thread_exit_callback_transition_test.c @@ -0,0 +1,1375 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* This test covers the completion and termination transitions that a thread's + TX_THREAD_EXIT notification callback runs inside. + + _tx_thread_shell_entry and _tx_thread_terminate both publish the terminal + state -- TX_COMPLETED or TX_TERMINATED -- and then call the thread's exit + notification callback, before the thread has been detached from the ready + list and before either service has finished with the pointer it holds to + the control block. That terminal state is the same state _tx_thread_delete + and _tx_thread_reset accept as authorization to invalidate or rebuild the + control block, so a callback used to be able to delete the object -- and + then lawfully recreate it over the same memory -- while the scheduler was + still linked to the old incarnation, or to reset a terminated thread and + resume it back onto the ready list after termination had reported success. + + The tx_thread_suspending flag marks the transition for exactly its + duration, and delete and reset now refuse a target whose flag is still set. + The refusal is transient: TX_DELETE_ERROR and TX_NOT_DONE are errors those + two services already document, and the same call succeeds once the + transition has finished. + + What the test measures, in every scenario: + + 1. what the callback observes -- the published state, the transition + flag, and the status of each lifecycle service it can reach, + 2. that the scheduler's ready lists, priority map and execute pointer + stay consistent with the control blocks they point at, taken as one + snapshot under interrupt lockout so that the reading is not itself a + race, + 3. that the documented behaviour still works afterwards: the same delete + and reset succeed once the transition has completed, so nothing has + become permanently undeletable. + + The consequences are demonstrated rather than described, and deliberately + from the terminator's context rather than the completing thread's: + + - scenario A has the callback recreate the control block if its delete + unexpectedly succeeded, at a lower priority, so that the outcome is an + assertable disagreement -- the old priority list still heading at a + control block whose own priority field names a different list -- rather + than a fault; + - scenario B has the callback reset and then resume the target, so that a + kernel which allows the reset is caught reporting a successful + termination for a thread that is runnable again. + + Both run while the victim is a ready thread that is not the current thread. + That ordering is not cosmetic. On the Linux, Win32 and Win64 host + simulation ports TX_THREAD_DELETE_PORT_COMPLETION cancels and joins the + host thread backing the deleted thread, so a callback-side delete of the + *completing* thread destroys the host thread the callback is running on and + wedges the simulator instead of reaching an assertion. The completion + scenarios therefore come last, and the scenario gate below stops the run + before them if anything earlier has already failed. + + Every list walk is bounded, so a corrupted ring costs an assertion and not + a hang. Expectations are counted rather than short-circuited within a + scenario, so a failing kernel reports how much it failed by; progression + between scenarios is gated on there being no failures yet, so a kernel that + has already corrupted the scheduler is not driven further into it. */ + +#include +#include "tx_api.h" +#include "tx_thread.h" + + +/* Priorities. The controller sits above every thread it drives. The victim + shares its priority with two peers, so that both the multi-member and the + single-member ready-ring unlink paths are reached. RECREATE_PRIORITY is + below VICTIM_PRIORITY on purpose: see the note in the callback. */ + +#define CONTROLLER_PRIORITY 10 +#define VICTIM_PRIORITY 20 +#define RECREATE_PRIORITY 21 + +/* Upper bound on a ready-ring walk. The suite never has this many threads at + one priority, so exceeding it means the ring is broken rather than long. */ + +#define READY_RING_LIMIT 32 + +/* Sentinel for a probe slot the callback did not reach. */ + +#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL + + +static unsigned long controller_counter = 0; +static unsigned long victim_counter = 0; +static unsigned long peer_a_counter = 0; +static unsigned long peer_b_counter = 0; +static unsigned long replacement_counter = 0; +static unsigned long callback_entry_counter = 0; +static unsigned long callback_exit_counter = 0; + +static TX_THREAD controller; +static TX_THREAD victim; +static TX_THREAD peer_a; +static TX_THREAD peer_b; +static TX_SEMAPHORE victim_semaphore; +static TX_MUTEX victim_mutex; + +static CHAR *victim_stack; +static CHAR *replacement_stack; + +/* What the victim's entry function should do before it returns. */ + +static volatile UINT victim_hold = TX_FALSE; +static volatile UINT victim_self_terminate = TX_FALSE; +static volatile UINT victim_take_mutex = TX_FALSE; +static volatile UINT victim_wait_semaphore = TX_FALSE; +static volatile UINT peers_run = TX_FALSE; + +/* What the exit callback should attempt the next time it runs. Independent + flags rather than one mode, because a delete that unexpectedly succeeds + destroys the object and would mask the reset measurement behind it. */ + +static volatile UINT probe_delete_enabled = TX_FALSE; +static volatile UINT probe_reset_enabled = TX_FALSE; +static volatile UINT probe_misc_enabled = TX_FALSE; +static volatile UINT probe_reuse_enabled = TX_FALSE; + +/* What the callback saw the last time it probed. */ + +static UINT probe_state; +static UINT probe_suspending; +static UINT probe_delete_status; +static UINT probe_core_delete_status; +static UINT probe_reset_status; +static UINT probe_core_reset_status; +static UINT probe_resume_status; +static UINT probe_suspend_status; +static UINT probe_terminate_status; +static UINT probe_create_status; +static UINT probe_consistent; +static UINT probe_ran; +static UINT delete_succeeded; + +/* Ordering witness for the notification pair. Entry and exit counts cannot + simply be compared: a victim created with TX_DONT_START never reports an + entry, and an incarnation that is reset and terminated again reports a + second exit with no second entry. What must hold is that an exit for a + thread that ran is preceded by that thread's entry. */ + +static UINT last_notify_type = PROBE_NOT_ATTEMPTED; +static UINT exit_saw_entry_first = TX_FALSE; + +/* Expectation accounting. */ + +#define FAILED_TAG_LIMIT 32 + +static unsigned long expectations_checked = 0; +static unsigned long expectations_failed = 0; +static UINT first_failure = 0; +static UINT failed_tags[FAILED_TAG_LIMIT]; +static UINT failed_tag_count = 0; + + +/* Define task prototypes. */ + +static void controller_entry(ULONG thread_input); +static void victim_entry(ULONG thread_input); +static void peer_entry(ULONG thread_input); +static void replacement_entry(ULONG thread_input); + + +/* Prototype for test control return. */ + +void test_control_return(UINT status); + + +/* Record one expectation. tag identifies it in the failure report; the first + failing tag is the number the suite prints, so tags are allocated in the + order the expectations are written. */ + +static void expect(UINT condition, UINT tag) +{ + + expectations_checked++; + + if (condition != TX_TRUE) + { + + expectations_failed++; + + if (first_failure == 0) + { + first_failure = tag; + } + + /* Keep the tags, so that a failing kernel reports which expectations + it failed rather than only the first and a total. */ + if (failed_tag_count < FAILED_TAG_LIMIT) + { + failed_tags[failed_tag_count] = tag; + failed_tag_count++; + } + } +} + + +static void expect_status(UINT actual, UINT wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void expect_pointer(void *actual, void *wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void expect_count(unsigned long actual, unsigned long wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +/* Walk one ready ring and report whether thread_ptr is a member of it. The + walk is bounded, so a ring that does not close is reported as a + non-membership rather than spun on for ever. */ + +static UINT ready_ring_contains(UINT priority, TX_THREAD *thread_ptr) +{ + +TX_THREAD *walk_ptr; +TX_THREAD *head_ptr; +UINT steps; +UINT found; + + + found = TX_FALSE; + head_ptr = _tx_thread_priority_list[priority]; + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT) && (found == TX_FALSE)) + { + + if (walk_ptr == thread_ptr) + { + found = TX_TRUE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + /* The ring has closed. */ + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + + return(found); +} + + +/* Take one snapshot of the scheduler's ready structures and check that they + agree with the control blocks they point at. + + This is the assertion the pre-fix behaviour fails. A callback that deletes + the victim and recreates it at another priority leaves the old priority list + heading at that control block while the block's own priority field names the + new list, and leaves the old priority's map bit set with nothing behind it. + Neither disagreement needs a fault to be visible. + + The whole snapshot is taken under interrupt lockout -- which is the global + SMP protection on an SMP port -- so that reading the scheduler's state is + not itself a race against another core. + + transitioning names the one thread that is allowed to be on a ready list + without being ready, which is precisely the thread whose completion or + termination transition is in progress: the terminal state is published + before the detachment, so inside the callback the victim is legitimately a + TX_COMPLETED or TX_TERMINATED member of its ready ring. That single + exemption is from the state clause only. Every other clause, and in + particular the clause that requires a ready-list member to agree that this + is its priority -- the one a callback-side delete and recreate breaks -- + still applies to it. */ + +static UINT scheduler_consistent_except(TX_THREAD *transitioning) +{ + +TX_INTERRUPT_SAVE_AREA + +TX_THREAD *head_ptr; +TX_THREAD *walk_ptr; +TX_THREAD *execute_ptr; +UINT priority; +UINT steps; +UINT consistent; +#ifdef TX_THREAD_SMP_MAX_CORES +UINT core; +#endif + + + consistent = TX_TRUE; + + TX_DISABLE + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + + head_ptr = _tx_thread_priority_list[priority]; + +#if TX_MAX_PRIORITIES <= 32 + + /* A priority with a ready list must have its map bit set, and a + priority with no ready list must not. */ + if (head_ptr == TX_NULL) + { + if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) != ((ULONG) 0)) + { + consistent = TX_FALSE; + } + } + else + { + if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) == ((ULONG) 0)) + { + consistent = TX_FALSE; + } + } +#endif + + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT)) + { + + /* Every thread on a ready list must be a live thread, must be + ready, and must agree that this is its priority. */ + if (walk_ptr -> tx_thread_id != TX_THREAD_ID) + { + consistent = TX_FALSE; + } + + if ((walk_ptr -> tx_thread_state != TX_READY) && (walk_ptr != transitioning)) + { + consistent = TX_FALSE; + } + + if (walk_ptr -> tx_thread_priority != priority) + { + consistent = TX_FALSE; + } + + /* The ring must be doubly linked. */ + if (walk_ptr -> tx_thread_ready_next -> tx_thread_ready_previous != walk_ptr) + { + consistent = TX_FALSE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + + /* A ring that did not close inside the bound is broken. */ + if (walk_ptr != TX_NULL) + { + consistent = TX_FALSE; + } + } + + /* Whatever the scheduler has selected to execute must be a member of the + ready list that its own priority field names. */ +#ifdef TX_THREAD_SMP_MAX_CORES + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + + execute_ptr = _tx_thread_execute_ptr[core]; + + if (execute_ptr != TX_NULL) + { + if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE) + { + consistent = TX_FALSE; + } + } + } +#else + + execute_ptr = _tx_thread_execute_ptr; + + if (execute_ptr != TX_NULL) + { + if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE) + { + consistent = TX_FALSE; + } + } +#endif + + TX_RESTORE + + return(consistent); +} + + +static UINT scheduler_consistent(void) +{ + return(scheduler_consistent_except(TX_NULL)); +} + + +/* Report whether thread_ptr is absent from every ready list and from every + execute slot, which is what a completed or terminated thread must be once + its transition has finished. */ + +static UINT fully_detached(TX_THREAD *thread_ptr) +{ + +TX_INTERRUPT_SAVE_AREA + +UINT priority; +UINT detached; +#ifdef TX_THREAD_SMP_MAX_CORES +UINT core; +#endif + + + detached = TX_TRUE; + + TX_DISABLE + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + if (ready_ring_contains(priority, thread_ptr) == TX_TRUE) + { + detached = TX_FALSE; + } + } + +#ifdef TX_THREAD_SMP_MAX_CORES + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + if (_tx_thread_execute_ptr[core] == thread_ptr) + { + detached = TX_FALSE; + } + } +#else + + if (_tx_thread_execute_ptr == thread_ptr) + { + detached = TX_FALSE; + } +#endif + + TX_RESTORE + + return(detached); +} + + +static void arm_probe(UINT probe_delete, UINT probe_reset, UINT probe_misc, UINT probe_reuse) +{ + + probe_state = PROBE_NOT_ATTEMPTED; + probe_suspending = PROBE_NOT_ATTEMPTED; + probe_delete_status = PROBE_NOT_ATTEMPTED; + probe_core_delete_status = PROBE_NOT_ATTEMPTED; + probe_reset_status = PROBE_NOT_ATTEMPTED; + probe_core_reset_status = PROBE_NOT_ATTEMPTED; + probe_resume_status = PROBE_NOT_ATTEMPTED; + probe_suspend_status = PROBE_NOT_ATTEMPTED; + probe_terminate_status = PROBE_NOT_ATTEMPTED; + probe_create_status = PROBE_NOT_ATTEMPTED; + probe_consistent = PROBE_NOT_ATTEMPTED; + probe_ran = TX_FALSE; + delete_succeeded = TX_FALSE; + + probe_delete_enabled = probe_delete; + probe_reset_enabled = probe_reset; + probe_misc_enabled = probe_misc; + probe_reuse_enabled = probe_reuse; +} + + +/* The exit notification callback. Everything it learns is recorded for the + controller to check; it prints nothing and it never suspends, so it obeys + the one restriction the manual places on a notification callback. */ + +static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type) +{ + + /* Only the victim's notifications are of interest. Its control block is + reused across scenarios, so the pointer is the identity. */ + if (thread_ptr != &victim) + { + return; + } + + if (type == TX_THREAD_ENTRY) + { + callback_entry_counter++; + last_notify_type = TX_THREAD_ENTRY; + return; + } + + if (type != TX_THREAD_EXIT) + { + return; + } + + callback_exit_counter++; + + if (last_notify_type == TX_THREAD_ENTRY) + { + exit_saw_entry_first = TX_TRUE; + } + + last_notify_type = TX_THREAD_EXIT; + + if ((probe_delete_enabled == TX_FALSE) && (probe_reset_enabled == TX_FALSE) && + (probe_misc_enabled == TX_FALSE)) + { + return; + } + + probe_ran = TX_TRUE; + + /* What the kernel has published about the thread at this point. */ + probe_state = thread_ptr -> tx_thread_state; + probe_suspending = thread_ptr -> tx_thread_suspending; + + /* None of the services below has a suspension option, which is the only + thing the manual forbids a callback from using, so every one of them is + reachable from here. + + Where a service is expected to refuse, the core service is called after + the _txe_ wrapper as well. The wrapper is what an ordinary build + reaches; the core service is what a TX_DISABLE_ERROR_CHECKING build + reaches directly, and the refusal has to live there so that disabling + error checking cannot disable it. The core call is made only when the + wrapper already refused, so that a kernel which allows the operation is + not asked to perform it twice. */ + + if (probe_delete_enabled == TX_TRUE) + { + + probe_delete_status = tx_thread_delete(thread_ptr); + + if (probe_delete_status == TX_SUCCESS) + { + delete_succeeded = TX_TRUE; + } + else + { + + probe_core_delete_status = _tx_thread_delete(thread_ptr); + + if (probe_core_delete_status == TX_SUCCESS) + { + delete_succeeded = TX_TRUE; + } + } + + /* Recreate the control block only if the delete unexpectedly + succeeded. On a fixed kernel this never runs. On an unfixed one it + is the lawful next step after a successful delete, and it is what + turns the window into an observable inconsistency: tx_thread_create + zeroes the whole control block, and the auto-start then links it + onto RECREATE_PRIORITY's ready list while the old incarnation is + still the head of VICTIM_PRIORITY's. + + RECREATE_PRIORITY is the lower of the two so that the stale head is + the one the scheduler selects from, and so that the ring at the new + priority has a single member. Recreating at the same priority would + instead have the auto-start write through the control block's + freshly zeroed ready_previous pointer, which is a fault rather than + an assertion. */ + if ((delete_succeeded == TX_TRUE) && (probe_reuse_enabled == TX_TRUE)) + { + probe_create_status = tx_thread_create(thread_ptr, "replacement", + replacement_entry, 0, + replacement_stack, + TEST_STACK_SIZE_PRINTF, + RECREATE_PRIORITY, RECREATE_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + } + } + + if ((probe_reset_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE)) + { + + probe_reset_status = tx_thread_reset(thread_ptr); + + if (probe_reset_status != TX_SUCCESS) + { + probe_core_reset_status = _tx_thread_reset(thread_ptr); + } + + /* Follow the reset with the resume that the reset would have unlocked. + On a fixed kernel the state is still terminal and the resume is + refused. On an unfixed one the reset has moved the state to + TX_SUSPENDED, the resume finds a suspending thread whose state is no + longer terminal, clears the flag and puts the thread back on the + ready list -- and the outer service then skips the removal and + reports success for a termination that did not happen. */ + probe_resume_status = tx_thread_resume(thread_ptr); + } + + if ((probe_misc_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE)) + { + probe_suspend_status = tx_thread_suspend(thread_ptr); + probe_terminate_status = tx_thread_terminate(thread_ptr); + } + + probe_consistent = scheduler_consistent_except(thread_ptr); +} + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_thread_exit_callback_transition_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + + /* Put first available memory address into a character pointer. */ + pointer = (CHAR *) first_unused_memory; + + status = tx_thread_create(&controller, "controller", controller_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + CONTROLLER_PRIORITY, CONTROLLER_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_create(&peer_a, "peer a", peer_entry, 1, + pointer, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_create(&peer_b, "peer b", peer_entry, 2, + pointer, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + /* The victim's control block is created and destroyed repeatedly, so its + two stacks are set aside here rather than per scenario. */ + victim_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + replacement_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_semaphore_create(&victim_semaphore, "victim semaphore", 0); + status += tx_mutex_create(&victim_mutex, "victim mutex", TX_INHERIT); + + if (status != TX_SUCCESS) + { + + printf("Running Thread Exit Callback Transition Test............... ERROR #1\n"); + test_control_return(1); + } + +#ifdef TX_THREAD_SMP_MAX_CORES + + /* Confine every thread this test drives to core 0. The measurements below + are of global scheduler structures at chosen moments, and letting the + peers and the victim run on other cores would make them a race against + the test rather than a property of it. A victim executing on a + different core when termination begins is deliberately covered by a + separate SMP test, so that the deterministic cases here cannot be made + flaky by it. */ + status = tx_thread_smp_core_exclude(&controller, 0xE); + status += tx_thread_smp_core_exclude(&peer_a, 0xE); + status += tx_thread_smp_core_exclude(&peer_b, 0xE); + + if (status != TX_SUCCESS) + { + + printf("Running Thread Exit Callback Transition Test............... ERROR #2\n"); + test_control_return(1); + } +#endif +} + + +/* Create the victim over its own control block, with the exit callback + registered. The notify status is folded into the result according to what + the configuration supports. */ + +static UINT create_victim(UINT priority, UINT auto_start) +{ + +UINT status; +UINT notify_status; + + + status = tx_thread_create(&victim, "victim", victim_entry, 0, + victim_stack, TEST_STACK_SIZE_PRINTF, + priority, priority, TX_NO_TIME_SLICE, auto_start); + + notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + if (notify_status != TX_SUCCESS) + { + status = notify_status; + } +#else + + /* Notification is compiled out, so the registration is expected to be + refused and the callback never runs. */ + if (notify_status != TX_FEATURE_NOT_ENABLED) + { + status = notify_status; + } +#endif + +#ifdef TX_THREAD_SMP_MAX_CORES + + if (status == TX_SUCCESS) + { + status = tx_thread_smp_core_exclude(&victim, 0xE); + } +#endif + + return(status); +} + + +/* The gate between scenarios. A kernel that has already failed an expectation + has, in these scenarios, already left the scheduler inconsistent, and + driving it further would report nothing more while risking a wedge instead + of a verdict. */ + +static UINT ok_so_far(void) +{ + +UINT result; + + + result = TX_FALSE; + + if (expectations_failed == 0) + { + result = TX_TRUE; + } + + return(result); +} + + +static void controller_entry(ULONG thread_input) +{ + +ULONG expected_exits; +ULONG expected_entries; +UINT index; + + + (void) thread_input; + + controller_counter++; + expected_exits = 0; + expected_entries = 0; + + /* Inform user. */ + printf("Running Thread Exit Callback Transition Test............... "); + + /* The scheduler must be self-consistent before anything is measured, so + that a later failure is attributable to this test. */ + expect(scheduler_consistent(), 10); + + /* ------------------------------------------------------------------- */ + /* Scenario A: terminate a ready target that is not the current thread, */ + /* with two peers ready at the same priority. The callback attempts */ + /* the delete, and recreates the control block if it succeeded. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + peers_run = TX_TRUE; + victim_hold = TX_TRUE; + + expect_status(tx_thread_resume(&peer_a), TX_SUCCESS, 11); + expect_status(tx_thread_resume(&peer_b), TX_SUCCESS, 12); + + arm_probe(TX_TRUE, TX_FALSE, TX_FALSE, TX_TRUE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 13); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim and both peers become ready and start spinning, so + that the victim's unlink is from a multi-member ring. */ + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_READY, 14); + expect(ready_ring_contains(VICTIM_PRIORITY, &victim), 15); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 16); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 17); + } + + if (ok_so_far() == TX_TRUE) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 18); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 19); + + /* The callback runs after the terminal state has been published, and + inside the transition, which is what the flag records. */ + expect_status(probe_state, TX_TERMINATED, 20); + expect_status(probe_suspending, TX_TRUE, 21); + + /* Both the wrapper and the core service must refuse. */ + expect_status(probe_delete_status, TX_DELETE_ERROR, 22); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 23); + + /* So the recreate never happened and nothing is inconsistent. */ + expect_status(delete_succeeded, TX_FALSE, 24); + expect_status(probe_create_status, PROBE_NOT_ATTEMPTED, 25); + expect_status(probe_consistent, TX_TRUE, 26); + expect_count(replacement_counter, 0, 27); +#endif + + /* After the service returns the termination is complete: the state is + terminal, the marker is clear, the target is on no ready list and in + no execute slot, and the peers are undisturbed. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 28); + expect_status(victim.tx_thread_suspending, TX_FALSE, 29); + expect(fully_detached(&victim), 30); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 31); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 32); + expect(scheduler_consistent(), 33); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + /* The documented behaviour is available again. */ + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 34); + } + + /* ------------------------------------------------------------------- */ + /* Scenario B: the same termination, but the callback attempts a reset */ + /* and then the resume that a successful reset would have unlocked. */ + /* */ + /* This is the leg where the transition check is the only thing */ + /* standing between the callback and a reset of a thread that is still */ + /* on the ready list. Without it the reset succeeds, the resume */ + /* clears the suspending flag and restores TX_READY, the outer service */ + /* then finds the flag clear and skips the removal, and */ + /* tx_thread_terminate returns TX_SUCCESS for a runnable thread. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_TRUE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 40); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_READY, 41); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 42); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 43); + expect_status(probe_state, TX_TERMINATED, 44); + expect_status(probe_suspending, TX_TRUE, 45); + + /* The discriminating leg. The target is not the current thread and + its published state is terminal, so both of the conditions the + service tested before this fix are satisfied. */ + expect_status(probe_reset_status, TX_NOT_DONE, 46); + expect_status(probe_core_reset_status, TX_NOT_DONE, 47); + + /* With the reset refused, the state is still terminal, so the resume + cannot reach the branch that would void the termination. */ + expect_status(probe_resume_status, TX_RESUME_ERROR, 48); + expect_status(probe_consistent, TX_TRUE, 49); +#endif + + /* And the termination stands. A kernel that let the callback reset + and resume the target arrives here with the victim ready to run. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 50); + expect_status(victim.tx_thread_suspending, TX_FALSE, 51); + expect(fully_detached(&victim), 52); + expect(scheduler_consistent(), 53); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 54); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 55); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 56); + } + + /* Retire the peers, so that the ring drops to a single member and then to + none, and check consistency across both unlinks. */ + + if (ok_so_far() == TX_TRUE) + { + + peers_run = TX_FALSE; + tx_thread_sleep(3); + + expect_status(peer_a.tx_thread_state, TX_COMPLETED, 57); + expect_status(peer_b.tx_thread_state, TX_COMPLETED, 58); + expect(scheduler_consistent(), 59); + expect_status(tx_thread_delete(&peer_a), TX_SUCCESS, 60); + expect_status(tx_thread_delete(&peer_b), TX_SUCCESS, 61); + } + + /* ------------------------------------------------------------------- */ + /* Scenario C: terminate a target suspended on an object, so that the */ + /* non-ready branch of _tx_thread_terminate runs -- suspension cleanup, */ + /* the terminated extension, the callback, and mutex release, in that */ + /* order. That branch used to clear the marker before the callback */ + /* rather than after the last dereference of the target. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE); + victim_hold = TX_FALSE; + victim_take_mutex = TX_TRUE; + victim_wait_semaphore = TX_TRUE; + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 70); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim take the mutex and suspend on the semaphore. */ + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_SEMAPHORE_SUSP, 71); + expect_pointer(victim_mutex.tx_mutex_owner, &victim, 72); + } + + if (ok_so_far() == TX_TRUE) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 73); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 74); + expect_status(probe_state, TX_TERMINATED, 75); + + /* The discriminating leg for this branch. The marker used to be + cleared before this point, which left the callback free to delete or + reset a control block that the service still had mutex-release + processing to do against. */ + expect_status(probe_suspending, TX_TRUE, 76); + expect_status(probe_delete_status, TX_DELETE_ERROR, 77); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 78); + expect_status(probe_reset_status, TX_NOT_DONE, 79); + expect_status(probe_core_reset_status, TX_NOT_DONE, 80); + expect_status(probe_resume_status, TX_RESUME_ERROR, 81); + expect_status(delete_succeeded, TX_FALSE, 82); + expect_status(probe_consistent, TX_TRUE, 83); +#endif + + /* The marker is cleared at the safe point, after the mutex release, + and the mutex the terminated thread owned has been given up. The + value of the flag during the release itself is not observable from + application code; what is observable is that there is exactly one + clear site and that it sits downstream of the release. */ + expect_status(victim.tx_thread_suspending, TX_FALSE, 84); + expect_pointer(victim_mutex.tx_mutex_owner, TX_NULL, 85); + expect_count((unsigned long) victim_semaphore.tx_semaphore_suspended_count, 0, 86); + expect(fully_detached(&victim), 87); + expect(scheduler_consistent(), 88); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 89); + } + + /* ------------------------------------------------------------------- */ + /* Scenario D: natural completion, victim alone at its priority. */ + /* */ + /* The callback here runs on the completing thread itself, so a */ + /* callback-side delete that succeeds destroys the host thread on the */ + /* simulation ports. This scenario is therefore placed after the ones */ + /* that reach an assertion, and the gate above stops the run before it */ + /* if any of them has already failed. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_TRUE, TX_TRUE, TX_TRUE, TX_FALSE); + victim_take_mutex = TX_FALSE; + victim_wait_semaphore = TX_FALSE; + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 100); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim run to completion, so that its callback runs inside + the completion transition. */ + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 101); + expect_status(probe_state, TX_COMPLETED, 102); + expect_status(probe_suspending, TX_TRUE, 103); + expect_status(probe_delete_status, TX_DELETE_ERROR, 104); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 105); + + /* Reset of the running thread is refused for two independent reasons + here -- it is the current thread, and its transition is in progress + -- so this leg is a guard rather than a discriminator. Scenario B + is where the transition check is the only thing refusing a reset. */ + expect_status(probe_reset_status, TX_NOT_DONE, 106); + expect_status(probe_core_reset_status, TX_NOT_DONE, 107); + + /* A completed thread is neither resumable nor suspendable, and + terminating one is a documented no-op. */ + expect_status(probe_resume_status, TX_RESUME_ERROR, 108); + expect_status(probe_suspend_status, TX_SUSPEND_ERROR, 109); + expect_status(probe_terminate_status, TX_SUCCESS, 110); + expect_status(delete_succeeded, TX_FALSE, 111); + expect_status(probe_consistent, TX_TRUE, 112); +#endif + + /* The transition has finished, so the calls the callback was refused + must succeed. The refusal is transient and nothing has become + permanently undeletable. */ + expect_status(victim.tx_thread_state, TX_COMPLETED, 113); + expect_status(victim.tx_thread_suspending, TX_FALSE, 114); + expect(fully_detached(&victim), 115); + expect(scheduler_consistent(), 116); + expect(((victim_counter > 0) ? TX_TRUE : TX_FALSE), 117); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + /* Reset, so that the documented reset of a completed thread is covered + on a target whose transition has finished, and so that the + reset/terminate/delete order is exercised on this branch too. */ + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 118); + expect_status(victim.tx_thread_state, TX_SUSPENDED, 119); + expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 120); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 121); + expect_status(victim.tx_thread_suspending, TX_FALSE, 122); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 123); + } + + /* A safe post-completion recreate at a different priority, which is the + lifecycle the callback was refused, performed at the point it is + legitimate. It exercises the priority map and the list heads for a + recreate that moves priority as well as for one that does not. */ + + if (ok_so_far() == TX_TRUE) + { + + expect_status(create_victim(RECREATE_PRIORITY, TX_AUTO_START), TX_SUCCESS, 124); + + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_COMPLETED, 125); + expect(scheduler_consistent(), 126); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 127); + } + + /* ------------------------------------------------------------------- */ + /* Scenario E: self termination. The callback fires once, in the */ + /* victim's own context, the lifecycle mutation is refused there too, */ + /* and another thread can complete the lifecycle afterwards. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + victim_self_terminate = TX_TRUE; + expected_exits = callback_exit_counter; + + arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 130); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* Exactly one exit notification, even though the thread terminated + itself from inside its own entry function. */ + expect_count(callback_exit_counter, expected_exits + 1, 131); + expect_status(probe_state, TX_TERMINATED, 132); + expect_status(probe_suspending, TX_TRUE, 133); + expect_status(probe_delete_status, TX_DELETE_ERROR, 134); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 135); + expect_status(probe_reset_status, TX_NOT_DONE, 136); + expect_status(delete_succeeded, TX_FALSE, 137); + expect_status(probe_consistent, TX_TRUE, 138); +#else + + expect_count(callback_exit_counter, expected_exits, 139); +#endif + + expect_status(victim.tx_thread_state, TX_TERMINATED, 140); + expect_status(victim.tx_thread_suspending, TX_FALSE, 141); + expect(fully_detached(&victim), 142); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 143); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 144); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 145); + expect(scheduler_consistent(), 146); + } + + /* ------------------------------------------------------------------- */ + /* Scenario F: a benign callback is unaffected. The notification */ + /* ordering and counts are what they were. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + victim_self_terminate = TX_FALSE; + expected_exits = callback_exit_counter; + expected_entries = callback_entry_counter; + exit_saw_entry_first = TX_FALSE; + last_notify_type = PROBE_NOT_ATTEMPTED; + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 150); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* This incarnation ran and completed, so it reported exactly one entry + and one exit, and the entry came first. The transition marker has + changed neither how often notifications are delivered nor in what + order. */ + expect_count(callback_exit_counter, expected_exits + 1, 151); + expect_count(callback_entry_counter, expected_entries + 1, 152); + expect_status(exit_saw_entry_first, TX_TRUE, 153); +#else + + expect_count(callback_exit_counter, expected_exits, 154); + expect_count(callback_entry_counter, expected_entries, 155); +#endif + + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 156); + expect(scheduler_consistent(), 157); + } + + /* ------------------------------------------------------------------- */ + /* Scenario G: state and boundary cases. A thread that never entered a */ + /* terminal state, and one that is already fully detached, behave as */ + /* documented -- the new refusal applies only while the marker is set. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + expect_status(create_victim(VICTIM_PRIORITY, TX_DONT_START), TX_SUCCESS, 170); + + /* Suspended, never run: neither state is terminal, so both services + are refused for the reason they always were, and the marker is not + set on a thread that is not transitioning. */ + expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 171); + expect_status(tx_thread_reset(&victim), TX_NOT_DONE, 172); + expect_status(victim.tx_thread_suspending, TX_FALSE, 173); + + /* Terminated from a suspended state: deletable immediately, and + repeatedly resettable and re-terminable in between. */ + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 174); + expect_status(victim.tx_thread_suspending, TX_FALSE, 175); + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 176); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 177); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 178); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 179); + expect(scheduler_consistent(), 180); + } + + if (expectations_failed != 0) + { + + printf("ERROR #%u (%lu of %lu expectations failed:", + first_failure, expectations_failed, expectations_checked); + + for (index = 0; index < failed_tag_count; index++) + { + printf(" %u", failed_tags[index]); + } + + if (expectations_failed > ((unsigned long) failed_tag_count)) + { + printf(" ..."); + } + + printf(")\n"); + fflush(stdout); + test_control_return(1); + } + else + { + printf("SUCCESS!\n"); + test_control_return(0); + } +} + + +static void victim_entry(ULONG thread_input) +{ + + (void) thread_input; + + victim_counter++; + + if (victim_take_mutex == TX_TRUE) + { + tx_mutex_get(&victim_mutex, TX_NO_WAIT); + } + + if (victim_wait_semaphore == TX_TRUE) + { + + /* Suspend on an object, so that a termination takes the non-ready + branch and runs the suspension cleanup routine. */ + tx_semaphore_get(&victim_semaphore, TX_WAIT_FOREVER); + } + + if (victim_self_terminate == TX_TRUE) + { + + /* This does not return: the termination detaches this thread and the + preemption check at the end of the service switches away from it. */ + tx_thread_terminate(tx_thread_identify()); + } + + while (victim_hold == TX_TRUE) + { + + /* Stay ready, so that a terminate issued by another thread finds this + thread on the ready list. */ + tx_thread_relinquish(); + } + + /* Returning enters the completion transition. */ +} + + +static void peer_entry(ULONG thread_input) +{ + + if (thread_input == 1) + { + peer_a_counter++; + } + else + { + peer_b_counter++; + } + + while (peers_run == TX_TRUE) + { + + /* Stay ready at the victim's priority, so that the victim's ready ring + has more than one member. */ + tx_thread_relinquish(); + } +} + + +static void replacement_entry(ULONG thread_input) +{ + + (void) thread_input; + + replacement_counter++; + + /* Only reached on a kernel that let the callback recreate the control + block. Suspend rather than return, so that the run does not depend on a + second completion transition over a control block already known to be + inconsistent. */ + tx_thread_suspend(tx_thread_identify()); +} diff --git a/test/tx/cmake/CMakeLists.txt b/test/tx/cmake/CMakeLists.txt index b2f0ab1fe..235a28d2e 100644 --- a/test/tx/cmake/CMakeLists.txt +++ b/test/tx/cmake/CMakeLists.txt @@ -61,6 +61,15 @@ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../.. threadx) add_subdirectory(regression) add_subdirectory(samples) +# The two configurations this change has to reach, TX_NOT_INTERRUPTABLE and +# TX_DISABLE_ERROR_CHECKING, are not among the five above and cannot be reached +# from the regression suite, which links one library per configuration. This +# directory compiles the sources concerned directly. It is listed last rather than +# beside regression on purpose: every branch in the current fix set adds an +# add_subdirectory line immediately after regression, and putting a second one +# there turns a set of clean merges into a set of one-line conflicts. +add_subdirectory(thread_transition) + # Coverage # # The gate here used to be the build type alone, and only one of the five diff --git a/test/tx/cmake/regression/CMakeLists.txt b/test/tx/cmake/regression/CMakeLists.txt index 7a0296dc1..1479a93e0 100644 --- a/test/tx/cmake/regression/CMakeLists.txt +++ b/test/tx/cmake/regression/CMakeLists.txt @@ -98,6 +98,7 @@ set(regression_test_cases ${SOURCE_DIR}/threadx_thread_sleep_terminate_test.c ${SOURCE_DIR}/threadx_thread_stack_checking_test.c ${SOURCE_DIR}/threadx_thread_terminate_delete_test.c + ${SOURCE_DIR}/threadx_thread_exit_callback_transition_test.c ${SOURCE_DIR}/threadx_thread_time_slice_change_test.c ${SOURCE_DIR}/threadx_thread_wait_abort_and_isr_test.c ${SOURCE_DIR}/threadx_thread_wait_abort_test.c diff --git a/test/tx/cmake/thread_transition/CMakeLists.txt b/test/tx/cmake/thread_transition/CMakeLists.txt new file mode 100644 index 000000000..f414ed675 --- /dev/null +++ b/test/tx/cmake/thread_transition/CMakeLists.txt @@ -0,0 +1,115 @@ +cmake_minimum_required(VERSION 3.13 FATAL_ERROR) +cmake_policy(SET CMP0057 NEW) + +project(thread_transition_test LANGUAGES C) + +set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..) +set(SOURCE_DIR ${REPO_ROOT}/test/tx/thread_transition) + +# TX_NOT_INTERRUPTABLE and TX_DISABLE_ERROR_CHECKING are not among the five build +# configurations this tree compiles, and the tree builds the whole ThreadX library +# once per configuration, so neither can be reached from inside the regression +# suite. Both matter to the completion and termination transitions: +# +# - TX_NOT_INTERRUPTABLE selects a different transition, in which the exit +# notification callback runs with interrupts disabled and the interruptable +# suspension path -- which is what sets and clears the transition marker in the +# configurations the tree does build -- is not used at all. The marker is set +# and cleared there by stores that exist only in that configuration, and a +# marker left set would make a normally completed thread permanently +# undeletable. +# +# - TX_DISABLE_ERROR_CHECKING removes the _txe_ wrappers, so the public names bind +# straight to the core services. The refusal lives in the core services for that +# reason, and the way to show it is to call the public names in a build with no +# wrappers. +# +# So the four sources that change are compiled directly into a test executable, once +# per combination, with recorders standing behind the scheduler services they call. +# That is the same technique the module manager tests in this tree use, for the same +# reason: the code under test cannot be reached through the library the suite links. +# +# These executables are deliberately left out of the coverage instrumentation. The +# same source compiled under different feature macros has a different line set, and +# merging those into the union the coverage report takes would confuse the figure +# rather than add to it. The coverage figure belongs to the five configurations the +# tree builds, and every line this change adds outside a TX_NOT_INTERRUPTABLE guard +# is in them. + +set(transition_kernel_sources + ${REPO_ROOT}/common/src/tx_thread_shell_entry.c + ${REPO_ROOT}/common/src/tx_thread_terminate.c + ${REPO_ROOT}/common/src/tx_thread_delete.c + ${REPO_ROOT}/common/src/tx_thread_reset.c + ${REPO_ROOT}/common/src/txe_thread_delete.c + ${REPO_ROOT}/common/src/txe_thread_reset.c + ${REPO_ROOT}/common/src/txe_thread_terminate.c + ${REPO_ROOT}/common/src/tx_thread_initialize.c) + +set(transition_sources + ${SOURCE_DIR}/threadx_thread_transition_configuration_test.c + ${transition_kernel_sources}) + +# The shim goes on the kernel sources only, not on the test. It defines +# TX_SOURCE_CODE, which is what tells tx_api.h to leave the public service names +# unmapped so that internal sources can use the core names; the test needs the +# opposite, because calling the public names and letting tx_api.h decide what they +# bind to is exactly what the TX_DISABLE_ERROR_CHECKING configuration is here to +# demonstrate. +set_source_files_properties( + ${transition_kernel_sources} + DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + PROPERTIES COMPILE_OPTIONS + "-include;${SOURCE_DIR}/threadx_thread_transition_host_test_port.h") + +# Each entry is a test-name suffix, a colon, and the feature macros that define the +# configuration, separated by "|". A semicolon cannot be used as that separator: it +# is CMake's own list separator, so the foreach below would iterate the macros +# instead of the configurations. +set(transition_configurations + "ni:TX_NOT_INTERRUPTABLE" + "ni_no_error_checking:TX_NOT_INTERRUPTABLE|TX_DISABLE_ERROR_CHECKING" + "ni_no_notify:TX_NOT_INTERRUPTABLE|TX_DISABLE_NOTIFY_CALLBACKS" + "no_error_checking:TX_DISABLE_ERROR_CHECKING") + +foreach(configuration ${transition_configurations}) + + string(REPLACE ":" ";" configuration_parts ${configuration}) + list(GET configuration_parts 0 configuration_name) + list(GET configuration_parts 1 configuration_macro_text) + string(REPLACE "|" ";" configuration_macros ${configuration_macro_text}) + + set(test_name threadx_thread_transition_${configuration_name}_test) + + add_executable(${test_name} ${transition_sources}) + + target_include_directories( + ${test_name} + PRIVATE ${SOURCE_DIR} + ${REPO_ROOT}/common/inc + ${REPO_ROOT}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc) + + target_compile_definitions(${test_name} PRIVATE ${configuration_macros}) + + # This directory is configured once per build configuration of the tree, so + # these executables inherit whichever feature macros that configuration sets -- + # which is useful, since it means the two configurations above are also seen in + # combination with stack checking and with the other four. The one that cannot + # be inherited is event tracing: the trace macros in these sources reference the + # trace component's buffer and registry, and this harness links four kernel + # sources rather than the library, so the references would not resolve. The + # trace insertions in these same sources are covered by the tree's own + # trace_build configuration, where the whole library is linked, so nothing is + # lost by turning tracing off here. Event logging is turned off for the same + # reason; no configuration of the tree enables it. + # + # The -U flags have to reach the test source as well as the kernel sources, + # because TX_ENABLE_EVENT_TRACE is visible to tx_api.h and the two must agree on + # what the headers declare. Target compile options land after the directory's + # -D flags on the command line, which is what makes the -U effective. + target_compile_options(${test_name} PRIVATE -UTX_ENABLE_EVENT_TRACE + -UTX_ENABLE_EVENT_LOG) + + add_test(${CMAKE_BUILD_TYPE}::${test_name} ${test_name}) + +endforeach() diff --git a/test/tx/regression/testcontrol.c b/test/tx/regression/testcontrol.c index e2b3f7253..00e31d9b0 100644 --- a/test/tx/regression/testcontrol.c +++ b/test/tx/regression/testcontrol.c @@ -218,6 +218,7 @@ void threadx_thread_simple_sleep_non_clear_application_define(void *); void threadx_thread_sleep_for_100ticks_application_define(void *); void threadx_thread_multiple_sleep_application_define(void *); void threadx_thread_terminate_delete_application_define(void *); +void threadx_thread_exit_callback_transition_application_define(void *); void threadx_thread_preemption_change_application_define(void *); void threadx_thread_priority_change_application_define(void *); void threadx_thread_time_slice_change_application_define(void *); @@ -335,6 +336,7 @@ TEST_ENTRY test_control_tests[] = threadx_thread_sleep_for_100ticks_application_define, threadx_thread_multiple_sleep_application_define, threadx_thread_terminate_delete_application_define, + threadx_thread_exit_callback_transition_application_define, threadx_thread_priority_change_application_define, diff --git a/test/tx/regression/threadx_thread_exit_callback_transition_test.c b/test/tx/regression/threadx_thread_exit_callback_transition_test.c new file mode 100644 index 000000000..c931085c5 --- /dev/null +++ b/test/tx/regression/threadx_thread_exit_callback_transition_test.c @@ -0,0 +1,1375 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* This test covers the completion and termination transitions that a thread's + TX_THREAD_EXIT notification callback runs inside. + + _tx_thread_shell_entry and _tx_thread_terminate both publish the terminal + state -- TX_COMPLETED or TX_TERMINATED -- and then call the thread's exit + notification callback, before the thread has been detached from the ready + list and before either service has finished with the pointer it holds to + the control block. That terminal state is the same state _tx_thread_delete + and _tx_thread_reset accept as authorization to invalidate or rebuild the + control block, so a callback used to be able to delete the object -- and + then lawfully recreate it over the same memory -- while the scheduler was + still linked to the old incarnation, or to reset a terminated thread and + resume it back onto the ready list after termination had reported success. + + The tx_thread_suspending flag marks the transition for exactly its + duration, and delete and reset now refuse a target whose flag is still set. + The refusal is transient: TX_DELETE_ERROR and TX_NOT_DONE are errors those + two services already document, and the same call succeeds once the + transition has finished. + + What the test measures, in every scenario: + + 1. what the callback observes -- the published state, the transition + flag, and the status of each lifecycle service it can reach, + 2. that the scheduler's ready lists, priority map and execute pointer + stay consistent with the control blocks they point at, taken as one + snapshot under interrupt lockout so that the reading is not itself a + race, + 3. that the documented behaviour still works afterwards: the same delete + and reset succeed once the transition has completed, so nothing has + become permanently undeletable. + + The consequences are demonstrated rather than described, and deliberately + from the terminator's context rather than the completing thread's: + + - scenario A has the callback recreate the control block if its delete + unexpectedly succeeded, at a lower priority, so that the outcome is an + assertable disagreement -- the old priority list still heading at a + control block whose own priority field names a different list -- rather + than a fault; + - scenario B has the callback reset and then resume the target, so that a + kernel which allows the reset is caught reporting a successful + termination for a thread that is runnable again. + + Both run while the victim is a ready thread that is not the current thread. + That ordering is not cosmetic. On the Linux, Win32 and Win64 host + simulation ports TX_THREAD_DELETE_PORT_COMPLETION cancels and joins the + host thread backing the deleted thread, so a callback-side delete of the + *completing* thread destroys the host thread the callback is running on and + wedges the simulator instead of reaching an assertion. The completion + scenarios therefore come last, and the scenario gate below stops the run + before them if anything earlier has already failed. + + Every list walk is bounded, so a corrupted ring costs an assertion and not + a hang. Expectations are counted rather than short-circuited within a + scenario, so a failing kernel reports how much it failed by; progression + between scenarios is gated on there being no failures yet, so a kernel that + has already corrupted the scheduler is not driven further into it. */ + +#include +#include "tx_api.h" +#include "tx_thread.h" + + +/* Priorities. The controller sits above every thread it drives. The victim + shares its priority with two peers, so that both the multi-member and the + single-member ready-ring unlink paths are reached. RECREATE_PRIORITY is + below VICTIM_PRIORITY on purpose: see the note in the callback. */ + +#define CONTROLLER_PRIORITY 10 +#define VICTIM_PRIORITY 20 +#define RECREATE_PRIORITY 21 + +/* Upper bound on a ready-ring walk. The suite never has this many threads at + one priority, so exceeding it means the ring is broken rather than long. */ + +#define READY_RING_LIMIT 32 + +/* Sentinel for a probe slot the callback did not reach. */ + +#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL + + +static unsigned long controller_counter = 0; +static unsigned long victim_counter = 0; +static unsigned long peer_a_counter = 0; +static unsigned long peer_b_counter = 0; +static unsigned long replacement_counter = 0; +static unsigned long callback_entry_counter = 0; +static unsigned long callback_exit_counter = 0; + +static TX_THREAD controller; +static TX_THREAD victim; +static TX_THREAD peer_a; +static TX_THREAD peer_b; +static TX_SEMAPHORE victim_semaphore; +static TX_MUTEX victim_mutex; + +static CHAR *victim_stack; +static CHAR *replacement_stack; + +/* What the victim's entry function should do before it returns. */ + +static volatile UINT victim_hold = TX_FALSE; +static volatile UINT victim_self_terminate = TX_FALSE; +static volatile UINT victim_take_mutex = TX_FALSE; +static volatile UINT victim_wait_semaphore = TX_FALSE; +static volatile UINT peers_run = TX_FALSE; + +/* What the exit callback should attempt the next time it runs. Independent + flags rather than one mode, because a delete that unexpectedly succeeds + destroys the object and would mask the reset measurement behind it. */ + +static volatile UINT probe_delete_enabled = TX_FALSE; +static volatile UINT probe_reset_enabled = TX_FALSE; +static volatile UINT probe_misc_enabled = TX_FALSE; +static volatile UINT probe_reuse_enabled = TX_FALSE; + +/* What the callback saw the last time it probed. */ + +static UINT probe_state; +static UINT probe_suspending; +static UINT probe_delete_status; +static UINT probe_core_delete_status; +static UINT probe_reset_status; +static UINT probe_core_reset_status; +static UINT probe_resume_status; +static UINT probe_suspend_status; +static UINT probe_terminate_status; +static UINT probe_create_status; +static UINT probe_consistent; +static UINT probe_ran; +static UINT delete_succeeded; + +/* Ordering witness for the notification pair. Entry and exit counts cannot + simply be compared: a victim created with TX_DONT_START never reports an + entry, and an incarnation that is reset and terminated again reports a + second exit with no second entry. What must hold is that an exit for a + thread that ran is preceded by that thread's entry. */ + +static UINT last_notify_type = PROBE_NOT_ATTEMPTED; +static UINT exit_saw_entry_first = TX_FALSE; + +/* Expectation accounting. */ + +#define FAILED_TAG_LIMIT 32 + +static unsigned long expectations_checked = 0; +static unsigned long expectations_failed = 0; +static UINT first_failure = 0; +static UINT failed_tags[FAILED_TAG_LIMIT]; +static UINT failed_tag_count = 0; + + +/* Define task prototypes. */ + +static void controller_entry(ULONG thread_input); +static void victim_entry(ULONG thread_input); +static void peer_entry(ULONG thread_input); +static void replacement_entry(ULONG thread_input); + + +/* Prototype for test control return. */ + +void test_control_return(UINT status); + + +/* Record one expectation. tag identifies it in the failure report; the first + failing tag is the number the suite prints, so tags are allocated in the + order the expectations are written. */ + +static void expect(UINT condition, UINT tag) +{ + + expectations_checked++; + + if (condition != TX_TRUE) + { + + expectations_failed++; + + if (first_failure == 0) + { + first_failure = tag; + } + + /* Keep the tags, so that a failing kernel reports which expectations + it failed rather than only the first and a total. */ + if (failed_tag_count < FAILED_TAG_LIMIT) + { + failed_tags[failed_tag_count] = tag; + failed_tag_count++; + } + } +} + + +static void expect_status(UINT actual, UINT wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void expect_pointer(void *actual, void *wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void expect_count(unsigned long actual, unsigned long wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +/* Walk one ready ring and report whether thread_ptr is a member of it. The + walk is bounded, so a ring that does not close is reported as a + non-membership rather than spun on for ever. */ + +static UINT ready_ring_contains(UINT priority, TX_THREAD *thread_ptr) +{ + +TX_THREAD *walk_ptr; +TX_THREAD *head_ptr; +UINT steps; +UINT found; + + + found = TX_FALSE; + head_ptr = _tx_thread_priority_list[priority]; + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT) && (found == TX_FALSE)) + { + + if (walk_ptr == thread_ptr) + { + found = TX_TRUE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + /* The ring has closed. */ + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + + return(found); +} + + +/* Take one snapshot of the scheduler's ready structures and check that they + agree with the control blocks they point at. + + This is the assertion the pre-fix behaviour fails. A callback that deletes + the victim and recreates it at another priority leaves the old priority list + heading at that control block while the block's own priority field names the + new list, and leaves the old priority's map bit set with nothing behind it. + Neither disagreement needs a fault to be visible. + + The whole snapshot is taken under interrupt lockout -- which is the global + SMP protection on an SMP port -- so that reading the scheduler's state is + not itself a race against another core. + + transitioning names the one thread that is allowed to be on a ready list + without being ready, which is precisely the thread whose completion or + termination transition is in progress: the terminal state is published + before the detachment, so inside the callback the victim is legitimately a + TX_COMPLETED or TX_TERMINATED member of its ready ring. That single + exemption is from the state clause only. Every other clause, and in + particular the clause that requires a ready-list member to agree that this + is its priority -- the one a callback-side delete and recreate breaks -- + still applies to it. */ + +static UINT scheduler_consistent_except(TX_THREAD *transitioning) +{ + +TX_INTERRUPT_SAVE_AREA + +TX_THREAD *head_ptr; +TX_THREAD *walk_ptr; +TX_THREAD *execute_ptr; +UINT priority; +UINT steps; +UINT consistent; +#ifdef TX_THREAD_SMP_MAX_CORES +UINT core; +#endif + + + consistent = TX_TRUE; + + TX_DISABLE + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + + head_ptr = _tx_thread_priority_list[priority]; + +#if TX_MAX_PRIORITIES <= 32 + + /* A priority with a ready list must have its map bit set, and a + priority with no ready list must not. */ + if (head_ptr == TX_NULL) + { + if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) != ((ULONG) 0)) + { + consistent = TX_FALSE; + } + } + else + { + if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) == ((ULONG) 0)) + { + consistent = TX_FALSE; + } + } +#endif + + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT)) + { + + /* Every thread on a ready list must be a live thread, must be + ready, and must agree that this is its priority. */ + if (walk_ptr -> tx_thread_id != TX_THREAD_ID) + { + consistent = TX_FALSE; + } + + if ((walk_ptr -> tx_thread_state != TX_READY) && (walk_ptr != transitioning)) + { + consistent = TX_FALSE; + } + + if (walk_ptr -> tx_thread_priority != priority) + { + consistent = TX_FALSE; + } + + /* The ring must be doubly linked. */ + if (walk_ptr -> tx_thread_ready_next -> tx_thread_ready_previous != walk_ptr) + { + consistent = TX_FALSE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + + /* A ring that did not close inside the bound is broken. */ + if (walk_ptr != TX_NULL) + { + consistent = TX_FALSE; + } + } + + /* Whatever the scheduler has selected to execute must be a member of the + ready list that its own priority field names. */ +#ifdef TX_THREAD_SMP_MAX_CORES + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + + execute_ptr = _tx_thread_execute_ptr[core]; + + if (execute_ptr != TX_NULL) + { + if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE) + { + consistent = TX_FALSE; + } + } + } +#else + + execute_ptr = _tx_thread_execute_ptr; + + if (execute_ptr != TX_NULL) + { + if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE) + { + consistent = TX_FALSE; + } + } +#endif + + TX_RESTORE + + return(consistent); +} + + +static UINT scheduler_consistent(void) +{ + return(scheduler_consistent_except(TX_NULL)); +} + + +/* Report whether thread_ptr is absent from every ready list and from every + execute slot, which is what a completed or terminated thread must be once + its transition has finished. */ + +static UINT fully_detached(TX_THREAD *thread_ptr) +{ + +TX_INTERRUPT_SAVE_AREA + +UINT priority; +UINT detached; +#ifdef TX_THREAD_SMP_MAX_CORES +UINT core; +#endif + + + detached = TX_TRUE; + + TX_DISABLE + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + if (ready_ring_contains(priority, thread_ptr) == TX_TRUE) + { + detached = TX_FALSE; + } + } + +#ifdef TX_THREAD_SMP_MAX_CORES + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + if (_tx_thread_execute_ptr[core] == thread_ptr) + { + detached = TX_FALSE; + } + } +#else + + if (_tx_thread_execute_ptr == thread_ptr) + { + detached = TX_FALSE; + } +#endif + + TX_RESTORE + + return(detached); +} + + +static void arm_probe(UINT probe_delete, UINT probe_reset, UINT probe_misc, UINT probe_reuse) +{ + + probe_state = PROBE_NOT_ATTEMPTED; + probe_suspending = PROBE_NOT_ATTEMPTED; + probe_delete_status = PROBE_NOT_ATTEMPTED; + probe_core_delete_status = PROBE_NOT_ATTEMPTED; + probe_reset_status = PROBE_NOT_ATTEMPTED; + probe_core_reset_status = PROBE_NOT_ATTEMPTED; + probe_resume_status = PROBE_NOT_ATTEMPTED; + probe_suspend_status = PROBE_NOT_ATTEMPTED; + probe_terminate_status = PROBE_NOT_ATTEMPTED; + probe_create_status = PROBE_NOT_ATTEMPTED; + probe_consistent = PROBE_NOT_ATTEMPTED; + probe_ran = TX_FALSE; + delete_succeeded = TX_FALSE; + + probe_delete_enabled = probe_delete; + probe_reset_enabled = probe_reset; + probe_misc_enabled = probe_misc; + probe_reuse_enabled = probe_reuse; +} + + +/* The exit notification callback. Everything it learns is recorded for the + controller to check; it prints nothing and it never suspends, so it obeys + the one restriction the manual places on a notification callback. */ + +static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type) +{ + + /* Only the victim's notifications are of interest. Its control block is + reused across scenarios, so the pointer is the identity. */ + if (thread_ptr != &victim) + { + return; + } + + if (type == TX_THREAD_ENTRY) + { + callback_entry_counter++; + last_notify_type = TX_THREAD_ENTRY; + return; + } + + if (type != TX_THREAD_EXIT) + { + return; + } + + callback_exit_counter++; + + if (last_notify_type == TX_THREAD_ENTRY) + { + exit_saw_entry_first = TX_TRUE; + } + + last_notify_type = TX_THREAD_EXIT; + + if ((probe_delete_enabled == TX_FALSE) && (probe_reset_enabled == TX_FALSE) && + (probe_misc_enabled == TX_FALSE)) + { + return; + } + + probe_ran = TX_TRUE; + + /* What the kernel has published about the thread at this point. */ + probe_state = thread_ptr -> tx_thread_state; + probe_suspending = thread_ptr -> tx_thread_suspending; + + /* None of the services below has a suspension option, which is the only + thing the manual forbids a callback from using, so every one of them is + reachable from here. + + Where a service is expected to refuse, the core service is called after + the _txe_ wrapper as well. The wrapper is what an ordinary build + reaches; the core service is what a TX_DISABLE_ERROR_CHECKING build + reaches directly, and the refusal has to live there so that disabling + error checking cannot disable it. The core call is made only when the + wrapper already refused, so that a kernel which allows the operation is + not asked to perform it twice. */ + + if (probe_delete_enabled == TX_TRUE) + { + + probe_delete_status = tx_thread_delete(thread_ptr); + + if (probe_delete_status == TX_SUCCESS) + { + delete_succeeded = TX_TRUE; + } + else + { + + probe_core_delete_status = _tx_thread_delete(thread_ptr); + + if (probe_core_delete_status == TX_SUCCESS) + { + delete_succeeded = TX_TRUE; + } + } + + /* Recreate the control block only if the delete unexpectedly + succeeded. On a fixed kernel this never runs. On an unfixed one it + is the lawful next step after a successful delete, and it is what + turns the window into an observable inconsistency: tx_thread_create + zeroes the whole control block, and the auto-start then links it + onto RECREATE_PRIORITY's ready list while the old incarnation is + still the head of VICTIM_PRIORITY's. + + RECREATE_PRIORITY is the lower of the two so that the stale head is + the one the scheduler selects from, and so that the ring at the new + priority has a single member. Recreating at the same priority would + instead have the auto-start write through the control block's + freshly zeroed ready_previous pointer, which is a fault rather than + an assertion. */ + if ((delete_succeeded == TX_TRUE) && (probe_reuse_enabled == TX_TRUE)) + { + probe_create_status = tx_thread_create(thread_ptr, "replacement", + replacement_entry, 0, + replacement_stack, + TEST_STACK_SIZE_PRINTF, + RECREATE_PRIORITY, RECREATE_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + } + } + + if ((probe_reset_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE)) + { + + probe_reset_status = tx_thread_reset(thread_ptr); + + if (probe_reset_status != TX_SUCCESS) + { + probe_core_reset_status = _tx_thread_reset(thread_ptr); + } + + /* Follow the reset with the resume that the reset would have unlocked. + On a fixed kernel the state is still terminal and the resume is + refused. On an unfixed one the reset has moved the state to + TX_SUSPENDED, the resume finds a suspending thread whose state is no + longer terminal, clears the flag and puts the thread back on the + ready list -- and the outer service then skips the removal and + reports success for a termination that did not happen. */ + probe_resume_status = tx_thread_resume(thread_ptr); + } + + if ((probe_misc_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE)) + { + probe_suspend_status = tx_thread_suspend(thread_ptr); + probe_terminate_status = tx_thread_terminate(thread_ptr); + } + + probe_consistent = scheduler_consistent_except(thread_ptr); +} + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_thread_exit_callback_transition_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + + /* Put first available memory address into a character pointer. */ + pointer = (CHAR *) first_unused_memory; + + status = tx_thread_create(&controller, "controller", controller_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + CONTROLLER_PRIORITY, CONTROLLER_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_create(&peer_a, "peer a", peer_entry, 1, + pointer, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_create(&peer_b, "peer b", peer_entry, 2, + pointer, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + /* The victim's control block is created and destroyed repeatedly, so its + two stacks are set aside here rather than per scenario. */ + victim_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + replacement_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_semaphore_create(&victim_semaphore, "victim semaphore", 0); + status += tx_mutex_create(&victim_mutex, "victim mutex", TX_INHERIT); + + if (status != TX_SUCCESS) + { + + printf("Running Thread Exit Callback Transition Test............... ERROR #1\n"); + test_control_return(1); + } + +#ifdef TX_THREAD_SMP_MAX_CORES + + /* Confine every thread this test drives to core 0. The measurements below + are of global scheduler structures at chosen moments, and letting the + peers and the victim run on other cores would make them a race against + the test rather than a property of it. A victim executing on a + different core when termination begins is deliberately covered by a + separate SMP test, so that the deterministic cases here cannot be made + flaky by it. */ + status = tx_thread_smp_core_exclude(&controller, 0xE); + status += tx_thread_smp_core_exclude(&peer_a, 0xE); + status += tx_thread_smp_core_exclude(&peer_b, 0xE); + + if (status != TX_SUCCESS) + { + + printf("Running Thread Exit Callback Transition Test............... ERROR #2\n"); + test_control_return(1); + } +#endif +} + + +/* Create the victim over its own control block, with the exit callback + registered. The notify status is folded into the result according to what + the configuration supports. */ + +static UINT create_victim(UINT priority, UINT auto_start) +{ + +UINT status; +UINT notify_status; + + + status = tx_thread_create(&victim, "victim", victim_entry, 0, + victim_stack, TEST_STACK_SIZE_PRINTF, + priority, priority, TX_NO_TIME_SLICE, auto_start); + + notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + if (notify_status != TX_SUCCESS) + { + status = notify_status; + } +#else + + /* Notification is compiled out, so the registration is expected to be + refused and the callback never runs. */ + if (notify_status != TX_FEATURE_NOT_ENABLED) + { + status = notify_status; + } +#endif + +#ifdef TX_THREAD_SMP_MAX_CORES + + if (status == TX_SUCCESS) + { + status = tx_thread_smp_core_exclude(&victim, 0xE); + } +#endif + + return(status); +} + + +/* The gate between scenarios. A kernel that has already failed an expectation + has, in these scenarios, already left the scheduler inconsistent, and + driving it further would report nothing more while risking a wedge instead + of a verdict. */ + +static UINT ok_so_far(void) +{ + +UINT result; + + + result = TX_FALSE; + + if (expectations_failed == 0) + { + result = TX_TRUE; + } + + return(result); +} + + +static void controller_entry(ULONG thread_input) +{ + +ULONG expected_exits; +ULONG expected_entries; +UINT index; + + + (void) thread_input; + + controller_counter++; + expected_exits = 0; + expected_entries = 0; + + /* Inform user. */ + printf("Running Thread Exit Callback Transition Test............... "); + + /* The scheduler must be self-consistent before anything is measured, so + that a later failure is attributable to this test. */ + expect(scheduler_consistent(), 10); + + /* ------------------------------------------------------------------- */ + /* Scenario A: terminate a ready target that is not the current thread, */ + /* with two peers ready at the same priority. The callback attempts */ + /* the delete, and recreates the control block if it succeeded. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + peers_run = TX_TRUE; + victim_hold = TX_TRUE; + + expect_status(tx_thread_resume(&peer_a), TX_SUCCESS, 11); + expect_status(tx_thread_resume(&peer_b), TX_SUCCESS, 12); + + arm_probe(TX_TRUE, TX_FALSE, TX_FALSE, TX_TRUE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 13); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim and both peers become ready and start spinning, so + that the victim's unlink is from a multi-member ring. */ + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_READY, 14); + expect(ready_ring_contains(VICTIM_PRIORITY, &victim), 15); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 16); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 17); + } + + if (ok_so_far() == TX_TRUE) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 18); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 19); + + /* The callback runs after the terminal state has been published, and + inside the transition, which is what the flag records. */ + expect_status(probe_state, TX_TERMINATED, 20); + expect_status(probe_suspending, TX_TRUE, 21); + + /* Both the wrapper and the core service must refuse. */ + expect_status(probe_delete_status, TX_DELETE_ERROR, 22); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 23); + + /* So the recreate never happened and nothing is inconsistent. */ + expect_status(delete_succeeded, TX_FALSE, 24); + expect_status(probe_create_status, PROBE_NOT_ATTEMPTED, 25); + expect_status(probe_consistent, TX_TRUE, 26); + expect_count(replacement_counter, 0, 27); +#endif + + /* After the service returns the termination is complete: the state is + terminal, the marker is clear, the target is on no ready list and in + no execute slot, and the peers are undisturbed. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 28); + expect_status(victim.tx_thread_suspending, TX_FALSE, 29); + expect(fully_detached(&victim), 30); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 31); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 32); + expect(scheduler_consistent(), 33); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + /* The documented behaviour is available again. */ + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 34); + } + + /* ------------------------------------------------------------------- */ + /* Scenario B: the same termination, but the callback attempts a reset */ + /* and then the resume that a successful reset would have unlocked. */ + /* */ + /* This is the leg where the transition check is the only thing */ + /* standing between the callback and a reset of a thread that is still */ + /* on the ready list. Without it the reset succeeds, the resume */ + /* clears the suspending flag and restores TX_READY, the outer service */ + /* then finds the flag clear and skips the removal, and */ + /* tx_thread_terminate returns TX_SUCCESS for a runnable thread. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_TRUE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 40); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_READY, 41); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 42); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 43); + expect_status(probe_state, TX_TERMINATED, 44); + expect_status(probe_suspending, TX_TRUE, 45); + + /* The discriminating leg. The target is not the current thread and + its published state is terminal, so both of the conditions the + service tested before this fix are satisfied. */ + expect_status(probe_reset_status, TX_NOT_DONE, 46); + expect_status(probe_core_reset_status, TX_NOT_DONE, 47); + + /* With the reset refused, the state is still terminal, so the resume + cannot reach the branch that would void the termination. */ + expect_status(probe_resume_status, TX_RESUME_ERROR, 48); + expect_status(probe_consistent, TX_TRUE, 49); +#endif + + /* And the termination stands. A kernel that let the callback reset + and resume the target arrives here with the victim ready to run. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 50); + expect_status(victim.tx_thread_suspending, TX_FALSE, 51); + expect(fully_detached(&victim), 52); + expect(scheduler_consistent(), 53); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 54); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 55); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 56); + } + + /* Retire the peers, so that the ring drops to a single member and then to + none, and check consistency across both unlinks. */ + + if (ok_so_far() == TX_TRUE) + { + + peers_run = TX_FALSE; + tx_thread_sleep(3); + + expect_status(peer_a.tx_thread_state, TX_COMPLETED, 57); + expect_status(peer_b.tx_thread_state, TX_COMPLETED, 58); + expect(scheduler_consistent(), 59); + expect_status(tx_thread_delete(&peer_a), TX_SUCCESS, 60); + expect_status(tx_thread_delete(&peer_b), TX_SUCCESS, 61); + } + + /* ------------------------------------------------------------------- */ + /* Scenario C: terminate a target suspended on an object, so that the */ + /* non-ready branch of _tx_thread_terminate runs -- suspension cleanup, */ + /* the terminated extension, the callback, and mutex release, in that */ + /* order. That branch used to clear the marker before the callback */ + /* rather than after the last dereference of the target. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE); + victim_hold = TX_FALSE; + victim_take_mutex = TX_TRUE; + victim_wait_semaphore = TX_TRUE; + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 70); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim take the mutex and suspend on the semaphore. */ + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_SEMAPHORE_SUSP, 71); + expect_pointer(victim_mutex.tx_mutex_owner, &victim, 72); + } + + if (ok_so_far() == TX_TRUE) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 73); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 74); + expect_status(probe_state, TX_TERMINATED, 75); + + /* The discriminating leg for this branch. The marker used to be + cleared before this point, which left the callback free to delete or + reset a control block that the service still had mutex-release + processing to do against. */ + expect_status(probe_suspending, TX_TRUE, 76); + expect_status(probe_delete_status, TX_DELETE_ERROR, 77); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 78); + expect_status(probe_reset_status, TX_NOT_DONE, 79); + expect_status(probe_core_reset_status, TX_NOT_DONE, 80); + expect_status(probe_resume_status, TX_RESUME_ERROR, 81); + expect_status(delete_succeeded, TX_FALSE, 82); + expect_status(probe_consistent, TX_TRUE, 83); +#endif + + /* The marker is cleared at the safe point, after the mutex release, + and the mutex the terminated thread owned has been given up. The + value of the flag during the release itself is not observable from + application code; what is observable is that there is exactly one + clear site and that it sits downstream of the release. */ + expect_status(victim.tx_thread_suspending, TX_FALSE, 84); + expect_pointer(victim_mutex.tx_mutex_owner, TX_NULL, 85); + expect_count((unsigned long) victim_semaphore.tx_semaphore_suspended_count, 0, 86); + expect(fully_detached(&victim), 87); + expect(scheduler_consistent(), 88); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 89); + } + + /* ------------------------------------------------------------------- */ + /* Scenario D: natural completion, victim alone at its priority. */ + /* */ + /* The callback here runs on the completing thread itself, so a */ + /* callback-side delete that succeeds destroys the host thread on the */ + /* simulation ports. This scenario is therefore placed after the ones */ + /* that reach an assertion, and the gate above stops the run before it */ + /* if any of them has already failed. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_TRUE, TX_TRUE, TX_TRUE, TX_FALSE); + victim_take_mutex = TX_FALSE; + victim_wait_semaphore = TX_FALSE; + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 100); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim run to completion, so that its callback runs inside + the completion transition. */ + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 101); + expect_status(probe_state, TX_COMPLETED, 102); + expect_status(probe_suspending, TX_TRUE, 103); + expect_status(probe_delete_status, TX_DELETE_ERROR, 104); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 105); + + /* Reset of the running thread is refused for two independent reasons + here -- it is the current thread, and its transition is in progress + -- so this leg is a guard rather than a discriminator. Scenario B + is where the transition check is the only thing refusing a reset. */ + expect_status(probe_reset_status, TX_NOT_DONE, 106); + expect_status(probe_core_reset_status, TX_NOT_DONE, 107); + + /* A completed thread is neither resumable nor suspendable, and + terminating one is a documented no-op. */ + expect_status(probe_resume_status, TX_RESUME_ERROR, 108); + expect_status(probe_suspend_status, TX_SUSPEND_ERROR, 109); + expect_status(probe_terminate_status, TX_SUCCESS, 110); + expect_status(delete_succeeded, TX_FALSE, 111); + expect_status(probe_consistent, TX_TRUE, 112); +#endif + + /* The transition has finished, so the calls the callback was refused + must succeed. The refusal is transient and nothing has become + permanently undeletable. */ + expect_status(victim.tx_thread_state, TX_COMPLETED, 113); + expect_status(victim.tx_thread_suspending, TX_FALSE, 114); + expect(fully_detached(&victim), 115); + expect(scheduler_consistent(), 116); + expect(((victim_counter > 0) ? TX_TRUE : TX_FALSE), 117); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + /* Reset, so that the documented reset of a completed thread is covered + on a target whose transition has finished, and so that the + reset/terminate/delete order is exercised on this branch too. */ + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 118); + expect_status(victim.tx_thread_state, TX_SUSPENDED, 119); + expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 120); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 121); + expect_status(victim.tx_thread_suspending, TX_FALSE, 122); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 123); + } + + /* A safe post-completion recreate at a different priority, which is the + lifecycle the callback was refused, performed at the point it is + legitimate. It exercises the priority map and the list heads for a + recreate that moves priority as well as for one that does not. */ + + if (ok_so_far() == TX_TRUE) + { + + expect_status(create_victim(RECREATE_PRIORITY, TX_AUTO_START), TX_SUCCESS, 124); + + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_COMPLETED, 125); + expect(scheduler_consistent(), 126); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 127); + } + + /* ------------------------------------------------------------------- */ + /* Scenario E: self termination. The callback fires once, in the */ + /* victim's own context, the lifecycle mutation is refused there too, */ + /* and another thread can complete the lifecycle afterwards. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + victim_self_terminate = TX_TRUE; + expected_exits = callback_exit_counter; + + arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 130); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* Exactly one exit notification, even though the thread terminated + itself from inside its own entry function. */ + expect_count(callback_exit_counter, expected_exits + 1, 131); + expect_status(probe_state, TX_TERMINATED, 132); + expect_status(probe_suspending, TX_TRUE, 133); + expect_status(probe_delete_status, TX_DELETE_ERROR, 134); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 135); + expect_status(probe_reset_status, TX_NOT_DONE, 136); + expect_status(delete_succeeded, TX_FALSE, 137); + expect_status(probe_consistent, TX_TRUE, 138); +#else + + expect_count(callback_exit_counter, expected_exits, 139); +#endif + + expect_status(victim.tx_thread_state, TX_TERMINATED, 140); + expect_status(victim.tx_thread_suspending, TX_FALSE, 141); + expect(fully_detached(&victim), 142); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 143); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 144); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 145); + expect(scheduler_consistent(), 146); + } + + /* ------------------------------------------------------------------- */ + /* Scenario F: a benign callback is unaffected. The notification */ + /* ordering and counts are what they were. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + victim_self_terminate = TX_FALSE; + expected_exits = callback_exit_counter; + expected_entries = callback_entry_counter; + exit_saw_entry_first = TX_FALSE; + last_notify_type = PROBE_NOT_ATTEMPTED; + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 150); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* This incarnation ran and completed, so it reported exactly one entry + and one exit, and the entry came first. The transition marker has + changed neither how often notifications are delivered nor in what + order. */ + expect_count(callback_exit_counter, expected_exits + 1, 151); + expect_count(callback_entry_counter, expected_entries + 1, 152); + expect_status(exit_saw_entry_first, TX_TRUE, 153); +#else + + expect_count(callback_exit_counter, expected_exits, 154); + expect_count(callback_entry_counter, expected_entries, 155); +#endif + + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 156); + expect(scheduler_consistent(), 157); + } + + /* ------------------------------------------------------------------- */ + /* Scenario G: state and boundary cases. A thread that never entered a */ + /* terminal state, and one that is already fully detached, behave as */ + /* documented -- the new refusal applies only while the marker is set. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + expect_status(create_victim(VICTIM_PRIORITY, TX_DONT_START), TX_SUCCESS, 170); + + /* Suspended, never run: neither state is terminal, so both services + are refused for the reason they always were, and the marker is not + set on a thread that is not transitioning. */ + expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 171); + expect_status(tx_thread_reset(&victim), TX_NOT_DONE, 172); + expect_status(victim.tx_thread_suspending, TX_FALSE, 173); + + /* Terminated from a suspended state: deletable immediately, and + repeatedly resettable and re-terminable in between. */ + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 174); + expect_status(victim.tx_thread_suspending, TX_FALSE, 175); + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 176); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 177); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 178); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 179); + expect(scheduler_consistent(), 180); + } + + if (expectations_failed != 0) + { + + printf("ERROR #%u (%lu of %lu expectations failed:", + first_failure, expectations_failed, expectations_checked); + + for (index = 0; index < failed_tag_count; index++) + { + printf(" %u", failed_tags[index]); + } + + if (expectations_failed > ((unsigned long) failed_tag_count)) + { + printf(" ..."); + } + + printf(")\n"); + fflush(stdout); + test_control_return(1); + } + else + { + printf("SUCCESS!\n"); + test_control_return(0); + } +} + + +static void victim_entry(ULONG thread_input) +{ + + (void) thread_input; + + victim_counter++; + + if (victim_take_mutex == TX_TRUE) + { + tx_mutex_get(&victim_mutex, TX_NO_WAIT); + } + + if (victim_wait_semaphore == TX_TRUE) + { + + /* Suspend on an object, so that a termination takes the non-ready + branch and runs the suspension cleanup routine. */ + tx_semaphore_get(&victim_semaphore, TX_WAIT_FOREVER); + } + + if (victim_self_terminate == TX_TRUE) + { + + /* This does not return: the termination detaches this thread and the + preemption check at the end of the service switches away from it. */ + tx_thread_terminate(tx_thread_identify()); + } + + while (victim_hold == TX_TRUE) + { + + /* Stay ready, so that a terminate issued by another thread finds this + thread on the ready list. */ + tx_thread_relinquish(); + } + + /* Returning enters the completion transition. */ +} + + +static void peer_entry(ULONG thread_input) +{ + + if (thread_input == 1) + { + peer_a_counter++; + } + else + { + peer_b_counter++; + } + + while (peers_run == TX_TRUE) + { + + /* Stay ready at the victim's priority, so that the victim's ready ring + has more than one member. */ + tx_thread_relinquish(); + } +} + + +static void replacement_entry(ULONG thread_input) +{ + + (void) thread_input; + + replacement_counter++; + + /* Only reached on a kernel that let the callback recreate the control + block. Suspend rather than return, so that the run does not depend on a + second completion transition over a control block already known to be + inconsistent. */ + tx_thread_suspend(tx_thread_identify()); +} 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 000000000..fe6acb7e5 --- /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 +#include + +/* 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); +} diff --git a/test/tx/thread_transition/threadx_thread_transition_host_test_port.h b/test/tx/thread_transition/threadx_thread_transition_host_test_port.h new file mode 100644 index 000000000..204933e3b --- /dev/null +++ b/test/tx/thread_transition/threadx_thread_transition_host_test_port.h @@ -0,0 +1,120 @@ +/*************************************************************************** + * 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 host test port shim */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +/* TX_NOT_INTERRUPTABLE and TX_DISABLE_ERROR_CHECKING are not among the five + build configurations either regression tree compiles, and both trees build + the whole ThreadX library once per configuration, so there is no way to + reach those two from inside them. The completion and termination + transitions have code that only exists under TX_NOT_INTERRUPTABLE -- the + callback there runs with interrupts disabled and the transition marker has + to be set and cleared explicitly rather than by the interruptable + suspension path -- and that code is exactly where a marker cleared at the + wrong point would make a normally completed thread permanently undeletable. + It therefore needs a test rather than a compile. + + This harness compiles the four sources that change, plus the two error + wrappers whose absence a TX_DISABLE_ERROR_CHECKING build is defined by, + directly into a test executable, once per configuration. It is deliberately + not part of the coverage report: the same file compiled under different + feature macros has different line sets, and merging them would confuse the + union rather than add to it. The configurations the trees do build are what + the coverage figure is taken from. + + This header is force-included ahead of each source under test. It brings + the base port's headers in first, so every declaration a source needs is the + port's own, and then replaces the primitives that would otherwise reach the + host's pthread emulation: + + - the interrupt lock becomes a counter, which is also how the test asserts + that the transition really did run with interrupts disabled and left the + nesting balanced, + - the delete and reset port completions become recorders, because on this + port they cancel and join the pthread backing the thread, which is not + something a host test that owns no such thread can be asked to do. */ + +#ifndef THREADX_THREAD_TRANSITION_HOST_TEST_PORT_H +#define THREADX_THREAD_TRANSITION_HOST_TEST_PORT_H + +#define TX_SOURCE_CODE + +#include "tx_api.h" + +#undef TX_INTERRUPT_SAVE_AREA +#undef TX_DISABLE +#undef TX_RESTORE +#undef TX_THREAD_DELETE_PORT_COMPLETION +#undef TX_THREAD_RESET_PORT_COMPLETION + +/* Nesting depth of the stand-in interrupt lock, the deepest it has reached, + and the number of times a restore was issued without a matching disable. + Defined by the test, which is the only translation unit that reads them. */ + +extern unsigned int test_interrupt_disable_depth; +extern unsigned int test_interrupt_disable_max_depth; +extern unsigned int test_interrupt_restore_underflows; + +/* Counts of the two port completions, and the interrupt depth each was + entered at. */ + +extern unsigned int test_delete_port_completion_count; +extern unsigned int test_reset_port_completion_count; + +#define TX_INTERRUPT_SAVE_AREA + +#define TX_DISABLE \ + { \ + test_interrupt_disable_depth++; \ + if (test_interrupt_disable_depth > test_interrupt_disable_max_depth) \ + { \ + test_interrupt_disable_max_depth = test_interrupt_disable_depth; \ + } \ + } + +#define TX_RESTORE \ + { \ + if (test_interrupt_disable_depth == 0U) \ + { \ + test_interrupt_restore_underflows++; \ + } \ + else \ + { \ + test_interrupt_disable_depth--; \ + } \ + } + +#define TX_THREAD_DELETE_PORT_COMPLETION(thread_ptr) \ + { \ + (void) (thread_ptr); \ + test_delete_port_completion_count++; \ + } + +#define TX_THREAD_RESET_PORT_COMPLETION(thread_ptr) \ + { \ + (void) (thread_ptr); \ + test_reset_port_completion_count++; \ + } + +#endif