/*************************************************************************** * 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 is the SMP counterpart of the ThreadX suite's trace entry update test. It 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 and tx_thread_system_suspend.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 one thing this version has to do that the ThreadX version does not is pin both threads to a single core. On four cores the two threads run at the same time, and thread 1 would give the block back before thread 0 had suspended waiting for it -- the suspension the second update block sits behind would never happen. Excluding both from cores 1 to 3 restores the strict alternation the ThreadX suite gets for free. 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 #include "tx_api.h" #include "threadx_test_port.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; /* Every thread this test creates is confined to core 0, so that the two of them take turns the way they would on a uniprocessor. Bits 1, 2 and 3 set means cores 1, 2 and 3 are excluded. */ #define TRACE_ENTRY_UPDATE_CORE_0_ONLY ((ULONG) 0xE) /* 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]; /* Four blocks of 20 bytes. */ static UCHAR block_pool_area[TX_TEST_BLOCK_POOL_BYTES(20, 4)]; static UCHAR byte_pool_area[TX_TEST_BYTE_POOL_BYTES(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. Both threads are created stopped so that the core exclusion is in place before either of them is schedulable. */ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0, pointer, TEST_STACK_SIZE_PRINTF, 15, 15, TX_NO_TIME_SLICE, TX_DONT_START); pointer = pointer + TEST_STACK_SIZE_PRINTF; status += tx_thread_smp_core_exclude(&thread_0, TRACE_ENTRY_UPDATE_CORE_0_ONLY); 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 -- and only because it shares thread 0's single core. */ status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0, pointer, TEST_STACK_SIZE_PRINTF, 16, 16, TX_NO_TIME_SLICE, TX_DONT_START); pointer = pointer + TEST_STACK_SIZE_PRINTF; status += tx_thread_smp_core_exclude(&thread_1, TRACE_ENTRY_UPDATE_CORE_0_ONLY); if (status != TX_SUCCESS) { printf("Running Trace Entry Update Test..................................... ERROR #7\n"); test_control_return(1); } /* Start them now that both exclusions are set. Priority decides the order, not this one. */ status = tx_thread_resume(&thread_1); status += tx_thread_resume(&thread_0); if (status != TX_SUCCESS) { printf("Running Trace Entry Update Test..................................... ERROR #8\n"); test_control_return(1); } } static void thread_0_entry(ULONG thread_input) { UINT status; void *block_ptr; void *byte_ptr; UINT i; /* Inform user. */ printf("Running Trace Entry Update Test..................................... "); /* Empty the block pool. The first of these takes the immediate-success path through tx_block_allocate, which carries the first update block. */ held_block = TX_NULL; for (i = 0; i < 4; i++) { status = tx_block_allocate(&block_pool_0, &block_ptr, TX_NO_WAIT); 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; } } /* 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 it goes through carries the update block in tx_thread_system_suspend that names the next thread to run on this core. */ status = tx_block_allocate(&block_pool_0, &block_ptr, TX_WAIT_FOREVER); if (status != TX_SUCCESS) { printf("ERROR #9\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("ERROR #10\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("ERROR #11\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 anywhere, which is a second update block in tx_thread_system_suspend -- the one on the branch taken when the priority map is empty. The two suspensions above never reach it, because thread 1 was always ready to take over. */ status = tx_thread_sleep(5); if (status != TX_SUCCESS) { printf("ERROR #12\n"); test_control_return(1); } if (error) { printf("ERROR #13\n"); test_control_return(1); } else { /* Successful test. */ printf("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); }