diff options
Diffstat (limited to 'test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c')
| -rw-r--r-- | test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c | 514 |
1 files changed, 514 insertions, 0 deletions
diff --git a/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c b/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c new file mode 100644 index 00000000..488c2822 --- /dev/null +++ b/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c @@ -0,0 +1,514 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* Define the ThreadX SMP thread exit callback remote-core termination test. + + This is the SMP-specific half of the exit-callback transition coverage. The + deterministic cases are in threadx_thread_exit_callback_transition_test, + which confines every thread it drives to core 0 precisely so that they + cannot be made flaky by concurrency; this test supplies the case that needs + it. + + The victim is excluded from every core but core 1 and spins there without + relinquishing, so it is genuinely executing on another core when the + controller -- excluded to core 0 -- terminates it. _tx_thread_terminate + publishes TX_TERMINATED, releases the global SMP protection at TX_RESTORE, + and calls the victim's exit notification callback in the controller's + context on core 0. On an unfixed kernel a callback-side reset of the victim + succeeds at that point, and _tx_thread_reset both memsets the victim's stack + to the fill pattern and rebuilds its initial frame -- over the stack a live + core is running on. + + What is measured: + + - that the victim really was mapped to and executing on another core when + termination began, which is the precondition; if it cannot be + established within a bounded wait the test says so and drops only the + assertions that depend on it, rather than reporting a pass it did not + earn or spinning until something changes, + - that the callback-side delete and reset are refused, from the wrapper + and from the core service alike, + - that the victim's stack is untouched, checked with a sentinel written + into the unused low end of the stack, which a reset's fill would erase, + - that after the service returns the victim is in no core's execute slot + and on no ready list. + + Every wait is bounded and every list walk is bounded, so neither a failing + kernel nor an unlucky schedule can turn this into a hang. */ + +#include <stdio.h> +#include "tx_api.h" +#include "tx_thread.h" + + +#define CONTROLLER_PRIORITY 10 +#define VICTIM_PRIORITY 20 + +/* The core the victim is pinned to, and the exclusion maps that pin it there + and the controller to core 0. */ + +#define VICTIM_CORE 1 +#define EXCLUDE_ALL_BUT_CORE_0 0xE +#define EXCLUDE_ALL_BUT_CORE_1 0xD + +/* Bound on the wait for the victim to be mapped to its core. */ + +#define REMOTE_WAIT_TICKS 200 + +/* Sentinel written into the unused low end of the victim's stack. A reset + memsets the whole stack to the fill pattern before rebuilding the frame, so + the sentinel is gone if a reset ran. Its offset keeps it clear of both the + frame a downward-growing stack builds at the top and the stack-checking + pattern the kernel inspects at the very bottom. */ + +#define SENTINEL_OFFSET 64 +#define SENTINEL_WORDS 8 +#define SENTINEL_VALUE 0xA5A5A5A5UL + +#define READY_RING_LIMIT 32 + +#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL + +#define FAILED_TAG_LIMIT 16 + + +static TX_THREAD controller; +static TX_THREAD victim; + +static CHAR *victim_stack; + +static volatile ULONG victim_spins = 0; +static volatile UINT victim_run = TX_FALSE; + +static UINT probe_state; +static UINT probe_suspending; +static UINT probe_core_mapped; +static UINT probe_delete_status = PROBE_NOT_ATTEMPTED; +static UINT probe_core_delete_status = PROBE_NOT_ATTEMPTED; +static UINT probe_reset_status = PROBE_NOT_ATTEMPTED; +static UINT probe_core_reset_status = PROBE_NOT_ATTEMPTED; +static UINT probe_ran = TX_FALSE; +static UINT remote_confirmed = TX_FALSE; +static UINT sentinel_intact_after = TX_FALSE; + +static unsigned long expectations_checked = 0; +static unsigned long expectations_failed = 0; +static UINT first_failure = 0; +static UINT failed_tags[FAILED_TAG_LIMIT]; +static UINT failed_tag_count = 0; + + +/* Define thread prototypes. */ + +static void controller_entry(ULONG thread_input); +static void victim_entry(ULONG thread_input); + + +/* Prototype for test control return. */ + +void test_control_return(UINT status); + + +static void expect(UINT condition, UINT tag) +{ + + expectations_checked++; + + if (condition != TX_TRUE) + { + + expectations_failed++; + + if (first_failure == 0) + { + first_failure = tag; + } + + if (failed_tag_count < FAILED_TAG_LIMIT) + { + failed_tags[failed_tag_count] = tag; + failed_tag_count++; + } + } +} + + +static void expect_status(UINT actual, UINT wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void write_sentinel(void) +{ + +ULONG *words; +UINT index; + + + words = (ULONG *) (victim_stack + SENTINEL_OFFSET); + + for (index = 0; index < SENTINEL_WORDS; index++) + { + words[index] = SENTINEL_VALUE; + } +} + + +static UINT sentinel_intact(void) +{ + +ULONG *words; +UINT index; +UINT intact; + + + words = (ULONG *) (victim_stack + SENTINEL_OFFSET); + intact = TX_TRUE; + + for (index = 0; index < SENTINEL_WORDS; index++) + { + if (words[index] != SENTINEL_VALUE) + { + intact = TX_FALSE; + } + } + + return(intact); +} + + +/* Report whether thread_ptr appears in any core's execute slot or on any ready + list. Both walks are bounded. */ + +static UINT fully_detached(TX_THREAD *thread_ptr) +{ + +TX_INTERRUPT_SAVE_AREA + +TX_THREAD *walk_ptr; +TX_THREAD *head_ptr; +UINT priority; +UINT steps; +UINT core; +UINT detached; + + + detached = TX_TRUE; + + TX_DISABLE + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + if (_tx_thread_execute_ptr[core] == thread_ptr) + { + detached = TX_FALSE; + } + } + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + + head_ptr = _tx_thread_priority_list[priority]; + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT)) + { + + if (walk_ptr == thread_ptr) + { + detached = TX_FALSE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + } + + TX_RESTORE + + return(detached); +} + + +static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type) +{ + + if (thread_ptr != &victim) + { + return; + } + + if (type != TX_THREAD_EXIT) + { + return; + } + + probe_ran = TX_TRUE; + probe_state = thread_ptr -> tx_thread_state; + probe_suspending = thread_ptr -> tx_thread_suspending; + probe_core_mapped = thread_ptr -> tx_thread_smp_core_mapped; + + /* The delete first, then the reset. Each core service is called only if + its wrapper already refused, so that a kernel which allows the operation + is not asked to perform it twice. */ + + probe_delete_status = tx_thread_delete(thread_ptr); + + if (probe_delete_status != TX_SUCCESS) + { + + probe_core_delete_status = _tx_thread_delete(thread_ptr); + + if (probe_core_delete_status != TX_SUCCESS) + { + + probe_reset_status = tx_thread_reset(thread_ptr); + + if (probe_reset_status != TX_SUCCESS) + { + probe_core_reset_status = _tx_thread_reset(thread_ptr); + } + } + } +} + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_smp_thread_exit_callback_remote_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + + pointer = (CHAR *) first_unused_memory; + + status = tx_thread_create(&controller, "controller", controller_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + CONTROLLER_PRIORITY, CONTROLLER_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + victim_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_smp_core_exclude(&controller, EXCLUDE_ALL_BUT_CORE_0); + + if (status != TX_SUCCESS) + { + + printf("Running SMP Thread Exit Callback Remote Test............... ERROR #1\n"); + test_control_return(1); + } +} + + +static void controller_entry(ULONG thread_input) +{ + +UINT status; +UINT notify_status; +UINT index; +ULONG waited; +ULONG spins_before; + + + (void) thread_input; + + printf("Running SMP Thread Exit Callback Remote Test............... "); + + victim_run = TX_TRUE; + + status = tx_thread_create(&victim, "victim", victim_entry, 0, + victim_stack, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + + status += tx_thread_smp_core_exclude(&victim, EXCLUDE_ALL_BUT_CORE_1); + + notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + if (notify_status != TX_SUCCESS) + { + status = notify_status; + } +#else + + if (notify_status != TX_FEATURE_NOT_ENABLED) + { + status = notify_status; + } +#endif + + expect_status(status, TX_SUCCESS, 10); + + /* The sentinel goes in after the create, because the create fills the whole + stack with the pattern the stack-checking configurations inspect. */ + write_sentinel(); + expect(sentinel_intact(), 11); + + if (expectations_failed == 0) + { + expect_status(tx_thread_resume(&victim), TX_SUCCESS, 12); + } + + /* Wait, bounded, for the victim to be both mapped to its core and selected + to execute there, and for it to have actually spun. Those three + together are the precondition this test exists for. */ + + waited = 0; + + while ((waited < REMOTE_WAIT_TICKS) && (remote_confirmed == TX_FALSE)) + { + + if ((_tx_thread_execute_ptr[VICTIM_CORE] == &victim) && + (victim.tx_thread_smp_core_mapped == ((UINT) VICTIM_CORE)) && + (victim_spins > 0)) + { + remote_confirmed = TX_TRUE; + } + else + { + tx_thread_sleep(1); + waited++; + } + } + + /* Confirm it is still spinning, so that the termination below really does + start against a running remote thread rather than a stalled one. */ + + spins_before = victim_spins; + + if (remote_confirmed == TX_TRUE) + { + tx_thread_sleep(2); + expect(((victim_spins > spins_before) ? TX_TRUE : TX_FALSE), 13); + } + else + { + + /* Say so rather than assert it. The refusal assertions below do not + depend on the victim being remote, so they still run; the ones that + do are dropped, and this line records that the environment did not + provide the precondition. */ + printf("(remote precondition not established in %u ticks; remote-only checks skipped) ", + (UINT) REMOTE_WAIT_TICKS); + } + + if (expectations_failed == 0) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 20); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 21); + expect_status(probe_state, TX_TERMINATED, 22); + + /* The transition marker is set across the callback, which is what the + two refusals below rest on. */ + expect_status(probe_suspending, TX_TRUE, 23); + + expect_status(probe_delete_status, TX_DELETE_ERROR, 24); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 25); + expect_status(probe_reset_status, TX_NOT_DONE, 26); + expect_status(probe_core_reset_status, TX_NOT_DONE, 27); + + if (remote_confirmed == TX_TRUE) + { + + /* The callback saw the victim still mapped to the remote core. */ + expect_status(probe_core_mapped, (UINT) VICTIM_CORE, 28); + } +#endif + + /* The reset was refused, so nothing memset the stack a live core was + running on. */ + sentinel_intact_after = sentinel_intact(); + expect_status(sentinel_intact_after, TX_TRUE, 29); + + /* And the termination completed: the marker is clear, and the victim is + in no core's execute slot and on no ready list. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 30); + expect_status(victim.tx_thread_suspending, TX_FALSE, 31); + expect(fully_detached(&victim), 32); + } + + /* The victim's loop exits on this, for the case where it is still between + the terminate and its core acting on the preemption. */ + victim_run = TX_FALSE; + + if (expectations_failed == 0) + { + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 40); + } + + if (expectations_failed != 0) + { + + printf("ERROR #%u (%lu of %lu expectations failed:", + first_failure, expectations_failed, expectations_checked); + + for (index = 0; index < failed_tag_count; index++) + { + printf(" %u", failed_tags[index]); + } + + printf(")\n"); + fflush(stdout); + test_control_return(1); + } + else + { + printf("SUCCESS!\n"); + test_control_return(0); + } +} + + +static void victim_entry(ULONG thread_input) +{ + + (void) thread_input; + + /* Spin without relinquishing, so this thread occupies its core rather than + yielding it. */ + while (victim_run == TX_TRUE) + { + victim_spins++; + } +} |
