From 6680a06bb073d1f30c5e5f6524355a38d0c21223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Fri, 28 Aug 2026 10:26:25 -0400 Subject: [PATCH] Covered the SMP suspension teardown and the long byte pool search Against the merged SMP coverage report -- every build configuration instrumented and unioned -- sixty-four lines of common_smp/src were uncovered, 5114 of 5178. Fifty-three of them are closed here and the report reads 5167 of 5178. The SMP coverage floor goes from 98 to 99 with it. Thirty-six of the sixty-four were one loop repeated four times: the walk in tx_block_pool_delete, tx_byte_pool_delete, tx_event_flags_delete and tx_queue_delete that releases every thread suspended on the object with TX_DELETED. The suite deletes all four object types after every single test, and that is exactly why the loop never ran. 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 it by tx_queue_delete. The SMP suite's cleanup deletes the threads first, deliberately -- 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. The side effect is that all four deletes now run against an empty suspension list. tx_semaphore_delete is the one member of the family that was already covered, because threadx_semaphore_delete_test deletes a busy semaphore on purpose. threadx_object_delete_suspension_test is the same idea for the other four. Two threads suspend on each of a block pool, a byte pool, an event flags group and a queue; the control thread waits on the object's own suspended count through tx_*_info_get rather than on an ordering it cannot guarantee across four cores, deletes the object, and checks both waiters came out with TX_DELETED. Two waiters rather than one so the loop takes its back edge as well as its body, and every wait is bounded in ticks so a suspension that never arrives fails the test instead of hanging it. threadx_trace_entry_update_test and threadx_thread_misaligned_stack_test are ports of the two tests that closed the equivalent gaps in common/src, and close fourteen more lines here: tx_block_allocate 123, 175, 182, 319 and 326, tx_byte_allocate 130, 210, 217, 359 and 366, tx_thread_system_suspend 504 and 560, tx_trace_object_register 221, and tx_thread_create 133. The one substantive change is core confinement. The trace test needs thread 0 to suspend and thread 1 to then release what it waits for; on four cores thread 1 gives the block back before thread 0 has suspended and the update block behind the suspension is never reached, so both threads are excluded from cores 1 to 3. The misaligned stack test needed no such change. threadx_byte_memory_long_search_test closes three of the eleven in tx_byte_pool_search. Lines 264, 267 and 270 are the TX_BYTE_POOL_MULTIPLE_BLOCK_SEARCH limit -- twenty on this port -- where a long search drops and retakes protection so that it cannot lock the other cores out for the whole walk. No byte pool in the suite ever had twenty fragments. This one is filled with small chunks until it refuses another and then has every second chunk released, so the free fragments are never adjacent and cannot be merged, and the request is larger than any of them but smaller than the pool's theoretical total, which is what makes _tx_byte_pool_search walk rather than refuse at the door. The layout is asserted rather than assumed: the test checks the fragment count and checks the probe request really does fail before the workers start, because either would otherwise turn it into a silent no-op. Eleven lines remain and they are not a to-do list. Eight are the delay loop in tx_byte_pool_search that fires when another thread claims the pool inside the window the search opens. The Linux SMP port serialises all four cores on one pthread mutex, so that window is an unlock immediately followed by a lock on that mutex, and glibc hands an uncontended mutex straight back to the thread that just released it: measured over 180,003 windows across three cores, with zero handovers. The shipped test therefore does 250 searches per worker rather than the sixty thousand that probe used, because the twenty-block threshold is crossed by the first search. The other three are in tx_thread_smp_utilities. Line 149 is a range guard placed after the shift it is meant to guard, so reaching it needs a shift by the width of the type; the fix is to move the check above the shift, matching the TX_MAX_PRIORITIES > 32 variant of the same function, and that belongs in its own change. Lines 1073 and 1074 need a mutex owner that is genuinely executing on another core when a waiter suspends, and three shapes were tried without producing one on this port. Measured twice before and twice after, every gcda deleted between runs and 570 of 570 tests passing each time: 5114 of 5178 both times before, 5167 of 5178 both times after. Branch coverage goes from 2768 to 2821 and 2823 of 3548. A floor of 99 needs 5127, so the ratchet lands with forty lines of headroom against a numerator that has been seen moving by two between runs. Assisted-by: Claude Opus 5 --- .github/workflows/regression_test.yml | 25 +- test/smp/cmake/regression/CMakeLists.txt | 4 + .../threadx_byte_memory_long_search_test.c | 337 ++++++++++++ .../threadx_object_delete_suspension_test.c | 481 ++++++++++++++++++ .../threadx_thread_misaligned_stack_test.c | 156 ++++++ .../threadx_trace_entry_update_test.c | 367 +++++++++++++ 6 files changed, 1361 insertions(+), 9 deletions(-) create mode 100644 test/smp/regression/threadx_byte_memory_long_search_test.c create mode 100644 test/smp/regression/threadx_object_delete_suspension_test.c create mode 100644 test/smp/regression/threadx_thread_misaligned_stack_test.c create mode 100644 test/smp/regression/threadx_trace_entry_update_test.c 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); +}