/*************************************************************************** * 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; } } }