summaryrefslogtreecommitdiff
path: root/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c
diff options
context:
space:
mode:
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.c514
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++;
+ }
+}