diff --git a/.github/workflows/regression_test.yml b/.github/workflows/regression_test.yml index a1e6f0c0a..014ceec9a 100644 --- a/.github/workflows/regression_test.yml +++ b/.github/workflows/regression_test.yml @@ -63,15 +63,22 @@ jobs: cmake_path: ./test/smp/cmake result_affix: SMP skip_deploy: true - # Lower than ThreadX's, because this suite is: the merged report reads - # 5114/5178 lines -- 98.76% -- on the same run, with 64 uncovered lines - # across 11 files of common_smp/src. #666 closed the equivalent gaps in - # common/src only, so common_smp still carries them, plus a family of - # its own in the *_delete.c teardown paths and tx_byte_pool_search.c. - # A shared floor of 99 would therefore red-wall this job on every run - # while ThreadX passed -- probed against the pinned action. Raise this - # towards 99 as those lines are covered. - coverage_thresholds: '98 100' + # Raised from 98 with the four tests that closed 53 of the 64 lines this + # comment used to list: the merged report goes from 5114/5178 -- 98.76% -- + # to 5167/5178, 99.79%. A floor of 99 needs 5127, so there are 40 lines of + # headroom against a numerator measured flickering by two between runs. + # + # Still not 100, and not because the last eleven lines were skipped. Eight + # are tx_byte_pool_search.c's ownership delay loop, which needs another + # core to take the pool inside the window the search opens when it drops + # protection every twenty blocks; this port serialises all four cores on + # one pthread mutex and the thread that releases it wins the re-acquire + # every time -- measured over 180,003 windows, zero handovers. The other + # three are in tx_thread_smp_utilities.c: one is a range guard placed + # after the shift it is meant to guard, so reaching it needs undefined + # behaviour, and two are a priority-inheritance branch that needs the + # mutex owner genuinely executing on another core. See T17. + coverage_thresholds: '99 100' freertos: permissions: contents: read diff --git a/test/smp/cmake/regression/CMakeLists.txt b/test/smp/cmake/regression/CMakeLists.txt index 908ac5c72..af33ca3f0 100644 --- a/test/smp/cmake/regression/CMakeLists.txt +++ b/test/smp/cmake/regression/CMakeLists.txt @@ -25,6 +25,7 @@ set(regression_test_cases ${SOURCE_DIR}/threadx_block_memory_thread_terminate_test.c ${SOURCE_DIR}/threadx_byte_memory_basic_test.c ${SOURCE_DIR}/threadx_byte_memory_information_test.c + ${SOURCE_DIR}/threadx_byte_memory_long_search_test.c ${SOURCE_DIR}/threadx_byte_memory_prioritize_test.c ${SOURCE_DIR}/threadx_byte_memory_suspension_test.c ${SOURCE_DIR}/threadx_byte_memory_suspension_timeout_test.c @@ -52,6 +53,7 @@ set(regression_test_cases ${SOURCE_DIR}/threadx_mutex_proritize_test.c ${SOURCE_DIR}/threadx_mutex_suspension_timeout_test.c ${SOURCE_DIR}/threadx_mutex_thread_terminate_test.c + ${SOURCE_DIR}/threadx_object_delete_suspension_test.c ${SOURCE_DIR}/threadx_queue_basic_eight_word_test.c ${SOURCE_DIR}/threadx_queue_basic_four_word_test.c ${SOURCE_DIR}/threadx_queue_basic_one_word_test.c @@ -95,6 +97,7 @@ set(regression_test_cases ${SOURCE_DIR}/threadx_thread_create_preemption_threshold_test.c ${SOURCE_DIR}/threadx_thread_delayed_suspension_test.c ${SOURCE_DIR}/threadx_thread_information_test.c + ${SOURCE_DIR}/threadx_thread_misaligned_stack_test.c ${SOURCE_DIR}/threadx_thread_multi_level_preemption_threshold_test.c ${SOURCE_DIR}/threadx_thread_multiple_non_current_test.c ${SOURCE_DIR}/threadx_thread_multiple_sleep_test.c @@ -124,6 +127,7 @@ set(regression_test_cases ${SOURCE_DIR}/threadx_timer_multiple_test.c ${SOURCE_DIR}/threadx_timer_simple_test.c ${SOURCE_DIR}/threadx_trace_basic_test.c + ${SOURCE_DIR}/threadx_trace_entry_update_test.c ${SOURCE_DIR}/threadx_initialize_kernel_setup_test.c) add_custom_command( diff --git a/test/smp/regression/threadx_byte_memory_long_search_test.c b/test/smp/regression/threadx_byte_memory_long_search_test.c new file mode 100644 index 000000000..86a99be05 --- /dev/null +++ b/test/smp/regression/threadx_byte_memory_long_search_test.c @@ -0,0 +1,337 @@ +/*************************************************************************** + * 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 makes _tx_byte_pool_search walk a long chain of fragments, from + more than one core at a time. + + The SMP version of that function carries two things its ThreadX counterpart + does not, and neither was covered. + + The first is the block-search limit. Under TX_BYTE_POOL_MULTIPLE_BLOCK_SEARCH + -- which this port's tx_user.h sets to 20 -- the search holds protection + across at most twenty blocks and then deliberately drops and retakes it, so + that a long search cannot lock the other cores out for the whole walk. + Nothing in the suite ever made one search examine twenty blocks: every byte + pool it builds has a handful of fragments at most. That is what this test + fixes. + + Having dropped protection, the search then checks whether some other thread + claimed the pool while it was let go, and if so spins for + TX_BYTE_POOL_DELAY_VALUE iterations before taking ownership back and + restarting the walk. That path this test does not reach, and the reason is a + property of the port rather than of the test: the Linux SMP port serialises + every core on one pthread mutex, and the window here is an unlock + immediately followed by a lock on that mutex. A thread spinning for the + protection on another core has to win that handoff, and glibc hands an + uncontended mutex straight back to the thread that just released it. + Measured: 180,003 windows over three cores, and the pool changed hands in + none of them. So tx_byte_pool_search.c lines 283 to 301 stay uncovered on + this port, and no number of iterations here will change that -- 250 is used + below rather than the tens of thousands that were tried, because the twenty- + block threshold above is crossed by the very first search and the rest buys + nothing. + + The pool is therefore filled with small allocations until it refuses another + one, and every second allocation is then released. That leaves upwards of + twenty free fragments, each too small to satisfy the request and none of + them adjacent to another, so the search can neither stop early nor merge its + way out of the walk. Every request is expected to fail with TX_NO_MEMORY; + the failure is the point, because a search that fails is a search that + walked the whole pool. + + The layout is asserted rather than assumed. If a change to the pool + arithmetic ever made the request satisfiable, or left fewer fragments than + the twenty-block threshold needs, this test would still pass while quietly + covering nothing -- so it checks the fragment count and checks that the + request really does fail before the threads start. + + The three worker threads are deliberately not confined to a core, so that + the walk really is driven from more than one core at a time. */ + +#include +#include "tx_api.h" +#include "threadx_test_port.h" + + +void test_control_return(UINT status); + + +/* Small chunks, every second one given back. The pool is filled until it + refuses another chunk rather than a fixed number of times, so that the + layout does not depend on this test agreeing with the allocator about the + per-block overhead -- get that wrong by one word and the pool is left with a + free tail large enough to satisfy the request below, and the search stops at + the first fragment instead of walking. */ + +#define CHUNK_BYTES ((ULONG) 8) +#define CHUNK_LIMIT ((UINT) 96) +#define MINIMUM_CHUNKS ((UINT) 40) + +/* Larger than any one free fragment and smaller than the pool's theoretical + capacity, which is what makes the search walk rather than refuse at the + door: _tx_byte_pool_search returns immediately when the request is at least + the theoretical total, without examining a single block. */ + +#define REQUEST_BYTES ((ULONG) 64) + +/* TX_BYTE_POOL_MULTIPLE_BLOCK_SEARCH is 20 on this port. The walk has to be + longer than that for the protection to be dropped at all. */ + +#define MINIMUM_FRAGMENTS ((ULONG) 24) + +/* Each worker searches this many times. One search is enough for the coverage; + the repetition is here so that the pool is genuinely walked from several + cores at once rather than by one thread at a time, which is the condition + the code under test was written for. */ + +#define SEARCHES_PER_THREAD ((ULONG) 250) + +#define WORKERS ((UINT) 3) + + +static TX_THREAD thread_0; +static TX_THREAD worker[WORKERS]; + +static TX_BYTE_POOL pool_0; + +static UCHAR pool_area[TX_TEST_BYTE_POOL_CAPACITY_BYTES(CHUNK_BYTES, 48)]; + +static void *chunk[CHUNK_LIMIT]; +static UINT chunk_count; + +static volatile ULONG workers_done = 0; +static volatile ULONG search_count[WORKERS]; + +static UINT error = 0; + + +static void thread_0_entry(ULONG thread_input); +static void worker_entry(ULONG thread_input); + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_byte_memory_long_search_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; +UINT i; +CHAR *name; +ULONG available; +ULONG fragments; +void *probe_ptr; + + + /* Setup a pointer. */ + pointer = (CHAR *) first_unused_memory; + + status = tx_byte_pool_create(&pool_0, "long search pool", pool_area, sizeof(pool_area)); + + if (status != TX_SUCCESS) + { + + printf("Running Byte Memory Long Search Test................................ ERROR #1\n"); + test_control_return(1); + return; + } + + /* Fill the pool completely with small chunks, so that no free tail is left + behind that could satisfy the request. */ + chunk_count = ((UINT) 0); + while (chunk_count < CHUNK_LIMIT) + { + + status = tx_byte_allocate(&pool_0, &chunk[chunk_count], CHUNK_BYTES, TX_NO_WAIT); + + if (status != TX_SUCCESS) + { + + break; + } + + chunk_count++; + } + + if ((chunk_count < MINIMUM_CHUNKS) || (chunk_count == CHUNK_LIMIT)) + { + + printf("Running Byte Memory Long Search Test................................ ERROR #2\n"); + test_control_return(1); + return; + } + + /* Give every second one back. Alternating rather than consecutive, so that + no free fragment has a free neighbour and the search cannot merge two of + them into a block big enough to satisfy the request. */ + for (i = 1; i < chunk_count; i = i + ((UINT) 2)) + { + + status = tx_byte_release(chunk[i]); + + if (status != TX_SUCCESS) + { + + printf("Running Byte Memory Long Search Test................................ ERROR #3\n"); + test_control_return(1); + return; + } + + chunk[i] = TX_NULL; + } + + /* The walk has to be long enough to cross the twenty-block threshold. */ + status = tx_byte_pool_info_get(&pool_0, &name, &available, &fragments, TX_NULL, TX_NULL, TX_NULL); + + if ((status != TX_SUCCESS) || (fragments < MINIMUM_FRAGMENTS)) + { + + printf("Running Byte Memory Long Search Test................................ ERROR #4\n"); + test_control_return(1); + return; + } + + /* And the request has to fail, or the search stops at the first fragment + that fits and the walk never happens. */ + status = tx_byte_allocate(&pool_0, &probe_ptr, REQUEST_BYTES, TX_NO_WAIT); + + if (status != TX_NO_MEMORY) + { + + printf("Running Byte Memory Long Search Test................................ ERROR #5\n"); + test_control_return(1); + return; + } + + 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 Byte Memory Long Search Test................................ ERROR #6\n"); + test_control_return(1); + return; + } + + /* The workers share one priority so that the scheduler is free to put them + on different cores, which is the whole point. */ + for (i = 0; i < WORKERS; i++) + { + + search_count[i] = ((ULONG) 0); + + status = tx_thread_create(&worker[i], "worker", worker_entry, (ULONG) i, + 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 Byte Memory Long Search Test................................ ERROR #7\n"); + test_control_return(1); + } + } +} + + +static void thread_0_entry(ULONG thread_input) +{ + +UINT status; +UINT i; +CHAR *name; +ULONG available; +ULONG fragments; + + + /* Inform user. */ + printf("Running Byte Memory Long Search Test................................ "); + + /* Wait for the workers. They do not sleep, so this thread has to. */ + while (workers_done != ((ULONG) WORKERS)) + { + + tx_thread_sleep(1); + } + + for (i = 0; i < WORKERS; i++) + { + + if (search_count[i] != SEARCHES_PER_THREAD) + { + + error++; + } + } + + /* Every search failed, so nothing was taken and nothing was merged: the + pool must look exactly as it did when the workers started. */ + status = tx_byte_pool_info_get(&pool_0, &name, &available, &fragments, TX_NULL, TX_NULL, TX_NULL); + + if ((status != TX_SUCCESS) || (fragments < MINIMUM_FRAGMENTS)) + { + + error++; + } + + if (error) + { + + printf("ERROR #8\n"); + test_control_return(1); + } + else + { + + /* Successful test. */ + printf("SUCCESS!\n"); + test_control_return(0); + } +} + + +static void worker_entry(ULONG thread_input) +{ + +UINT status; +ULONG i; +void *memory_ptr; + + + for (i = 0; i < SEARCHES_PER_THREAD; i++) + { + + status = tx_byte_allocate(&pool_0, &memory_ptr, REQUEST_BYTES, TX_NO_WAIT); + + if (status != TX_NO_MEMORY) + { + + error++; + break; + } + + search_count[thread_input]++; + } + + workers_done++; +} diff --git a/test/smp/regression/threadx_object_delete_suspension_test.c b/test/smp/regression/threadx_object_delete_suspension_test.c new file mode 100644 index 000000000..20633f087 --- /dev/null +++ b/test/smp/regression/threadx_object_delete_suspension_test.c @@ -0,0 +1,481 @@ +/*************************************************************************** + * 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 deletes a block pool, a byte pool, an event flags group and a + queue while two threads are suspended on each of them, and checks that every + suspended thread is released with TX_DELETED. + + Each of those four delete routines carries the same loop, and in this suite + the whole loop body was uncovered -- thirty-six lines across the four files, + the largest single gap in the SMP coverage report: + + while (suspended_count != TX_NO_SUSPENSIONS) + { + suspended_count--; + TX_DISABLE + thread_ptr -> tx_thread_suspend_cleanup = TX_NULL; + thread_ptr -> tx_thread_suspend_status = TX_DELETED; + next_thread = thread_ptr -> tx_thread_suspended_next; + ... + _tx_thread_system_resume(thread_ptr); + thread_ptr = next_thread; + } + + The reason it was uncovered is worth writing down, because it is not that + the suite never deletes these objects -- it deletes all four after every + single test. test_control_cleanup() in the ThreadX suite deletes the + application's objects first and its threads last, so a test that ends with a + thread parked on a queue has that thread walked out of the queue by + tx_queue_delete. The SMP suite's cleanup deletes the threads first, on + purpose: it was changed so that no application-owned object is still + referenced when the object loops run, which is what stopped a class of + teardown hang. Correct, and the side effect is that every one of those + deletes now runs against an empty suspension list. + + So the coverage has to come from a test that means to delete a busy object + rather than from teardown that happens to. threadx_semaphore_delete_test + already does exactly this for semaphores, which is why tx_semaphore_delete.c + is the one member of the family that was fully covered; this is the same + idea for the other four. + + Two waiters per object rather than one, so the loop runs twice and its back + edge is taken as well as its body. + + The threads are not confined to a core. On this port they start on separate + cores and suspend in whatever order they get scheduled, so the control + thread waits on the object's own suspended count through tx_*_info_get + rather than on an ordering it cannot guarantee. The wait is bounded: a + suspension that never arrives fails the test instead of hanging it. */ + +#include +#include "tx_api.h" +#include "threadx_test_port.h" + + +void test_control_return(UINT status); + + +/* The four objects, deleted in this order. */ + +#define STAGE_BLOCK_POOL ((UINT) 0) +#define STAGE_BYTE_POOL ((UINT) 1) +#define STAGE_EVENT_FLAGS ((UINT) 2) +#define STAGE_QUEUE ((UINT) 3) +#define STAGE_COUNT ((UINT) 4) + +/* Number of threads that suspend on each object. */ + +#define WAITERS ((ULONG) 2) + +/* How many ticks the control thread will wait for a suspension before giving + up. Generous, because it is only ever reached when the test is failing -- + and finite, because the alternative is a run that hangs instead of one that + reports. */ + +#define WAIT_LIMIT_TICKS ((UINT) 200) + + +static TX_THREAD thread_0; +static TX_THREAD thread_1; +static TX_THREAD thread_2; + +static TX_BLOCK_POOL block_pool_0; +static TX_BYTE_POOL byte_pool_0; +static TX_EVENT_FLAGS_GROUP group_0; +static TX_QUEUE queue_0; + +/* Eight blocks of 16 bytes, all of them taken before any waiter runs. */ + +static UCHAR block_pool_area[TX_TEST_BLOCK_POOL_BYTES(16, 8)]; +static UCHAR byte_pool_area[TX_TEST_BYTE_POOL_BYTES(256)]; +static ULONG queue_area[16]; + +/* Counts of waiters that saw TX_DELETED, one entry per stage. */ + +static volatile ULONG deleted_count[STAGE_COUNT]; + +static UINT error = 0; + + +static void thread_0_entry(ULONG thread_input); +static void waiter_entry(ULONG thread_input); + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_object_delete_suspension_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; +void *block_ptr; +void *bytes_ptr; +UINT i; + + + /* Setup a pointer. */ + pointer = (CHAR *) first_unused_memory; + + for (i = 0; i < STAGE_COUNT; i++) + { + + deleted_count[i] = ((ULONG) 0); + } + + /* Create the block pool and take every block, so that a waiter has nothing + to be given and suspends instead. */ + status = tx_block_pool_create(&block_pool_0, "delete suspension block pool", + 16, block_pool_area, sizeof(block_pool_area)); + + if (status != TX_SUCCESS) + { + + printf("Running Object Delete Suspension Test............................... ERROR #1\n"); + test_control_return(1); + } + + while (tx_block_allocate(&block_pool_0, &block_ptr, TX_NO_WAIT) == TX_SUCCESS) + { + + /* Nothing to do with the block; the point is that the pool is empty. */ + } + + /* Same idea for the byte pool: take enough of it that the 128-byte requests + below cannot be met. */ + status = tx_byte_pool_create(&byte_pool_0, "delete suspension byte pool", + byte_pool_area, sizeof(byte_pool_area)); + + if (status != TX_SUCCESS) + { + + printf("Running Object Delete Suspension Test............................... ERROR #2\n"); + test_control_return(1); + } + + status = tx_byte_allocate(&byte_pool_0, &bytes_ptr, 200, TX_NO_WAIT); + + if (status != TX_SUCCESS) + { + + printf("Running Object Delete Suspension Test............................... ERROR #3\n"); + test_control_return(1); + } + + /* An event flags group with nothing set. The waiters ask for a flag that is + never set by anybody. */ + status = tx_event_flags_create(&group_0, "delete suspension group"); + + if (status != TX_SUCCESS) + { + + printf("Running Object Delete Suspension Test............................... ERROR #4\n"); + test_control_return(1); + } + + /* An empty queue. The waiters receive from it and nobody ever sends. */ + status = tx_queue_create(&queue_0, "delete suspension queue", TX_1_ULONG, + queue_area, sizeof(queue_area)); + + if (status != TX_SUCCESS) + { + + printf("Running Object Delete Suspension Test............................... ERROR #5\n"); + test_control_return(1); + } + + /* The control thread runs at a higher priority than the waiters, but that + decides nothing here -- it waits on the suspended counts. */ + 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 Object Delete Suspension Test............................... ERROR #6\n"); + test_control_return(1); + } + + status = tx_thread_create(&thread_1, "thread 1", waiter_entry, 1, + 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 Object Delete Suspension Test............................... ERROR #7\n"); + test_control_return(1); + } + + status = tx_thread_create(&thread_2, "thread 2", waiter_entry, 2, + 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 Object Delete Suspension Test............................... ERROR #8\n"); + test_control_return(1); + } +} + + +/* Read the number of threads suspended on the object belonging to a stage. + Returns TX_TRUE when the count could be read, so that a failing info_get is + reported rather than mistaken for "nobody has suspended yet". */ + +static UINT suspended_on(UINT stage, ULONG *count) +{ + +UINT status; + + + /* Every destination but the suspended count is TX_NULL: each info_get + tests its destinations individually and skips the ones it is not given, + so there is nothing to declare here to throw away. */ + switch (stage) + { + + case STAGE_BLOCK_POOL: + + status = tx_block_pool_info_get(&block_pool_0, TX_NULL, TX_NULL, TX_NULL, + TX_NULL, count, TX_NULL); + break; + + case STAGE_BYTE_POOL: + + status = tx_byte_pool_info_get(&byte_pool_0, TX_NULL, TX_NULL, TX_NULL, + TX_NULL, count, TX_NULL); + break; + + case STAGE_EVENT_FLAGS: + + status = tx_event_flags_info_get(&group_0, TX_NULL, TX_NULL, + TX_NULL, count, TX_NULL); + break; + + default: + + status = tx_queue_info_get(&queue_0, TX_NULL, TX_NULL, TX_NULL, + TX_NULL, count, TX_NULL); + break; + } + + return((status == TX_SUCCESS) ? ((UINT) TX_TRUE) : ((UINT) TX_FALSE)); +} + + +/* Sleep until every waiter has suspended on this stage's object, or until the + budget runs out. Returns TX_TRUE if the waiters arrived. */ + +static UINT wait_for_waiters(UINT stage) +{ + +UINT ticks; +ULONG count; + + + count = ((ULONG) 0); + for (ticks = 0; ticks < WAIT_LIMIT_TICKS; ticks++) + { + + if (suspended_on(stage, &count) == ((UINT) TX_FALSE)) + { + + return((UINT) TX_FALSE); + } + + if (count == WAITERS) + { + + return((UINT) TX_TRUE); + } + + tx_thread_sleep(1); + } + + return((UINT) TX_FALSE); +} + + +/* Sleep until every waiter has reported TX_DELETED for this stage, or until the + budget runs out. */ + +static UINT wait_for_reports(UINT stage) +{ + +UINT ticks; + + + for (ticks = 0; ticks < WAIT_LIMIT_TICKS; ticks++) + { + + if (deleted_count[stage] == WAITERS) + { + + return((UINT) TX_TRUE); + } + + tx_thread_sleep(1); + } + + return((UINT) TX_FALSE); +} + + +static void thread_0_entry(ULONG thread_input) +{ + +UINT status; +UINT stage; + + + /* Inform user. */ + printf("Running Object Delete Suspension Test............................... "); + + for (stage = 0; stage < STAGE_COUNT; stage++) + { + + /* Both waiters have to be on the list before the delete, or the loop + under test walks a shorter list than this test means it to. */ + if (wait_for_waiters(stage) == ((UINT) TX_FALSE)) + { + + printf("ERROR #9 (stage %u)\n", stage); + test_control_return(1); + } + + switch (stage) + { + + case STAGE_BLOCK_POOL: + + status = tx_block_pool_delete(&block_pool_0); + break; + + case STAGE_BYTE_POOL: + + status = tx_byte_pool_delete(&byte_pool_0); + break; + + case STAGE_EVENT_FLAGS: + + status = tx_event_flags_delete(&group_0); + break; + + default: + + status = tx_queue_delete(&queue_0); + break; + } + + if (status != TX_SUCCESS) + { + + printf("ERROR #10 (stage %u)\n", stage); + test_control_return(1); + } + + /* Every waiter must come out of the delete with TX_DELETED, which is + the status the loop under test writes into each suspended thread. */ + if (wait_for_reports(stage) == ((UINT) TX_FALSE)) + { + + printf("ERROR #11 (stage %u)\n", stage); + test_control_return(1); + } + } + + if (error) + { + + printf("ERROR #12\n"); + test_control_return(1); + } + else + { + + /* Successful test. */ + printf("SUCCESS!\n"); + test_control_return(0); + } +} + + +static void waiter_entry(ULONG thread_input) +{ + +UINT status; +UINT stage; +void *block_ptr; +void *bytes_ptr; +ULONG actual_flags; +ULONG message; + + + for (stage = 0; stage < STAGE_COUNT; stage++) + { + + switch (stage) + { + + case STAGE_BLOCK_POOL: + + /* The pool was emptied before any thread ran, so this suspends. */ + status = tx_block_allocate(&block_pool_0, &block_ptr, TX_WAIT_FOREVER); + break; + + case STAGE_BYTE_POOL: + + /* More than is left after the allocation made at define time. */ + status = tx_byte_allocate(&byte_pool_0, &bytes_ptr, 128, TX_WAIT_FOREVER); + break; + + case STAGE_EVENT_FLAGS: + + /* Nobody ever sets this flag. */ + status = tx_event_flags_get(&group_0, ((ULONG) 0x1), TX_AND_CLEAR, + &actual_flags, TX_WAIT_FOREVER); + break; + + default: + + /* Nobody ever sends to this queue. */ + status = tx_queue_receive(&queue_0, &message, TX_WAIT_FOREVER); + break; + } + + if (status == TX_DELETED) + { + + deleted_count[stage]++; + } + else + { + + /* Anything else means the object was not deleted out from under + this thread, and the control thread will say so. */ + error++; + return; + } + } +} diff --git a/test/smp/regression/threadx_thread_misaligned_stack_test.c b/test/smp/regression/threadx_thread_misaligned_stack_test.c new file mode 100644 index 000000000..fa72aab5a --- /dev/null +++ b/test/smp/regression/threadx_thread_misaligned_stack_test.c @@ -0,0 +1,156 @@ +/*************************************************************************** + * 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. + It is the SMP counterpart of the ThreadX suite's test of the same name. + + 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 line is compiled only under TX_ENABLE_STACK_CHECKING, so it is absent + from three of the five configurations' reports rather than uncovered in + them. + + 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 +#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) +{ + + /* On this port the two threads start on separate cores, so this one does + not have to wait for a relinquish to run. It sleeps well past the single + tick thread 1 sleeps for, so the checks below are made against a thread + that has actually finished either way. */ + 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/smp/regression/threadx_trace_entry_update_test.c b/test/smp/regression/threadx_trace_entry_update_test.c new file mode 100644 index 000000000..17c366004 --- /dev/null +++ b/test/smp/regression/threadx_trace_entry_update_test.c @@ -0,0 +1,367 @@ +/*************************************************************************** + * 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); +}