diff options
Diffstat (limited to 'test/tx/regression')
30 files changed, 2462 insertions, 55 deletions
diff --git a/test/tx/regression/testcontrol.c b/test/tx/regression/testcontrol.c index e2b3f725..bec50ee7 100644 --- a/test/tx/regression/testcontrol.c +++ b/test/tx/regression/testcontrol.c @@ -10,7 +10,7 @@ /***************************************************************************/ /* This is the test control routine of the ThreadX kernel. All tests are dispatched from this routine. */ -// Some portions generated by Codex (gpt 5.4). +// Portions of this file were generated with AI assistance. #include "tx_api.h" #include <stdio.h> @@ -69,6 +69,7 @@ TX_BLOCK_POOL init_block_pool; ULONG init_block_pool_area[50]; TX_EVENT_FLAGS_GROUP init_event_flags; TX_TIMER init_timer; +TX_TIMER init_timer_change; TX_THREAD init_test_thread; TX_THREAD second_test_thread; #ifndef TX_TIMER_PROCESS_IN_ISR @@ -218,6 +219,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 *); @@ -229,6 +231,7 @@ void threadx_thread_create_preemption_threshold_application_define(void *); void threadx_thread_information_application_define(void *); void threadx_thread_reset_application_define(void *); void threadx_thread_stack_checking_application_define(void *); +void threadx_thread_stack_fill_value_application_define(void *); void threadx_time_get_set_application_define(void *); @@ -335,6 +338,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, @@ -348,6 +352,7 @@ TEST_ENTRY test_control_tests[] = threadx_thread_information_application_define, threadx_thread_reset_application_define, threadx_thread_stack_checking_application_define, + threadx_thread_stack_fill_value_application_define, threadx_time_get_set_application_define, @@ -565,6 +570,11 @@ TX_THREAD *thread_ptr; test_timer_create_init = tx_timer_create(&init_timer, "init timer", init_timer_entry, 0x5678, 100, 200, TX_AUTO_ACTIVATE); + /* Test timer change from initialization. */ + test_timer_create_init += tx_timer_create(&init_timer_change, "init timer change", init_timer_entry, 0x5678, + 100, 200, TX_NO_ACTIVATE); + test_timer_create_init += tx_timer_change(&init_timer_change, 50, 100); + /* Test calling tx_thread_relinquish to see if the error checking throws it out. */ tx_thread_relinquish(); #endif diff --git a/test/tx/regression/threadx_block_memory_basic_test.c b/test/tx/regression/threadx_block_memory_basic_test.c index 94cc8793..d7c609d2 100644 --- a/test/tx/regression/threadx_block_memory_basic_test.c +++ b/test/tx/regression/threadx_block_memory_basic_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test simple memory block pool creation, deletion, and allocates and releases. */ @@ -72,7 +74,7 @@ void test_control_return(UINT status); /* Prototype direct call to block pool core service. */ -UINT _tx_block_pool_create(TX_BLOCK_POOL *pool_ptr, CHAR *name_ptr, ULONG block_size, +UINT _tx_block_pool_create(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG block_size, VOID *pool_start, ULONG pool_size); diff --git a/test/tx/regression/threadx_block_memory_error_detection_test.c b/test/tx/regression/threadx_block_memory_error_detection_test.c index 41148120..b0303b76 100644 --- a/test/tx/regression/threadx_block_memory_error_detection_test.c +++ b/test/tx/regression/threadx_block_memory_error_detection_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test error detection for simple memory block operations. */ #include <stdio.h> @@ -29,7 +31,7 @@ static CHAR *pointer; static void thread_0_entry(ULONG thread_input); -UINT _txe_block_pool_create(TX_BLOCK_POOL *pool_ptr, CHAR *name_ptr, ULONG block_size, +UINT _txe_block_pool_create(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG block_size, VOID *pool_start, ULONG pool_size, UINT pool_control_block_size); diff --git a/test/tx/regression/threadx_block_memory_information_test.c b/test/tx/regression/threadx_block_memory_information_test.c index 82d57f40..0c04cb7e 100644 --- a/test/tx/regression/threadx_block_memory_information_test.c +++ b/test/tx/regression/threadx_block_memory_information_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test block memory information services. */ #include <stdio.h> @@ -68,7 +70,7 @@ static void thread_5_entry(ULONG thread_input); static void thread_6_entry(ULONG thread_input); /* Direct core function to bypass the error checking shell. */ -UINT _tx_block_pool_info_get(TX_BLOCK_POOL *pool_ptr, CHAR **name, ULONG *available_blocks, +UINT _tx_block_pool_info_get(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR **name, ULONG *available_blocks, ULONG *total_blocks, TX_THREAD **first_suspended, ULONG *suspended_count, TX_BLOCK_POOL **next_pool); @@ -227,7 +229,7 @@ static void thread_0_entry(ULONG thread_input) UINT status; VOID *pointer; -CHAR *name; +TX_NAME_CONST CHAR *name; ULONG available; ULONG total_blocks; TX_THREAD *first_suspended; @@ -369,7 +371,7 @@ ULONG timeouts; status += tx_block_pool_info_get(&block_pool_0, &name, &available, &total_blocks, &first_suspended, &suspended_count, &next_pool); /* Check for an error condition. */ - if ((status) || (available != block_pool_0.tx_block_pool_available) || (total_blocks != block_pool_0.tx_block_pool_total) || + if ((status) || (name != block_pool_0.tx_block_pool_name) || (available != block_pool_0.tx_block_pool_available) || (total_blocks != block_pool_0.tx_block_pool_total) || (first_suspended != &thread_4) || (suspended_count != block_pool_0.tx_block_pool_suspended_count) || (next_pool != &block_pool_0)) { diff --git a/test/tx/regression/threadx_byte_memory_basic_test.c b/test/tx/regression/threadx_byte_memory_basic_test.c index 45f92e12..6b916f67 100644 --- a/test/tx/regression/threadx_byte_memory_basic_test.c +++ b/test/tx/regression/threadx_byte_memory_basic_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test simple memory byte pool creation, deletion, and allocates and releases. */ @@ -51,7 +53,7 @@ extern UINT test_byte_pool_create_init; extern VOID (*test_isr_dispatch)(void); -UINT _txe_byte_pool_create(TX_BYTE_POOL *pool_ptr, CHAR *name_ptr, VOID *pool_start, +UINT _txe_byte_pool_create(TX_BYTE_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, VOID *pool_start, ULONG pool_size, UINT pool_control_block_size); diff --git a/test/tx/regression/threadx_byte_memory_information_test.c b/test/tx/regression/threadx_byte_memory_information_test.c index f1624a65..f65efac3 100644 --- a/test/tx/regression/threadx_byte_memory_information_test.c +++ b/test/tx/regression/threadx_byte_memory_information_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test byte memory information. */ #include <stdio.h> @@ -226,7 +228,7 @@ static void thread_0_entry(ULONG thread_input) UINT status; VOID *pointer; -CHAR *name; +TX_NAME_CONST CHAR *name; ULONG available; ULONG fragments; TX_THREAD *first_suspended; @@ -369,7 +371,7 @@ ULONG timeouts; status += tx_byte_pool_info_get(&byte_pool_0, &name, &available, &fragments, &first_suspended, &suspended_count, &next_pool); /* Check the status. */ - if ((status != TX_SUCCESS) || (available != byte_pool_0.tx_byte_pool_available) || (fragments != byte_pool_0.tx_byte_pool_fragments) || + if ((status != TX_SUCCESS) || (name != byte_pool_0.tx_byte_pool_name) || (available != byte_pool_0.tx_byte_pool_available) || (fragments != byte_pool_0.tx_byte_pool_fragments) || (first_suspended != &thread_4) || (suspended_count != byte_pool_0.tx_byte_pool_suspended_count) || (next_pool != &byte_pool_0)) { diff --git a/test/tx/regression/threadx_event_flag_basic_test.c b/test/tx/regression/threadx_event_flag_basic_test.c index 8ab990f7..fed424a6 100644 --- a/test/tx/regression/threadx_event_flag_basic_test.c +++ b/test/tx/regression/threadx_event_flag_basic_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test simple event flag group creation, deletion, gets and sets. */ @@ -57,7 +59,7 @@ static TX_EVENT_FLAGS_GROUP group_3; static void thread_0_entry(ULONG thread_input); static void thread_1_entry(ULONG thread_input); -UINT _txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, CHAR *name_ptr, UINT event_control_block_size); +UINT _txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, TX_NAME_CONST CHAR *name_ptr, UINT event_control_block_size); /* Prototype for test control return. */ diff --git a/test/tx/regression/threadx_event_flag_information_test.c b/test/tx/regression/threadx_event_flag_information_test.c index 45e547f9..1c7cfd2d 100644 --- a/test/tx/regression/threadx_event_flag_information_test.c +++ b/test/tx/regression/threadx_event_flag_information_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test the event flag group information gathering services. */ #include <stdio.h> @@ -134,7 +136,7 @@ static void thread_0_entry(ULONG thread_input) UINT status; ULONG actual_events; -CHAR *name; +TX_NAME_CONST CHAR *name; ULONG current_flags; TX_THREAD *first_suspended; ULONG suspended_count; @@ -356,7 +358,7 @@ ULONG timeouts; status += tx_event_flags_info_get(&group_0, &name, ¤t_flags, &first_suspended, &suspended_count, &next_group); /* Check the status. */ - if ((status != TX_SUCCESS) || (current_flags != group_0.tx_event_flags_group_current) || (first_suspended != TX_NULL) || (suspended_count != 0) || (next_group != &group_1)) + if ((status != TX_SUCCESS) || (name != group_0.tx_event_flags_group_name) || (current_flags != group_0.tx_event_flags_group_current) || (first_suspended != TX_NULL) || (suspended_count != 0) || (next_group != &group_1)) { /* Event flag error. */ diff --git a/test/tx/regression/threadx_initialize_kernel_setup_test.c b/test/tx/regression/threadx_initialize_kernel_setup_test.c index 057ac223..b69a26bd 100644 --- a/test/tx/regression/threadx_initialize_kernel_setup_test.c +++ b/test/tx/regression/threadx_initialize_kernel_setup_test.c @@ -10,7 +10,7 @@ /***************************************************************************/ /* This test is designed to test kernel setup functionality in ThreadX. */ -// Some portions generated by Codex (gpt 5.4). +// Portions of this file were generated with AI assistance. #include <stdio.h> #include <stdlib.h> diff --git a/test/tx/regression/threadx_mutex_basic_test.c b/test/tx/regression/threadx_mutex_basic_test.c index bb81eb9a..5a652843 100644 --- a/test/tx/regression/threadx_mutex_basic_test.c +++ b/test/tx/regression/threadx_mutex_basic_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test the mutex create/delete and immediate return gets and puts. */ @@ -69,7 +71,7 @@ static void thread_2_entry(ULONG thread_input); static void thread_3_entry(ULONG thread_input); static void thread_4_entry(ULONG thread_input); -UINT _txe_mutex_create(TX_MUTEX *mutex_ptr, CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size); +UINT _txe_mutex_create(TX_MUTEX *mutex_ptr, TX_NAME_CONST CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size); /* Prototype for test control return. */ diff --git a/test/tx/regression/threadx_mutex_information_test.c b/test/tx/regression/threadx_mutex_information_test.c index 3829122c..fea78c04 100644 --- a/test/tx/regression/threadx_mutex_information_test.c +++ b/test/tx/regression/threadx_mutex_information_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test the mutex information services. */ #include <stdio.h> @@ -134,7 +136,7 @@ static void thread_0_entry(ULONG thread_input) { UINT status; -CHAR *name; +TX_NAME_CONST CHAR *name; ULONG count; TX_THREAD *owner; TX_THREAD *first_suspended; @@ -300,7 +302,7 @@ ULONG inheritances; status += tx_mutex_info_get(&mutex_2, &name, &count, &owner, &first_suspended, &suspended_count, &next_mutex); /* Check status. */ - if ((status != TX_SUCCESS) || (count != mutex_2.tx_mutex_ownership_count) || (owner != mutex_2.tx_mutex_owner) || + if ((status != TX_SUCCESS) || (name != mutex_2.tx_mutex_name) || (count != mutex_2.tx_mutex_ownership_count) || (owner != mutex_2.tx_mutex_owner) || (first_suspended != mutex_2.tx_mutex_suspension_list) || (suspended_count != mutex_2.tx_mutex_suspended_count) || (next_mutex != mutex_2.tx_mutex_created_next)) { diff --git a/test/tx/regression/threadx_queue_basic_one_word_test.c b/test/tx/regression/threadx_queue_basic_one_word_test.c index 1abb6986..58142471 100644 --- a/test/tx/regression/threadx_queue_basic_one_word_test.c +++ b/test/tx/regression/threadx_queue_basic_one_word_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test immediate response queue services including create and delete. This test is for queue sizes of 1 ULONG. Two queues are used one with a capacity of 1 message and another with a capacity of 3 messages. */ @@ -51,7 +53,7 @@ static void thread_0_entry(ULONG thread_input); static void thread_1_entry(ULONG thread_input); -UINT _txe_queue_create(TX_QUEUE *queue_ptr, CHAR *name_ptr, UINT message_size, +UINT _txe_queue_create(TX_QUEUE *queue_ptr, TX_NAME_CONST CHAR *name_ptr, UINT message_size, VOID *queue_start, ULONG queue_size, UINT queue_control_block_size); diff --git a/test/tx/regression/threadx_queue_information_test.c b/test/tx/regression/threadx_queue_information_test.c index f884bd0d..8929c465 100644 --- a/test/tx/regression/threadx_queue_information_test.c +++ b/test/tx/regression/threadx_queue_information_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test the queue information services. */ #include <stdio.h> @@ -102,7 +104,7 @@ UINT status; ULONG source_message = 0x12345678UL; ULONG dest_message; ULONG expected_message; -CHAR *name; +TX_NAME_CONST CHAR *name; ULONG enqueued; ULONG available_storage; TX_THREAD *first_suspended; @@ -409,7 +411,7 @@ ULONG timeouts; status += tx_queue_info_get(&queue_0, &name, &enqueued, &available_storage, &first_suspended, &suspended_count, &next_queue); /* Check for errors. */ - if ((status != TX_SUCCESS) || (enqueued != queue_0.tx_queue_enqueued) || (available_storage != queue_0.tx_queue_available_storage) || + if ((status != TX_SUCCESS) || (name != queue_0.tx_queue_name) || (enqueued != queue_0.tx_queue_enqueued) || (available_storage != queue_0.tx_queue_available_storage) || (first_suspended != queue_0.tx_queue_suspension_list) || (suspended_count != queue_0.tx_queue_suspended_count) || (next_queue != queue_0.tx_queue_created_next)) { diff --git a/test/tx/regression/threadx_semaphore_basic_test.c b/test/tx/regression/threadx_semaphore_basic_test.c index 3209b586..a1a3d340 100644 --- a/test/tx/regression/threadx_semaphore_basic_test.c +++ b/test/tx/regression/threadx_semaphore_basic_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test the semaphore create/delete and immediate return gets and puts. */ #include <stdio.h> @@ -62,7 +64,7 @@ static TX_SEMAPHORE semaphore_3; static void thread_0_entry(ULONG thread_input); static void thread_1_entry(ULONG thread_input); -UINT _txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size); +UINT _txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size); /* Prototype for test control return. */ diff --git a/test/tx/regression/threadx_semaphore_information_test.c b/test/tx/regression/threadx_semaphore_information_test.c index ede276e9..c0bb3c72 100644 --- a/test/tx/regression/threadx_semaphore_information_test.c +++ b/test/tx/regression/threadx_semaphore_information_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test the semaphore information services. */ #include <stdio.h> @@ -100,7 +102,7 @@ static void thread_0_entry(ULONG thread_input) { UINT status; -CHAR *name; +TX_NAME_CONST CHAR *name; ULONG current_value; TX_THREAD *first_suspended; ULONG suspended_count; @@ -211,7 +213,7 @@ ULONG timeouts; status += tx_semaphore_info_get(&semaphore_0, &name, ¤t_value, &first_suspended, &suspended_count, &next_semaphore); /* Check status. */ - if ((status != TX_SUCCESS) || (current_value != semaphore_0.tx_semaphore_count) || + if ((status != TX_SUCCESS) || (name != semaphore_0.tx_semaphore_name) || (current_value != semaphore_0.tx_semaphore_count) || (first_suspended != semaphore_0.tx_semaphore_suspension_list) || (suspended_count != semaphore_0.tx_semaphore_suspended_count) || (next_semaphore != semaphore_0.tx_semaphore_created_next)) { diff --git a/test/tx/regression/threadx_test_port.h b/test/tx/regression/threadx_test_port.h index b8502b4d..5d79c8e8 100644 --- a/test/tx/regression/threadx_test_port.h +++ b/test/tx/regression/threadx_test_port.h @@ -32,6 +32,11 @@ typedef ALIGN_TYPE TX_TEST_POINTER_WORD; -#define TX_TEST_STORE_POINTER(a, b) (a) = ((TX_TEST_POINTER_WORD) TX_POINTER_TO_ALIGN_TYPE_CONVERT(b)) +/* TX_POINTER_TO_ALIGN_TYPE_CONVERT is defined only when TX_MISRA_ENABLE is absent, + so the conversion is written out here rather than taken from the API. This is + test scaffolding storing a pointer in a word wide enough to hold one; it is not + kernel source and carries no MISRA obligation of its own. */ + +#define TX_TEST_STORE_POINTER(a, b) (a) = ((TX_TEST_POINTER_WORD) (ALIGN_TYPE) (b)) #endif diff --git a/test/tx/regression/threadx_thread_basic_execution_test.c b/test/tx/regression/threadx_thread_basic_execution_test.c index c51a677c..ea9b1177 100644 --- a/test/tx/regression/threadx_thread_basic_execution_test.c +++ b/test/tx/regression/threadx_thread_basic_execution_test.c @@ -1,4 +1,6 @@ /***************************************************************************/ + +// Portions of this file were generated with AI assistance. /* Copyright (c) 2024 Microsoft Corporation */ /* Copyright (c) 2026 Eclipse ThreadX contributors */ /* */ @@ -71,7 +73,7 @@ extern TX_TIMER_INTERNAL *_tx_timer_expired_timer_ptr; static void thread_0_entry(ULONG task_input); -UINT _txe_thread_create(TX_THREAD *thread_ptr, CHAR *name_ptr, +UINT _txe_thread_create(TX_THREAD *thread_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*entry_function)(ULONG), ULONG entry_input, VOID *stack_start, ULONG stack_size, UINT priority, UINT preempt_threshold, @@ -401,7 +403,7 @@ VOID (*temp_mutex_release)(TX_THREAD *thread_ptr); test_thread.tx_thread_timer.tx_timer_internal_list_head = TX_NULL; test_thread.tx_thread_suspending = TX_TRUE; test_thread.tx_thread_delayed_suspend = TX_TRUE; -#if defined(_WIN64) || defined(TX_TIMER_EXTENSION_PTR_DEFINED) +#if defined(_WIN32) || defined(TX_TIMER_EXTENSION_PTR_DEFINED) { TX_TIMER_INTERNAL timeout_timer; TX_TIMER_INTERNAL *saved_expired_timer_ptr; diff --git a/test/tx/regression/threadx_thread_delayed_suspension_test.c b/test/tx/regression/threadx_thread_delayed_suspension_test.c index 2bf26421..1841458a 100644 --- a/test/tx/regression/threadx_thread_delayed_suspension_test.c +++ b/test/tx/regression/threadx_thread_delayed_suspension_test.c @@ -12,6 +12,7 @@ /* This test checks out the delayed suspension clear from tx_thread_resume. */ #include <stdio.h> +#include <time.h> #include "tx_api.h" #include "tx_thread.h" #include "tx_timer.h" @@ -235,6 +236,12 @@ static void thread_0_entry(ULONG thread_input) UINT status; +#ifndef TX_NOT_INTERRUPTABLE +#define DELAYED_SUSPENSION_SECOND_BUDGET ((ULONG) 120) + +time_t start_wall; +#endif + /* Inform user. */ printf("Running Thread Delayed Suspension Clearing Test..................... "); @@ -278,8 +285,23 @@ UINT status; /* Resume the test thread. */ tx_thread_resume(&thread_2); - /* Wait until we see the delayed suspension set flag. */ - while(delayed_suspend_set == 0) + /* Wait until we see the delayed suspension set flag. + + The flag is set by the interrupt above, and only when that interrupt lands + while thread 2 is part way through suspending. That is a narrow window, + and waiting for it without a bound leaves the test no way out when it is + not reached. How narrow it is depends on the build: this loop finishes in + between a tenth of a second and three seconds in four of the five ThreadX + configurations, and took 490 seconds in trace_build, where it was the most + expensive thing in the suite by a wide margin. + + The budget is in wall clock seconds rather than ticks. A tick arrives only + when the port's timer thread runs, so the tick clock falls behind real + time under load or instrumentation, which is exactly the configuration + that needs bounding here. */ + start_wall = time(TX_NULL); + while ((delayed_suspend_set == 0) && + (((ULONG) (time(TX_NULL) - start_wall)) <= DELAYED_SUSPENSION_SECOND_BUDGET)) { /* Abort the suspension for thread 2. */ @@ -293,7 +315,18 @@ UINT status; tx_thread_relinquish(); /* At this point, check for an error. */ - if (thread_2_counter != thread_2_counter_capture) + if (delayed_suspend_set == 0) + { + + /* The window was never reached, so thread_2_counter_capture was never + taken and the comparison below would be against a value that means + nothing. Not reaching the window is a gap in what this run covered + rather than a fault in the code under test, so say so and skip the + check that depends on it. */ + printf("(delayed suspension window not reached in %lu seconds) ", + (ULONG) (time(TX_NULL) - start_wall)); + } + else if (thread_2_counter != thread_2_counter_capture) { /* Delayed suspension error... thread kept running! */ 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 00000000..c931085c --- /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 <stdio.h> +#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/regression/threadx_thread_information_test.c b/test/tx/regression/threadx_thread_information_test.c index b5d06e71..fef3473f 100644 --- a/test/tx/regression/threadx_thread_information_test.c +++ b/test/tx/regression/threadx_thread_information_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is for the thread information services. */ #include <stdio.h> @@ -63,7 +65,7 @@ static void thread_0_entry(ULONG thread_input) { UINT status; -CHAR *name; +TX_NAME_CONST CHAR *name; UINT state; ULONG run_count; UINT priority; @@ -96,7 +98,7 @@ ULONG idle_returns; status += tx_thread_info_get(&thread_0, &name, &state, &run_count, &priority, &preemption_threshold, &time_slice, &next_thread, &suspended_thread); /* Check for error status. */ - if ((status != TX_SUCCESS) || (state != TX_READY) || (run_count != thread_0.tx_thread_run_count) || (priority != 16) || (preemption_threshold != 16) || + if ((status != TX_SUCCESS) || (name != thread_0.tx_thread_name) || (state != TX_READY) || (run_count != thread_0.tx_thread_run_count) || (priority != 16) || (preemption_threshold != 16) || (time_slice != 0) || (next_thread != thread_0.tx_thread_created_next) || (suspended_thread != thread_0.tx_thread_suspended_next)) { diff --git a/test/tx/regression/threadx_thread_misaligned_stack_test.c b/test/tx/regression/threadx_thread_misaligned_stack_test.c new file mode 100644 index 00000000..63d64775 --- /dev/null +++ b/test/tx/regression/threadx_thread_misaligned_stack_test.c @@ -0,0 +1,150 @@ +/*************************************************************************** + * 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 creates a thread whose stack does not begin on a ULONG boundary. + + tx_thread_create rounds the starting address up to the next ULONG and then + takes a ULONG off the size, so that the rounding cannot push the end of the + stack past the memory the caller supplied: + + if (new_stack_start != updated_stack_start) + { + stack_size = stack_size - (sizeof(ULONG)); + } + + Every other test in the suite hands tx_thread_create an aligned stack, so + that subtraction was never executed -- the only uncovered line in + tx_thread_create.c across all five build configurations. A misaligned stack + is legitimate: the API takes a VOID * and the alignment fix-up exists + precisely to accept one. + + The thread is then run to completion rather than merely created, because the + point of the adjustment is that the resulting stack is still usable and still + inside the caller's buffer. */ + +#include <stdio.h> +#include "tx_api.h" + + +void test_control_return(UINT status); + + +static TX_THREAD thread_0; +static TX_THREAD thread_1; + +/* Thread 1's stack is carved out of this by hand so that its start can be put + deliberately off a ULONG boundary. The extra ULONG is what the misaligned + start is offset into, so that the stack still ends inside the array after + tx_thread_create has rounded the start up. */ + +static UCHAR misaligned_area[TEST_STACK_SIZE_PRINTF + sizeof(ULONG)]; + +static ULONG thread_1_counter = 0; + + +static void thread_0_entry(ULONG thread_input); +static void thread_1_entry(ULONG thread_input); + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_thread_misaligned_stack_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; +CHAR *misaligned_stack; + + + /* Setup a pointer. */ + pointer = (CHAR *) first_unused_memory; + + status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + if (status != TX_SUCCESS) + { + + printf("Running Thread Misaligned Stack Test................................ ERROR #1\n"); + test_control_return(1); + } + + /* Push the start one byte past a ULONG boundary. The array itself is + aligned, so adding one is enough to guarantee the fix-up runs whatever + the alignment of the array turned out to be. */ + misaligned_stack = ((CHAR *) misaligned_area) + 1; + + status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0, + misaligned_stack, TEST_STACK_SIZE_PRINTF, + 15, 15, TX_NO_TIME_SLICE, TX_AUTO_START); + + if (status != TX_SUCCESS) + { + + printf("Running Thread Misaligned Stack Test................................ ERROR #2\n"); + test_control_return(1); + } +} + + +static void thread_0_entry(ULONG thread_input) +{ + + /* Thread 1 is the higher priority of the two, so this thread first gets the + processor while thread 1 is sleeping. Sleep past that, so the check below + is made against a thread that has actually finished. */ + tx_thread_sleep(5); + + /* Both increments must have happened: one before thread 1 slept and one + after it woke, which is the half that needed the stack to survive a + context switch. */ + if (thread_1_counter != ((ULONG) 2)) + { + + printf("Running Thread Misaligned Stack Test................................ ERROR #3\n"); + test_control_return(1); + } + + /* The thread that ran on the misaligned stack must have run to completion, + which is what says the adjusted stack was usable. */ + if (thread_1.tx_thread_state != TX_COMPLETED) + { + + printf("Running Thread Misaligned Stack Test................................ ERROR #4\n"); + test_control_return(1); + } + + printf("Running Thread Misaligned Stack Test................................ SUCCESS!\n"); + test_control_return(0); +} + + +static void thread_1_entry(ULONG thread_input) +{ + + /* Use the stack either side of a context switch rather than returning + straight away. */ + thread_1_counter++; + + tx_thread_sleep(1); + + thread_1_counter++; +} diff --git a/test/tx/regression/threadx_thread_multi_level_preemption_threshold_test.c b/test/tx/regression/threadx_thread_multi_level_preemption_threshold_test.c index 59ed4f4d..f7159384 100644 --- a/test/tx/regression/threadx_thread_multi_level_preemption_threshold_test.c +++ b/test/tx/regression/threadx_thread_multi_level_preemption_threshold_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test multi-level preemption threshold. The protection placed by a thread must be preserved after higher-priority thread preemption that is above the threshold. */ @@ -74,7 +76,7 @@ static TX_TIMER timer_0; /* Define counters. */ -static unsigned long timer_0_counter = 0; +static volatile unsigned long timer_0_counter = 0; static unsigned long thread_1_0_counter = 0; static unsigned long thread_2_1_counter = 0; static unsigned long thread_3_2_counter = 0; diff --git a/test/tx/regression/threadx_thread_priority_change.c b/test/tx/regression/threadx_thread_priority_change.c index 1cb52ff9..ac750a5f 100644 --- a/test/tx/regression/threadx_thread_priority_change.c +++ b/test/tx/regression/threadx_thread_priority_change.c @@ -354,6 +354,15 @@ static void thread_3_entry(ULONG thread_input) UINT old_priority; UINT loop; +/* The interrupt below only fires on a narrow window: thread 3 at priority 6, + ready, and not yet at the head of its priority list. Waiting for it without a + bound made this test hang indefinitely when the window was never hit, which + showed up in CI as a timeout carrying no information about what went wrong. + The TX_NOT_INTERRUPTABLE path already stopped after a fixed amount of work; + this gives the interruptable path the same protection. */ +UINT attempts = 0; +#define MAX_PRIORITY_CHANGE_ATTEMPTS 100000 + /* Resume threads 4. */ tx_thread_resume(&thread_4); @@ -378,7 +387,23 @@ UINT loop; if (thread_4_counter) thread_1_counter++; - } while (test_isr_dispatch); + attempts++; + + } while (test_isr_dispatch && (attempts < MAX_PRIORITY_CHANGE_ATTEMPTS)); + + /* Stop the interrupt regardless, so a window that was never hit does not + leave the handler installed for the rest of the test. */ + test_isr_dispatch = TX_NULL; + + if (attempts >= MAX_PRIORITY_CHANGE_ATTEMPTS) + { + + /* The window was not reached. That is a gap in what this test covered on + this run rather than a fault in the code under test, so say so instead + of failing, and leave the counters alone so the checks that follow + still mean what they did before. */ + printf("(priority change window not reached in %u attempts) ", attempts); + } } diff --git a/test/tx/regression/threadx_thread_stack_checking_test.c b/test/tx/regression/threadx_thread_stack_checking_test.c index 9db10d0a..2ef9a2df 100644 --- a/test/tx/regression/threadx_thread_stack_checking_test.c +++ b/test/tx/regression/threadx_thread_stack_checking_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +// Portions of this file were generated with AI assistance. + /* This test is for the thread stack checking services. */ #include <stdio.h> @@ -200,6 +202,19 @@ TX_THREAD fake_thread; fake_thread.tx_thread_stack_highest_ptr = TX_NULL; _tx_thread_stack_analyze(&fake_thread); + /* Call the stack analyze with a highest stack pointer below the start of the stack, which is + what a stack overflow or a corrupted control block looks like. This used to hang. */ + fake_thread.tx_thread_id = ((ULONG) 0x54485244); + fake_thread.tx_thread_stack_start = (void *) 0x2000; + fake_thread.tx_thread_stack_highest_ptr = (void *) 0x1000; + _tx_thread_stack_analyze(&fake_thread); + + /* Call the stack analyze with a highest stack pointer equal to the start of the stack. */ + fake_thread.tx_thread_id = ((ULONG) 0x54485244); + fake_thread.tx_thread_stack_start = (void *) 0x2000; + fake_thread.tx_thread_stack_highest_ptr = (void *) 0x2000; + _tx_thread_stack_analyze(&fake_thread); + /* Clear the pattern in thread 2's stack. */ TX_MEMSET(thread_2_stack_start, (CHAR) 0x11, TEST_STACK_SIZE_PRINTF); diff --git a/test/tx/regression/threadx_thread_stack_fill_value_test.c b/test/tx/regression/threadx_thread_stack_fill_value_test.c new file mode 100644 index 00000000..fa6eda22 --- /dev/null +++ b/test/tx/regression/threadx_thread_stack_fill_value_test.c @@ -0,0 +1,190 @@ +/***************************************************************************/ +/* 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. */ +/* */ +/* SPDX-License-Identifier: MIT */ +/***************************************************************************/ + +// Portions of this file were generated with AI assistance. + +/* This test checks that the stack fill value recorded in a thread control block is the + value that was actually written into that thread's stack. The two used to disagree + when TX_ENABLE_RANDOM_NUMBER_STACK_FILLING was enabled, because the control block was + cleared after the random value had been chosen and the stack had been filled with it, + which left the stack checking and stack analyze routines looking for a pattern that is + not the one present in the stack. */ + +#include <stdio.h> +#include "tx_api.h" + + +/* Number of probe threads created. A random fill value can legitimately come out as zero, + which hides the defect for that one thread, so several threads are created and at least + one of them is required to have produced a non-zero pattern. */ + +#define TEST_PROBE_THREADS 16 + + +static unsigned long thread_0_counter = 0; +static TX_THREAD thread_0; + +static TX_THREAD probe_thread; +static ULONG probe_stack[TX_MINIMUM_STACK / sizeof(ULONG)]; + + +/* Define task prototypes. */ + +static void thread_0_entry(ULONG task_input); + + +/* Prototype for test control return. */ + +void test_control_return(UINT status); + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_thread_stack_fill_value_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + pointer = (CHAR *) first_unused_memory; + + /* Put system definition stuff in here, e.g. thread creates and other assorted + create information. */ + + status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START); + + /* Check for status. */ + if (status != TX_SUCCESS) + { + + printf("Running Thread Stack Fill Value Test................................ ERROR #1\n"); + test_control_return(1); + } +} + + +/* Define the test threads. */ + +static void thread_0_entry(ULONG thread_input) +{ + +UINT status; +UINT i; +ULONG stack_pattern; +UINT non_zero_seen = TX_FALSE; + + + /* Increment thread 0 counter. */ + thread_0_counter++; + + /* Inform user of success getting to this test. */ + printf("Running Thread Stack Fill Value Test................................ "); + + for (i = 0; i < TEST_PROBE_THREADS; i++) + { + + /* Create a thread that is never started, so that its stack still holds nothing but + the fill pattern and the initial stack frame built at the top of the stack. */ + status = tx_thread_create(&probe_thread, "probe thread", thread_0_entry, 0, + probe_stack, sizeof(probe_stack), + 17, 17, TX_NO_TIME_SLICE, TX_DONT_START); + + /* Check for status. */ + if (status != TX_SUCCESS) + { + + printf("ERROR #2\n"); + test_control_return(1); + } + + /* Pick up the pattern the create service wrote into the unused part of the stack. */ + stack_pattern = *(((ULONG *) probe_thread.tx_thread_stack_start) + 4); + +#ifndef TX_DISABLE_STACK_FILLING +#if defined(TX_ENABLE_RANDOM_NUMBER_STACK_FILLING) && defined(TX_ENABLE_STACK_CHECKING) + + /* The control block must hold the value that was written into the stack. */ + if (probe_thread.tx_thread_stack_fill_value != stack_pattern) + { + + printf("ERROR #3\n"); + test_control_return(1); + } + + /* The random value is duplicated into all four bytes of the fill value. */ + if ((stack_pattern & 0xFFUL) != + ((stack_pattern >> 24) & 0xFFUL)) + { + + printf("ERROR #4\n"); + test_control_return(1); + } + + if (stack_pattern != ((ULONG) 0)) + { + + non_zero_seen = TX_TRUE; + } +#else + + /* Without random filling, the pattern is the fixed one. */ + if (stack_pattern != ((ULONG) 0xEFEFEFEFUL)) + { + + printf("ERROR #5\n"); + test_control_return(1); + } + + non_zero_seen = TX_TRUE; +#endif +#else + + /* Stack filling is disabled, so there is nothing to compare. */ + non_zero_seen = TX_TRUE; +#endif + + /* Terminate and delete the probe thread so that the next pass starts from a clean + slate. A thread created with TX_DONT_START is suspended rather than completed, + so it must be terminated before it can be deleted. */ + status = tx_thread_terminate(&probe_thread); + + if (status == TX_SUCCESS) + { + + status = tx_thread_delete(&probe_thread); + } + + /* Check for status. */ + if (status != TX_SUCCESS) + { + + printf("ERROR #6\n"); + test_control_return(1); + } + } + + /* A run in which every fill value came out as zero would prove nothing. */ + if (non_zero_seen != TX_TRUE) + { + + printf("ERROR #7\n"); + test_control_return(1); + } + + /* Success! */ + printf("SUCCESS!\n"); + test_control_return(0); +} diff --git a/test/tx/regression/threadx_thread_wait_abort_and_isr_test.c b/test/tx/regression/threadx_thread_wait_abort_and_isr_test.c index e15b410f..6745ad0d 100644 --- a/test/tx/regression/threadx_thread_wait_abort_and_isr_test.c +++ b/test/tx/regression/threadx_thread_wait_abort_and_isr_test.c @@ -12,6 +12,7 @@ /* This test is designed to test for simultaneous thread suspension lifting AND thread wait abort calls. */ #include <stdio.h> +#include <time.h> #include "tx_api.h" static unsigned long thread_0_counter = 0; @@ -31,6 +32,90 @@ extern UINT _tx_timer_system_clock; static TX_SEMAPHORE semaphore_0; +#ifdef __linux__ + +/* The window this test waits for is a few instructions wide, and the interrupt + that samples it is the same one that wakes the thread being sampled, so the + two run in lockstep: the tick wakes thread 0, thread 0 does a fixed amount of + work and suspends, and the next tick arrives a fixed interval later with + thread 0 in the same place every time. That is the resonance the handler below + describes, and perturbing the handler's duration only shifts the phase rather + than decorrelating it. + + The simulator's timer thread waits on _tx_linux_timer_semaphore with a + one-tick deadline and delivers an interrupt early if the semaphore is posted, + which the port itself relies on elsewhere. A plain POSIX thread posting it + therefore injects interrupts at moments unrelated to the tick grid, which + samples thread 0 at arbitrary points in its cycle instead of the same one. + + It posts only when nothing is outstanding, so interrupts can never be queued + faster than they are serviced and the injector cannot starve the system. */ + +#include <pthread.h> +#include <semaphore.h> +#include <unistd.h> + +extern sem_t _tx_linux_timer_semaphore; + +static volatile int injector_stop = 0; +static pthread_t injector_thread; + +#define INJECTOR_INTERVAL_USEC 100 + + +static void *interrupt_injector(void *p) +{ + +int pending; + + + (void) p; + + while (injector_stop == 0) + { + + if ((sem_getvalue(&_tx_linux_timer_semaphore, &pending) == 0) && (pending == 0)) + { + sem_post(&_tx_linux_timer_semaphore); + } + + usleep(INJECTOR_INTERVAL_USEC); + } + + return(NULL); +} + + +static void interrupt_injector_start(void) +{ + + injector_stop = 0; + if (pthread_create(&injector_thread, NULL, interrupt_injector, NULL) != 0) + { + + /* Without the injector the loop still runs, on its own budget. */ + injector_stop = 1; + } +} + + +static void interrupt_injector_stop(void) +{ + + if (injector_stop == 0) + { + + injector_stop = 1; + pthread_join(injector_thread, NULL); + } +} + +#else +#define interrupt_injector_start() +#define interrupt_injector_stop() +#endif + + /* Define thread prototypes. */ static void thread_0_entry(ULONG thread_input); @@ -177,6 +262,56 @@ static void thread_0_entry(ULONG thread_input) UINT status; +/* The window this test waits for is probabilistic, and the handler above says + as much: it can settle into a resonance in which the condition is never met. + Waiting for it without a bound means such a run never ends, and this test has + been the most expensive thing in the suite: run one at a time in CI it took + between 148 and 726 seconds per configuration, 88 percent of the whole + ThreadX suite, against 273 seconds for the other ninety five tests together. + + The budget is in wall clock seconds rather than ticks on purpose. The + simulated tick clock is not a proxy for elapsed time here: a tick arrives only + when the port's timer thread gets to run, so under load, or under coverage + instrumentation, ticks fall behind and never catch up. A budget of 20000 + ticks, nominally 200 seconds, failed to stop a run that took 726 seconds, + because fewer than 20000 ticks had passed. time() does not drift that way. + + How many windows to ask for is set by what a run can actually reach. Before the + injector above, four CI runs of the same tree measured this loop in two modes: + one where a window arrives in milliseconds, and one where a single window + costs around forty seconds. Seven of twenty configuration-runs landed in the + slow mode and ran out of budget, reaching 0, 3, 3, 3, 4, 7 and 7 of the ten + then asked for, every one of them still reporting a pass, and the count had to + come down to three to stay reachable. With the interrupt source decorrelated + from the thread it samples the slow mode does not occur: all six + configurations of a run reach ten of ten in under a second. The count is + therefore back to ten, on the same reasoning that lowered it. + + The SMP copy deliberately does not get the injector. It has never been in the + slow mode, reaching its twenty windows in about a second, and injecting + interrupts there measurably hurts: 8.9 seconds against 0 to 1 for the same + twenty, which is the extra interrupt traffic contending across the simulated + cores. Its budget and ceiling still match this one; only the injector differs, + because only this copy has the problem it solves. + + Reaching the window no times at all is different in kind, which is what the + ceiling below is for. The check after the loop compares semaphore bookkeeping + that a window has to have touched to mean anything, so a pass with a count of + zero claims coverage the run did not have. One of those twenty runs did + exactly that, and said so only in an artifact nobody reads. A run that has not + reached the window once keeps trying up to the ceiling, and fails if it still + has not. + + The budget and the ceiling stay as they were, as a net rather than a working + limit. They were sized for the slow mode: 180 seconds covers three windows at + the worst rate measured, 17 to 40 seconds each. Nothing should approach them + now, and if something does, the behaviour is what it was before. */ +#define WAIT_ABORT_WINDOWS_WANTED ((ULONG) 10) +#define WAIT_ABORT_SECOND_BUDGET ((ULONG) 180) +#define WAIT_ABORT_ZERO_WINDOW_CEILING ((ULONG) 300) + +time_t start_wall; + /* Setup ISR for this test. */ test_isr_dispatch = isr_entry; @@ -185,7 +320,9 @@ UINT status; printf("Running Thread Wait Abort and ISR Resume Test....................... "); /* Loop to exploit the probability window inside tx_thread_wait_abort. */ - while (condition_count < 10) + start_wall = time(TX_NULL); + interrupt_injector_start(); + while (condition_count < WAIT_ABORT_WINDOWS_WANTED) { /* Suspend on the semaphore that is going to be set via the ISR. */ @@ -226,11 +363,50 @@ UINT status; #endif } + + /* Out of budget? A run that has not reached the window even once has + verified nothing yet, so it gets the higher ceiling before giving up. */ +#ifdef TX_NOT_INTERRUPTABLE + if (((ULONG) (time(TX_NULL) - start_wall)) > WAIT_ABORT_SECOND_BUDGET) + break; +#else + if (condition_count == 0) + { + if (((ULONG) (time(TX_NULL) - start_wall)) > WAIT_ABORT_ZERO_WINDOW_CEILING) + break; + } + else if (((ULONG) (time(TX_NULL) - start_wall)) > WAIT_ABORT_SECOND_BUDGET) + break; +#endif } + interrupt_injector_stop(); + /* Clear ISR dispatch. */ test_isr_dispatch = TX_NULL; + /* Say what this run reached, on every run and not only a short one. A count + printed only on shortfall cannot be told apart from a count nobody + recorded, and this line is what the CI artifacts carry. Falling short is a + gap in what was exercised rather than a fault in the code under test, so + the check below still runs and still means what it did. */ + printf("(reached %lu of %lu windows in %lu seconds) ", + (ULONG) condition_count, WAIT_ABORT_WINDOWS_WANTED, + (ULONG) (time(TX_NULL) - start_wall)); + +#ifndef TX_NOT_INTERRUPTABLE + + /* Reached it no times? Then the check below compares bookkeeping no window + ever touched, and a pass would report coverage this run did not have. */ + if (condition_count == 0) + { + + /* Test error! */ + printf("ERROR #8\n"); + test_control_return(4); + } +#endif + #ifdef TX_NOT_INTERRUPTABLE /* At this point, check to see if we got all the semaphores! */ if ((thread_0_counter != (semaphore_put_counter - semaphore_0.tx_semaphore_count)) || diff --git a/test/tx/regression/threadx_timer_information_test.c b/test/tx/regression/threadx_timer_information_test.c index ae3bb414..cbb88239 100644 --- a/test/tx/regression/threadx_timer_information_test.c +++ b/test/tx/regression/threadx_timer_information_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test timer information services. */ #include <stdio.h> @@ -104,7 +106,7 @@ static void thread_0_entry(ULONG thread_input) UINT status; UINT interrupt_status; -CHAR *name; +TX_NAME_CONST CHAR *name; UINT active; ULONG remaining_ticks; ULONG reschedule_ticks; @@ -231,7 +233,7 @@ TX_TIMER_INTERNAL **list_head; status += tx_timer_info_get(&timer_0, &name, &active, &remaining_ticks, &reschedule_ticks, &next_timer); /* Check for successful completion. */ - if ((status != TX_SUCCESS) || (active != TX_TRUE) || (remaining_ticks != 100) || (reschedule_ticks != 200) || (next_timer != &timer_1)) + if ((status != TX_SUCCESS) || (name != timer_0.tx_timer_name) || (active != TX_TRUE) || (remaining_ticks != 100) || (reschedule_ticks != 200) || (next_timer != &timer_1)) { /* Application timer error. */ diff --git a/test/tx/regression/threadx_timer_simple_test.c b/test/tx/regression/threadx_timer_simple_test.c index cad67086..f9c93ee4 100644 --- a/test/tx/regression/threadx_timer_simple_test.c +++ b/test/tx/regression/threadx_timer_simple_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test a simple application timer services, including create, activate, deactivate, change, and delete. */ @@ -63,7 +65,7 @@ static void thread_1_entry(ULONG thread_input); static void timer_0_expiration(ULONG timer_input); static void timer_1_expiration(ULONG timer_input); -UINT _txe_timer_create(TX_TIMER *timer_ptr, CHAR *name_ptr, +UINT _txe_timer_create(TX_TIMER *timer_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*expiration_function)(ULONG), ULONG expiration_input, ULONG initial_ticks, ULONG reschedule_ticks, UINT auto_activate, UINT timer_control_block_size); diff --git a/test/tx/regression/threadx_trace_basic_test.c b/test/tx/regression/threadx_trace_basic_test.c index c40be8f1..cacf72b3 100644 --- a/test/tx/regression/threadx_trace_basic_test.c +++ b/test/tx/regression/threadx_trace_basic_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This test is designed to test trace functionality in ThreadX. */ @@ -23,7 +25,7 @@ extern VOID (*test_isr_dispatch)(void); extern UINT _tx_trace_interrupt_control(UINT new_posture); -extern VOID _tx_trace_object_register(UCHAR object_type, VOID *object_ptr, CHAR *object_name, ULONG parameter_1, ULONG parameter_2); +extern VOID _tx_trace_object_register(UCHAR object_type, VOID *object_ptr, TX_NAME_CONST CHAR *object_name, ULONG parameter_1, ULONG parameter_2); extern VOID _tx_trace_object_unregister(VOID *object_ptr); static unsigned long thread_0_counter = 0; @@ -66,6 +68,16 @@ static unsigned long error = 0; static unsigned long full_buffer = 0; static void *save_pointer; +#define TRACE_BLOCK_POOL_SIZE ((ULONG) (((100U + sizeof(ALIGN_TYPE) - 1U) / sizeof(ALIGN_TYPE)) * sizeof(ALIGN_TYPE))) +#define TRACE_BYTE_POOL_SIZE ((ULONG) (((1000U + sizeof(ALIGN_TYPE) - 1U) / sizeof(ALIGN_TYPE)) * sizeof(ALIGN_TYPE))) + +/* Keep pool storage aligned for ports with pointer-wide metadata. */ +static struct +{ + ALIGN_TYPE alignment; + CHAR bytes[TRACE_BLOCK_POOL_SIZE + TRACE_BYTE_POOL_SIZE + 400U]; +} object_memory; + #if defined(TX_WIN32_MEMORY_SIZE) || defined(TX_LINUX_MEMORY_SIZE) static FILE *trace_dump_file; @@ -185,25 +197,21 @@ INT status; CHAR *pointer; - /* Setup a pointer. */ - pointer = (CHAR *) first_unused_memory; - - /* Adjust it forward just to make sure there is some space for the test below. */ - pointer = pointer + 200; + /* Setup aligned object storage. */ + pointer = object_memory.bytes; /* Create a bunch of objects before being enabled. */ /* Create a timer for the test. */ save_pointer = (void *) pointer; tx_timer_create(&timer_0, "timer 0", timer_entry, 0, 2, 2, TX_AUTO_ACTIVATE); - tx_block_pool_create(&block_pool_0, "block pool 0", 20, pointer, 100); - pointer = pointer + 100; - tx_byte_pool_create(&byte_pool_0, "byte pool 0", pointer, 1000); - pointer = pointer + 1000; + tx_block_pool_create(&block_pool_0, "block pool 0", 20, pointer, TRACE_BLOCK_POOL_SIZE); + pointer = pointer + TRACE_BLOCK_POOL_SIZE; + tx_byte_pool_create(&byte_pool_0, "byte pool 0", pointer, TRACE_BYTE_POOL_SIZE); + pointer = pointer + TRACE_BYTE_POOL_SIZE; tx_event_flags_create(&group_0, "event flags group 0"); tx_mutex_create(&mutex_0, "mutex 0", TX_NO_INHERIT); tx_queue_create(&queue_0, "queue 0", 16, pointer, 400); - pointer = pointer + 400; tx_semaphore_create(&semaphore_0, "semaphore 0", 1); /* Enable event tracing. */ @@ -230,6 +238,9 @@ CHAR *pointer; } #endif + pointer = (CHAR *) first_unused_memory; + pointer = pointer + 200; + /* Put system definition stuff in here, e.g. thread creates and other assorted create information. */ @@ -281,12 +292,11 @@ static void thread_0_entry(ULONG thread_input) UINT status; UINT old_interrupt; CHAR *pointer; -TX_INTERRUPT_SAVE_AREA ULONG object; /* Coverage for build without TraceX enabled. */ - tx_saved_posture = _tx_trace_interrupt_control(TX_INT_DISABLE); - _tx_trace_interrupt_control(tx_saved_posture); + old_interrupt = _tx_trace_interrupt_control(TX_INT_DISABLE); + _tx_trace_interrupt_control(old_interrupt); #ifndef TX_ENABLE_EVENT_TRACE _tx_trace_object_register(0, TX_NULL, TX_NULL, 0, 0); _tx_trace_object_register(1, TX_NULL, TX_NULL, 0, 0); @@ -354,14 +364,13 @@ ULONG object; /* Now, create them all again. */ pointer = (CHAR *) save_pointer; - tx_block_pool_create(&block_pool_0, "block pool 0", 20, pointer, 100); - pointer = pointer + 100; - tx_byte_pool_create(&byte_pool_0, "byte pool 0", pointer, 1000); - pointer = pointer + 1000; + tx_block_pool_create(&block_pool_0, "block pool 0", 20, pointer, TRACE_BLOCK_POOL_SIZE); + pointer = pointer + TRACE_BLOCK_POOL_SIZE; + tx_byte_pool_create(&byte_pool_0, "byte pool 0", pointer, TRACE_BYTE_POOL_SIZE); + pointer = pointer + TRACE_BYTE_POOL_SIZE; tx_event_flags_create(&group_0, "event flags group 0"); tx_mutex_create(&mutex_0, "mutex 0", TX_NO_INHERIT); tx_queue_create(&queue_0, "queue 0", 16, pointer, 400); - pointer = pointer + 400; tx_semaphore_create(&semaphore_0, "semaphore 0", 1); /* Attempt to enable event tracing again. */ diff --git a/test/tx/regression/threadx_trace_entry_update_test.c b/test/tx/regression/threadx_trace_entry_update_test.c new file mode 100644 index 00000000..f18c2d9b --- /dev/null +++ b/test/tx/regression/threadx_trace_entry_update_test.c @@ -0,0 +1,383 @@ +/*************************************************************************** + * 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 + **************************************************************************/ +// Portions of this file were generated with AI assistance. + +/* This test drives the trace entry update paths -- the blocks guarded by + TX_ENABLE_EVENT_TRACE that go back and patch a trace entry after the call + they belong to has finished its work. + + Those blocks all have the same shape: + + if (entry_ptr != TX_NULL) + { + if (time_stamp == entry_ptr -> tx_trace_buffer_entry_time_stamp) + { + entry_ptr -> tx_trace_buffer_entry_information_field_N = ...; + } + } + + entry_ptr comes from _tx_trace_buffer_current_ptr, which stays TX_NULL until + tx_trace_enable() is called at run time. Compiling with TX_ENABLE_EVENT_TRACE + is therefore not enough to reach them, and the trace_build configuration on + its own never does: the only other test that enables tracing -- + threadx_trace_basic_test -- exercises the enable API itself and never calls + tx_block_allocate or tx_byte_allocate at all. So the update blocks in + tx_block_allocate.c, tx_byte_allocate.c, tx_thread_system_suspend.c and + tx_thread_system_resume.c were absent from the coverage report's covered set + while appearing in its denominator. + + Both allocators are driven twice, because they carry two of these blocks + each and only one is on the path that succeeds immediately: the second sits + after a suspension, where the allocation completes in the releasing thread's + context. Hence the second thread, which exists only to release what + thread 0 is waiting on. + + The test is built in all five configurations, and four of them do not define + TX_ENABLE_EVENT_TRACE. There tx_trace_enable reports TX_FEATURE_NOT_ENABLED, + the update blocks do not exist, and the allocation sequence below is still a + valid exercise of both allocators' suspension paths -- so the test is + meaningful in every configuration rather than skipped in four. */ + +#include <stdio.h> +#include "tx_api.h" +#define TX_SOURCE_CODE +#include "tx_trace.h" + + +void test_control_return(UINT status); + + +static TX_THREAD thread_0; +static TX_THREAD thread_1; + +static TX_BLOCK_POOL block_pool_0; +static TX_BYTE_POOL byte_pool_0; +static TX_SEMAPHORE semaphore_0; + +/* The trace buffer is sized generously on purpose. The inner test of each + update block compares the saved time stamp against the one in the entry, and + a buffer small enough to wrap between the call and its completion would + overwrite the entry and take the false branch every time -- the block would + be reached and its body still never run. */ + +static UCHAR trace_buffer[16384]; + +/* Enough storage for several 20-byte blocks on both 32-bit and 64-bit builds. */ + +static UCHAR block_pool_area[100]; +static UCHAR byte_pool_area[512]; + +/* Allocated by thread 0 and released by thread 1, which is what lets thread 0's + suspended allocations complete. */ + +static void *held_block; +static void *held_bytes; + +static UINT error = 0; + + +static void thread_0_entry(ULONG thread_input); +static void thread_1_entry(ULONG thread_input); + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_trace_entry_update_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + + /* Setup a pointer. */ + pointer = (CHAR *) first_unused_memory; + + /* Create the pools before tracing is enabled, so that the events this test + cares about are the allocations rather than the creates. */ + status = tx_block_pool_create(&block_pool_0, "trace entry update block pool", + 20, block_pool_area, sizeof(block_pool_area)); + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #1\n"); + test_control_return(1); + } + + status = tx_byte_pool_create(&byte_pool_0, "trace entry update byte pool", + byte_pool_area, sizeof(byte_pool_area)); + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #2\n"); + test_control_return(1); + } + + /* Turn tracing on. This is the whole point of the test: without it + _tx_trace_buffer_current_ptr stays TX_NULL and every update block below + takes its guarding false branch. */ + _tx_trace_initialize(); + status = tx_trace_enable(trace_buffer, sizeof(trace_buffer), 16); + +#ifdef TX_ENABLE_EVENT_TRACE + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #3\n"); + test_control_return(1); + } +#else + + /* Four of the five build configurations reach this branch. The rest of the + test still runs; it just is not exercising any update block. */ + if (status != TX_FEATURE_NOT_ENABLED) + { + + printf("Running Trace Entry Update Test..................................... ERROR #4\n"); + test_control_return(1); + } +#endif + + /* Register an object with no name at all, now that tracing is on. The name + copy in _tx_trace_object_register guards against a TX_NULL name and + breaks out of the copy loop, and nothing else in the suite takes that + branch with tracing enabled -- threadx_trace_basic_test makes the + equivalent call only under #ifndef TX_ENABLE_EVENT_TRACE, against the + no-op stub. + + A semaphore rather than a thread, deliberately: for a thread object the + register function dereferences the pointer it is handed to read the + thread's priority, so that type needs a real TX_THREAD behind it. */ + status = tx_semaphore_create(&semaphore_0, TX_NULL, 1); + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #5\n"); + test_control_return(1); + } + + /* Thread 0 drives the test and must run first, so it takes the higher + priority -- the lower number. */ + status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + 15, 15, TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #6\n"); + test_control_return(1); + } + + /* Thread 1 releases what thread 0 waits on. It runs only while thread 0 is + suspended, which is exactly when it is needed. */ + status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #7\n"); + test_control_return(1); + } +} + + +static void thread_0_entry(ULONG thread_input) +{ + +UINT status; +void *block_ptr; +void *byte_ptr; +UINT i; + + + /* Empty the block pool. The first allocation takes the immediate-success + path through tx_block_allocate, which carries the first update block. + The exact capacity is port-dependent because each block has a pointer- + sized header, so allocate until the pool reports that it is empty. */ + held_block = TX_NULL; + for (i = 0; i < 6u; i++) + { + + status = tx_block_allocate(&block_pool_0, &block_ptr, TX_NO_WAIT); + + if (status == TX_NO_MEMORY) + { + + break; + } + + if (status != TX_SUCCESS) + { + + error++; + break; + } + + /* Keep the first one for thread 1 to give back. */ + if (held_block == TX_NULL) + { + + held_block = block_ptr; + } + } + + if ((status != TX_NO_MEMORY) || (held_block == TX_NULL)) + { + + error++; + } + + /* The pool is empty now, so this suspends. It completes in thread 1's + context when the block comes back, which is the second update block in + tx_block_allocate -- and the suspend and resume it goes through carry the + update blocks in tx_thread_system_suspend and tx_thread_system_resume. */ + status = tx_block_allocate(&block_pool_0, &block_ptr, TX_WAIT_FOREVER); + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #8\n"); + test_control_return(1); + } + + /* Now the same shape for the byte pool. Take most of it first, on the path + that succeeds immediately. */ + status = tx_byte_allocate(&byte_pool_0, &held_bytes, 300, TX_NO_WAIT); + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #9\n"); + test_control_return(1); + } + + /* Not enough left, so this one suspends and is satisfied by thread 1's + release. */ + status = tx_byte_allocate(&byte_pool_0, &byte_ptr, 300, TX_WAIT_FOREVER); + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #10\n"); + test_control_return(1); + } + + /* Sleep, so that thread 1 gets to run out its entry function and suspend + itself. That leaves nothing ready to execute, which is a third update + block in tx_thread_system_suspend -- the one on the path that sets + _tx_thread_execute_ptr to TX_NULL. The two suspensions above never reach + it, because the other thread was always ready to take over. */ + status = tx_thread_sleep(5); + + if (status != TX_SUCCESS) + { + + printf("Running Trace Entry Update Test..................................... ERROR #11\n"); + test_control_return(1); + } + +#ifdef TX_ENABLE_EVENT_TRACE + + /* Every update block above compares a saved time stamp against the one in + the entry, so a port whose time source never changes makes all of them + take their true branch for the wrong reason: not because the entry + survived, but because every entry reads the same. Check the buffer holds + at least one entry with a time stamp the port actually supplied. The + events written by this point number in the dozens, so a single zero + reading from a real clock cannot fail this. */ + { + + TX_TRACE_BUFFER_ENTRY *entry_ptr; + UINT stamped; + + + stamped = TX_FALSE; + + for (entry_ptr = _tx_trace_buffer_start_ptr; entry_ptr < _tx_trace_buffer_end_ptr; entry_ptr++) + { + + if (entry_ptr -> tx_trace_buffer_entry_time_stamp != ((ULONG) 0)) + { + + stamped = TX_TRUE; + break; + } + } + + if (stamped != TX_TRUE) + { + + printf("Running Trace Entry Update Test..................................... ERROR #13\n"); + test_control_return(1); + } + } +#endif + + if (error) + { + + printf("Running Trace Entry Update Test..................................... ERROR #12\n"); + test_control_return(1); + } + else + { + + printf("Running Trace Entry Update Test..................................... SUCCESS!\n"); + test_control_return(0); + } +} + + +static void thread_1_entry(ULONG thread_input) +{ + +UINT status; + + + /* Reached only when thread 0 has suspended on the empty block pool. */ + status = tx_block_release(held_block); + + if (status != TX_SUCCESS) + { + + error++; + } + + /* And again when thread 0 has suspended on the byte pool. Thread 0 preempts + on the release above, runs until it suspends a second time, and control + comes back here. */ + status = tx_byte_release(held_bytes); + + if (status != TX_SUCCESS) + { + + error++; + } + + /* Thread 0 reports the result; this thread has nothing left to do. */ + tx_thread_suspend(&thread_1); +} |
