/*************************************************************************** * 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 drives every thread service with event tracing enabled. Each service carries one in-line trace insert, and that insert behaves differently according to the system state it runs in, whether its event is filtered, whether it is the entry that wraps the trace buffer, and whether a buffer-full notification is registered. The test walks the thread services once per combination, so every service's insert is driven through all of them. The scheduler's own inserts sit behind the published services: a resume that preempts and a suspend that does not both record an event and then update it in place once the next thread to execute is known. The preemption pass at the end drives those with tracing enabled, which is the only way the update is reached at all. Without event tracing compiled in the inserts expand to nothing, so the test only checks that the trace services report the feature as disabled. */ #include #include "tx_api.h" #define TX_SOURCE_CODE #include "tx_thread.h" #include "tx_trace.h" #include "tx_initialize.h" /* Define the ISR dispatch. */ extern VOID (*test_isr_dispatch)(void); /* Define the thread services, in the order the test drives them. The scratch thread is created by the first and deleted by the last, so a pass over the whole list leaves the system as it found it. */ #define THREAD_SERVICE_CREATE 0 #define THREAD_SERVICE_IDENTIFY 1 #define THREAD_SERVICE_INFO_GET 2 #define THREAD_SERVICE_ENTRY_EXIT_NOTIFY 3 #define THREAD_SERVICE_PREEMPTION_CHANGE 4 #define THREAD_SERVICE_PRIORITY_CHANGE 5 #define THREAD_SERVICE_TIME_SLICE_CHANGE 6 #define THREAD_SERVICE_RESUME 7 #define THREAD_SERVICE_SUSPEND 8 #define THREAD_SERVICE_WAIT_ABORT 9 #define THREAD_SERVICE_RELINQUISH 10 #define THREAD_SERVICE_TIME_SLICE 11 #define THREAD_SERVICE_SLEEP 12 #define THREAD_SERVICE_RESET 13 #define THREAD_SERVICE_TERMINATE 14 #define THREAD_SERVICE_DELETE 15 #define THREAD_SERVICES 16 /* The priorities the test runs at. The scratch thread sits below the test thread so that creating, resuming and suspending it never schedules it; the preempting thread sits above so that resuming it always does. */ #define TEST_PRIORITY 16 #define SCRATCH_PRIORITY 20 #define PREEMPT_PRIORITY 10 static unsigned long thread_0_counter = 0; static TX_THREAD thread_0; /* The scratch thread the lifecycle services are driven against. */ static TX_THREAD thread_1; static CHAR *thread_1_stack; /* The reset target. It is left terminated between passes, which is the only state a thread can be reset from, and it is also what the interrupt pass drives its services against. */ static TX_THREAD thread_2; /* The preempting thread, used by the last pass only. */ static unsigned long thread_3_counter = 0; static TX_THREAD thread_3; static ULONG buffer_full_count = 0; static ULONG isr_pass_done = 0; static UINT isr_status = TX_SUCCESS; /* The trace buffer is deliberately small. Several passes over the services fit in it many times over, so the wrap path is reached by ordinary use as well as by the positioned wrap below. */ static UCHAR trace_buffer[512]; #define TRACE_REGISTRY_ENTRIES 4 #define SCRATCH_STACK_SIZE TEST_STACK_SIZE_PRINTF /* Define thread prototypes. */ static void thread_0_entry(ULONG thread_input); static void thread_1_entry(ULONG thread_input); static void thread_3_entry(ULONG thread_input); /* Prototype for test control return. */ void test_control_return(UINT status); /* Drive one thread service through its published entry point. */ static UINT thread_service(UINT service) { UINT status; CHAR *name; UINT state; ULONG run_count; UINT priority; UINT threshold; ULONG time_slice; TX_THREAD *next_thread; TX_THREAD *next_suspended_thread; ULONG old_time_slice; switch (service) { case THREAD_SERVICE_CREATE: status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 1, thread_1_stack, SCRATCH_STACK_SIZE, SCRATCH_PRIORITY, SCRATCH_PRIORITY, TX_NO_TIME_SLICE, TX_DONT_START); break; case THREAD_SERVICE_IDENTIFY: if (tx_thread_identify() == TX_NULL) { status = TX_NOT_DONE; } else { status = TX_SUCCESS; } break; case THREAD_SERVICE_INFO_GET: status = tx_thread_info_get(&thread_1, &name, &state, &run_count, &priority, &threshold, &time_slice, &next_thread, &next_suspended_thread); break; case THREAD_SERVICE_ENTRY_EXIT_NOTIFY: status = tx_thread_entry_exit_notify(&thread_1, TX_NULL); break; case THREAD_SERVICE_PREEMPTION_CHANGE: status = tx_thread_preemption_change(&thread_1, SCRATCH_PRIORITY, &threshold); break; case THREAD_SERVICE_PRIORITY_CHANGE: status = tx_thread_priority_change(&thread_1, SCRATCH_PRIORITY, &priority); break; case THREAD_SERVICE_TIME_SLICE_CHANGE: status = tx_thread_time_slice_change(&thread_1, TX_NO_TIME_SLICE, &old_time_slice); break; case THREAD_SERVICE_RESUME: status = tx_thread_resume(&thread_1); break; case THREAD_SERVICE_SUSPEND: status = tx_thread_suspend(&thread_1); break; case THREAD_SERVICE_WAIT_ABORT: status = tx_thread_wait_abort(&thread_1); break; case THREAD_SERVICE_RELINQUISH: tx_thread_relinquish(); status = TX_SUCCESS; break; case THREAD_SERVICE_TIME_SLICE: /* The time-slice check has no published entry point of its own: the timer drives it. Calling it directly is what puts its insert on the boundary in the wrap passes rather than leaving it to a tick. */ _tx_thread_time_slice(); status = TX_SUCCESS; break; case THREAD_SERVICE_SLEEP: status = tx_thread_sleep(1); break; case THREAD_SERVICE_RESET: /* The reset is followed by a terminate so that the next pass finds the thread in the one state a reset is allowed from. The terminate runs after the reset, so the reset's own insert is still the one that wraps the buffer in the wrap passes. */ status = tx_thread_reset(&thread_2); if (tx_thread_terminate(&thread_2) != TX_SUCCESS) { status = TX_NOT_DONE; } break; case THREAD_SERVICE_TERMINATE: status = tx_thread_terminate(&thread_1); break; default: status = tx_thread_delete(&thread_1); break; } return(status); } /* The status a service returns when the pass above is run to completion. The create and delete balance, the resume and suspend balance, and the wait abort is driven against a thread that is suspended rather than waiting, so the pass is repeatable. */ static UINT thread_service_expected(UINT service) { UINT expected; if (service == THREAD_SERVICE_WAIT_ABORT) { expected = TX_WAIT_ABORT_ERROR; } #ifdef TX_DISABLE_NOTIFY_CALLBACKS else if (service == THREAD_SERVICE_ENTRY_EXIT_NOTIFY) { expected = TX_FEATURE_NOT_ENABLED; } #endif else { expected = TX_SUCCESS; } return(expected); } #ifdef TX_ENABLE_EVENT_TRACE /* The services an ISR is allowed to call. The rest are rejected by the error checking shell from any caller that is not a thread, so their inserts cannot be reached this way; the forced system state pass below reaches them instead. They are driven against the terminated reset target, so the pass changes no scheduling state at all. */ static UINT isr_services[] = {THREAD_SERVICE_IDENTIFY, THREAD_SERVICE_INFO_GET, THREAD_SERVICE_ENTRY_EXIT_NOTIFY, THREAD_SERVICE_RESUME, THREAD_SERVICE_SUSPEND, THREAD_SERVICE_WAIT_ABORT}; #define ISR_SERVICES (sizeof(isr_services)/sizeof(isr_services[0])) /* Drive one thread service from interrupt context, against the terminated thread. */ static UINT thread_service_isr(UINT service) { UINT status; CHAR *name; UINT state; ULONG run_count; UINT priority; UINT threshold; ULONG time_slice; TX_THREAD *next_thread; TX_THREAD *next_suspended_thread; switch (service) { case THREAD_SERVICE_IDENTIFY: /* An interrupt that arrives while the system is idle has no thread to identify, so the return value carries nothing to assert here. What is under test is the insert the service runs on the way. */ (VOID) tx_thread_identify(); status = TX_SUCCESS; break; case THREAD_SERVICE_INFO_GET: status = tx_thread_info_get(&thread_2, &name, &state, &run_count, &priority, &threshold, &time_slice, &next_thread, &next_suspended_thread); break; case THREAD_SERVICE_ENTRY_EXIT_NOTIFY: status = tx_thread_entry_exit_notify(&thread_2, TX_NULL); break; case THREAD_SERVICE_RESUME: status = tx_thread_resume(&thread_2); break; case THREAD_SERVICE_SUSPEND: status = tx_thread_suspend(&thread_2); break; default: status = tx_thread_wait_abort(&thread_2); break; } return(status); } /* What each of those returns against a terminated thread. */ static UINT thread_service_isr_expected(UINT service) { UINT expected; switch (service) { case THREAD_SERVICE_RESUME: expected = TX_RESUME_ERROR; break; case THREAD_SERVICE_SUSPEND: expected = TX_SUSPEND_ERROR; break; case THREAD_SERVICE_WAIT_ABORT: expected = TX_WAIT_ABORT_ERROR; break; #ifdef TX_DISABLE_NOTIFY_CALLBACKS case THREAD_SERVICE_ENTRY_EXIT_NOTIFY: expected = TX_FEATURE_NOT_ENABLED; break; #endif default: expected = TX_SUCCESS; break; } return(expected); } /* Drive one thread service through its internal entry point. The error checking shell rejects most of these from any caller that is not a thread, so the inserts behind them cannot be reached through the published entry points at a forced system state. The shell itself is covered by its own tests; what is under test here is the insert behind it. */ static UINT thread_service_internal(UINT service) { UINT status; CHAR *name; UINT state; ULONG run_count; UINT priority; UINT threshold; ULONG time_slice; TX_THREAD *next_thread; TX_THREAD *next_suspended_thread; ULONG old_time_slice; switch (service) { case THREAD_SERVICE_CREATE: status = _tx_thread_create(&thread_1, "thread 1", thread_1_entry, 1, thread_1_stack, SCRATCH_STACK_SIZE, SCRATCH_PRIORITY, SCRATCH_PRIORITY, TX_NO_TIME_SLICE, TX_DONT_START); break; case THREAD_SERVICE_IDENTIFY: if (_tx_thread_identify() == TX_NULL) { status = TX_NOT_DONE; } else { status = TX_SUCCESS; } break; case THREAD_SERVICE_INFO_GET: status = _tx_thread_info_get(&thread_1, &name, &state, &run_count, &priority, &threshold, &time_slice, &next_thread, &next_suspended_thread); break; case THREAD_SERVICE_ENTRY_EXIT_NOTIFY: status = _tx_thread_entry_exit_notify(&thread_1, TX_NULL); break; case THREAD_SERVICE_PREEMPTION_CHANGE: status = _tx_thread_preemption_change(&thread_1, SCRATCH_PRIORITY, &threshold); break; case THREAD_SERVICE_PRIORITY_CHANGE: status = _tx_thread_priority_change(&thread_1, SCRATCH_PRIORITY, &priority); break; case THREAD_SERVICE_TIME_SLICE_CHANGE: status = _tx_thread_time_slice_change(&thread_1, TX_NO_TIME_SLICE, &old_time_slice); break; case THREAD_SERVICE_RESUME: status = _tx_thread_resume(&thread_1); break; case THREAD_SERVICE_SUSPEND: status = _tx_thread_suspend(&thread_1); break; case THREAD_SERVICE_WAIT_ABORT: status = _tx_thread_wait_abort(&thread_1); break; case THREAD_SERVICE_RELINQUISH: _tx_thread_relinquish(); status = TX_SUCCESS; break; case THREAD_SERVICE_TIME_SLICE: _tx_thread_time_slice(); status = TX_SUCCESS; break; case THREAD_SERVICE_SLEEP: /* The sleep service tests the system state itself, ahead of its own insert, and refuses every caller that is not a thread. The insert behind it is therefore only ever reached at system state zero. The call is made anyway so that the refusal is what the pass records. */ status = _tx_thread_sleep(1); break; case THREAD_SERVICE_RESET: status = _tx_thread_reset(&thread_2); if (_tx_thread_terminate(&thread_2) != TX_SUCCESS) { status = TX_NOT_DONE; } break; case THREAD_SERVICE_TERMINATE: status = _tx_thread_terminate(&thread_1); break; default: status = _tx_thread_delete(&thread_1); break; } return(status); } /* What each service returns at a forced system state. */ static UINT thread_service_internal_expected(UINT service) { UINT expected; if (service == THREAD_SERVICE_SLEEP) { expected = TX_CALLER_ERROR; } else { expected = thread_service_expected(service); } return(expected); } /* Run a pass over the services with the system state forced to the value the insert has to see. Interrupts are held off across the pass, so the timer thread cannot observe the forced state and the interrupt nesting count the same variable carries is restored before anything else reads it. */ static UINT pass_with_system_state(ULONG system_state) { TX_INTERRUPT_SAVE_AREA UINT service; UINT status = TX_SUCCESS; ULONG saved_system_state; TX_DISABLE saved_system_state = _tx_thread_system_state; _tx_thread_system_state = system_state; for (service = 0; service < THREAD_SERVICES; service++) { if (thread_service_internal(service) != thread_service_internal_expected(service)) { status = TX_NOT_DONE; } } _tx_thread_system_state = saved_system_state; TX_RESTORE return(status); } /* Position the trace buffer so that the given event is the one that wraps it. A wrap reached by filling the buffer lands on whichever service happens to write the last entry; positioning puts each service's own insert on the boundary in turn. The scheduler's inserts sit behind the published ones, so the count says how many entries a service writes before the insert under test. */ static void position_before_buffer_end(ULONG entries) { TX_INTERRUPT_SAVE_AREA TX_DISABLE _tx_trace_buffer_current_ptr = _tx_trace_buffer_end_ptr - entries; _tx_trace_header_ptr -> tx_trace_header_buffer_current_pointer = (ULONG) _tx_trace_buffer_current_ptr; TX_RESTORE } /* Run a pass over the services, wrapping the trace buffer on every one of them. The resume and the suspend are driven twice: once with their own insert on the boundary and once with the scheduler's insert behind them there. */ static UINT pass_with_buffer_wrap(void) { UINT service; UINT status = TX_SUCCESS; for (service = 0; service < THREAD_SERVICES; service++) { if ((service == THREAD_SERVICE_RESUME) || (service == THREAD_SERVICE_SUSPEND)) { position_before_buffer_end(2); } else { position_before_buffer_end(1); } if (thread_service(service) != thread_service_expected(service)) { status = TX_NOT_DONE; } } /* Now with the published service's own insert on the boundary. */ position_before_buffer_end(1); if (thread_service(THREAD_SERVICE_CREATE) != TX_SUCCESS) { status = TX_NOT_DONE; } position_before_buffer_end(1); if (thread_service(THREAD_SERVICE_RESUME) != TX_SUCCESS) { status = TX_NOT_DONE; } position_before_buffer_end(1); if (thread_service(THREAD_SERVICE_SUSPEND) != TX_SUCCESS) { status = TX_NOT_DONE; } position_before_buffer_end(1); if (thread_service(THREAD_SERVICE_TERMINATE) != TX_SUCCESS) { status = TX_NOT_DONE; } position_before_buffer_end(1); if (thread_service(THREAD_SERVICE_DELETE) != TX_SUCCESS) { status = TX_NOT_DONE; } return(status); } /* Count the trace buffer wraps the kernel reports. */ static VOID trace_buffer_full(void *buffer) { buffer_full_count++; } /* Drive the services an ISR may call, once, from interrupt context. */ static void test_isr(void) { UINT service; if (isr_pass_done == 0) { for (service = 0; service < ISR_SERVICES; service++) { if (thread_service_isr(isr_services[service]) != thread_service_isr_expected(isr_services[service])) { isr_status = TX_NOT_DONE; } } isr_pass_done = 1; } } #endif /* Define what the initial system looks like. */ #ifdef CTEST void test_application_define(void *first_unused_memory) #else void threadx_thread_trace_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; /* 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, TEST_PRIORITY, TEST_PRIORITY, TX_NO_TIME_SLICE, TX_AUTO_START); pointer = pointer + TEST_STACK_SIZE_PRINTF; /* Check for status. */ if (status != TX_SUCCESS) { printf("Running Thread Trace Test.......................................... ERROR #1\n"); test_control_return(1); } /* Hold the scratch thread's stack. The thread itself is created and deleted once per pass, so only its stack outlives a pass. */ thread_1_stack = pointer; pointer = pointer + SCRATCH_STACK_SIZE; /* Create the reset target and leave it terminated, which is the one state a thread can be reset from. */ status = tx_thread_create(&thread_2, "thread 2", thread_1_entry, 2, pointer, SCRATCH_STACK_SIZE, SCRATCH_PRIORITY, SCRATCH_PRIORITY, TX_NO_TIME_SLICE, TX_DONT_START); pointer = pointer + SCRATCH_STACK_SIZE; /* Check for status. */ if (status != TX_SUCCESS) { printf("Running Thread Trace Test.......................................... ERROR #2\n"); test_control_return(1); } status = tx_thread_terminate(&thread_2); /* Check for status. */ if (status != TX_SUCCESS) { printf("Running Thread Trace Test.......................................... ERROR #3\n"); test_control_return(1); } /* Create the preempting thread. It runs above the test thread, so resuming it always preempts and suspending it always gives the processor back. */ status = tx_thread_create(&thread_3, "thread 3", thread_3_entry, 3, pointer, TEST_STACK_SIZE_PRINTF, PREEMPT_PRIORITY, PREEMPT_PRIORITY, TX_NO_TIME_SLICE, TX_DONT_START); pointer = pointer + TEST_STACK_SIZE_PRINTF; /* Check for status. */ if (status != TX_SUCCESS) { printf("Running Thread Trace Test.......................................... ERROR #4\n"); test_control_return(1); } } /* The scratch thread never runs: it is created below the test thread's priority and is suspended or terminated whenever the test thread gives up the processor. */ static void thread_1_entry(ULONG thread_input) { while (1) { tx_thread_suspend(&thread_1); } } /* The preempting thread gives the processor straight back, so a resume of it is a preemption and nothing else. */ static void thread_3_entry(ULONG thread_input) { while (1) { thread_3_counter++; tx_thread_suspend(&thread_3); } } /* Define the test thread. */ static void thread_0_entry(ULONG thread_input) { UINT status; UINT service; #ifdef TX_ENABLE_EVENT_TRACE ULONG wait; #endif /* Increment thread 0 counter. */ thread_0_counter++; /* Inform user of success getting to this test. */ printf("Running Thread Trace Test.......................................... "); /* Enable event tracing. */ status = tx_trace_enable(trace_buffer, sizeof(trace_buffer), TRACE_REGISTRY_ENTRIES); #ifndef TX_ENABLE_EVENT_TRACE /* Without tracing compiled in the inserts expand to nothing, so there is nothing here to drive. Check that the service says so and finish. */ if (status != TX_FEATURE_NOT_ENABLED) { printf("ERROR #5\n"); test_control_return(1); } /* One pass over the services, so the test still exercises them. */ for (service = 0; service < THREAD_SERVICES; service++) { if (thread_service(service) != thread_service_expected(service)) { printf("ERROR #6\n"); test_control_return(1); } } printf("SUCCESS!\n"); test_control_return(0); #else if (status != TX_SUCCESS) { printf("ERROR #7\n"); test_control_return(1); } /* Thread context, every event enabled: the insert runs and takes the current thread's priority. */ for (service = 0; service < THREAD_SERVICES; service++) { if (thread_service(service) != thread_service_expected(service)) { printf("ERROR #8\n"); test_control_return(1); } } /* Thread events filtered out: the insert is reached and declines to record anything. The scheduler's own events are filtered with them, so the inserts behind the published services are covered as well. */ status = tx_trace_event_filter(TX_TRACE_THREAD_EVENTS | TX_TRACE_INTERNAL_EVENTS); if (status != TX_SUCCESS) { printf("ERROR #9\n"); test_control_return(1); } for (service = 0; service < THREAD_SERVICES; service++) { if (thread_service(service) != thread_service_expected(service)) { printf("ERROR #10\n"); test_control_return(1); } } status = tx_trace_event_unfilter(TX_TRACE_THREAD_EVENTS | TX_TRACE_INTERNAL_EVENTS); if (status != TX_SUCCESS) { printf("ERROR #11\n"); test_control_return(1); } /* Interrupt context: the insert records the interrupted thread instead of a priority. Bounded, so a tick that never arrives fails the test rather than hanging it. */ test_isr_dispatch = test_isr; wait = 0; while ((isr_pass_done == 0) && (wait < 100)) { tx_thread_sleep(1); wait++; } test_isr_dispatch = TX_NULL; if (isr_pass_done == 0) { printf("ERROR #12\n"); test_control_return(1); } if (isr_status != TX_SUCCESS) { printf("ERROR #13\n"); test_control_return(1); } /* Initialization context: the insert has no thread to record at all. */ if (pass_with_system_state(TX_INITIALIZE_IN_PROGRESS) != TX_SUCCESS) { printf("ERROR #14\n"); test_control_return(1); } /* The same state an ISR presents, for the services an ISR may not call. */ if (pass_with_system_state((ULONG) 1) != TX_SUCCESS) { printf("ERROR #15\n"); test_control_return(1); } /* Wrap the trace buffer on every service with no notification registered, then again with one. */ if (pass_with_buffer_wrap() != TX_SUCCESS) { printf("ERROR #16\n"); test_control_return(1); } status = tx_trace_buffer_full_notify(trace_buffer_full); if (status != TX_SUCCESS) { printf("ERROR #17\n"); test_control_return(1); } if (pass_with_buffer_wrap() != TX_SUCCESS) { printf("ERROR #18\n"); test_control_return(1); } /* Every service in the last pass wrapped the buffer, so the notification has to have been called once for each of them. */ if (buffer_full_count < THREAD_SERVICES) { printf("ERROR #19\n"); test_control_return(1); } status = tx_trace_buffer_full_notify(TX_NULL); if (status != TX_SUCCESS) { printf("ERROR #20\n"); test_control_return(1); } /* Preemption, with tracing on. The scheduler records a resume before it knows which thread will run next and updates that entry in place once it does, so the update is only reached when a resume actually preempts and an entry was recorded to update. The same holds for the suspend that gives the processor back. */ for (wait = 0; wait < 4; wait++) { status = tx_thread_resume(&thread_3); if (status != TX_SUCCESS) { printf("ERROR #21\n"); test_control_return(1); } } if (thread_3_counter != 4) { printf("ERROR #22\n"); test_control_return(1); } status = tx_trace_disable(); if (status != TX_SUCCESS) { printf("ERROR #23\n"); test_control_return(1); } printf("SUCCESS!\n"); test_control_return(0); #endif }