summaryrefslogtreecommitdiff
path: root/test/tx/regression/threadx_thread_exit_callback_transition_test.c
diff options
context:
space:
mode:
authorFrédéric Desbiens <[email protected]>2026-10-02 10:42:44 -0400
committerGitHub <[email protected]>2026-10-02 10:42:44 -0400
commit93387b0a60382a29579bbad5045c68328e3751df (patch)
tree42be790d602cb76f55090e5a7c418df7ea2b3428 /test/tx/regression/threadx_thread_exit_callback_transition_test.c
parent44d7c95c582d415c4ad84527180b29c93c3bf664 (diff)
parent28281cd5647d4671236ca08d19bd18ed15b2265a (diff)
Merge pull request #783 from eclipse-threadx/devv6.5.2.202603_rel
Merged changes ahead of the v6.5.2.202603 release
Diffstat (limited to 'test/tx/regression/threadx_thread_exit_callback_transition_test.c')
-rw-r--r--test/tx/regression/threadx_thread_exit_callback_transition_test.c1375
1 files changed, 1375 insertions, 0 deletions
diff --git a/test/tx/regression/threadx_thread_exit_callback_transition_test.c b/test/tx/regression/threadx_thread_exit_callback_transition_test.c
new file mode 100644
index 00000000..c931085c
--- /dev/null
+++ b/test/tx/regression/threadx_thread_exit_callback_transition_test.c
@@ -0,0 +1,1375 @@
+/***************************************************************************
+ * 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 covers the completion and termination transitions that a thread's
+ TX_THREAD_EXIT notification callback runs inside.
+
+ _tx_thread_shell_entry and _tx_thread_terminate both publish the terminal
+ state -- TX_COMPLETED or TX_TERMINATED -- and then call the thread's exit
+ notification callback, before the thread has been detached from the ready
+ list and before either service has finished with the pointer it holds to
+ the control block. That terminal state is the same state _tx_thread_delete
+ and _tx_thread_reset accept as authorization to invalidate or rebuild the
+ control block, so a callback used to be able to delete the object -- and
+ then lawfully recreate it over the same memory -- while the scheduler was
+ still linked to the old incarnation, or to reset a terminated thread and
+ resume it back onto the ready list after termination had reported success.
+
+ The tx_thread_suspending flag marks the transition for exactly its
+ duration, and delete and reset now refuse a target whose flag is still set.
+ The refusal is transient: TX_DELETE_ERROR and TX_NOT_DONE are errors those
+ two services already document, and the same call succeeds once the
+ transition has finished.
+
+ What the test measures, in every scenario:
+
+ 1. what the callback observes -- the published state, the transition
+ flag, and the status of each lifecycle service it can reach,
+ 2. that the scheduler's ready lists, priority map and execute pointer
+ stay consistent with the control blocks they point at, taken as one
+ snapshot under interrupt lockout so that the reading is not itself a
+ race,
+ 3. that the documented behaviour still works afterwards: the same delete
+ and reset succeed once the transition has completed, so nothing has
+ become permanently undeletable.
+
+ The consequences are demonstrated rather than described, and deliberately
+ from the terminator's context rather than the completing thread's:
+
+ - scenario A has the callback recreate the control block if its delete
+ unexpectedly succeeded, at a lower priority, so that the outcome is an
+ assertable disagreement -- the old priority list still heading at a
+ control block whose own priority field names a different list -- rather
+ than a fault;
+ - scenario B has the callback reset and then resume the target, so that a
+ kernel which allows the reset is caught reporting a successful
+ termination for a thread that is runnable again.
+
+ Both run while the victim is a ready thread that is not the current thread.
+ That ordering is not cosmetic. On the Linux, Win32 and Win64 host
+ simulation ports TX_THREAD_DELETE_PORT_COMPLETION cancels and joins the
+ host thread backing the deleted thread, so a callback-side delete of the
+ *completing* thread destroys the host thread the callback is running on and
+ wedges the simulator instead of reaching an assertion. The completion
+ scenarios therefore come last, and the scenario gate below stops the run
+ before them if anything earlier has already failed.
+
+ Every list walk is bounded, so a corrupted ring costs an assertion and not
+ a hang. Expectations are counted rather than short-circuited within a
+ scenario, so a failing kernel reports how much it failed by; progression
+ between scenarios is gated on there being no failures yet, so a kernel that
+ has already corrupted the scheduler is not driven further into it. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "tx_thread.h"
+
+
+/* Priorities. The controller sits above every thread it drives. The victim
+ shares its priority with two peers, so that both the multi-member and the
+ single-member ready-ring unlink paths are reached. RECREATE_PRIORITY is
+ below VICTIM_PRIORITY on purpose: see the note in the callback. */
+
+#define CONTROLLER_PRIORITY 10
+#define VICTIM_PRIORITY 20
+#define RECREATE_PRIORITY 21
+
+/* Upper bound on a ready-ring walk. The suite never has this many threads at
+ one priority, so exceeding it means the ring is broken rather than long. */
+
+#define READY_RING_LIMIT 32
+
+/* Sentinel for a probe slot the callback did not reach. */
+
+#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL
+
+
+static unsigned long controller_counter = 0;
+static unsigned long victim_counter = 0;
+static unsigned long peer_a_counter = 0;
+static unsigned long peer_b_counter = 0;
+static unsigned long replacement_counter = 0;
+static unsigned long callback_entry_counter = 0;
+static unsigned long callback_exit_counter = 0;
+
+static TX_THREAD controller;
+static TX_THREAD victim;
+static TX_THREAD peer_a;
+static TX_THREAD peer_b;
+static TX_SEMAPHORE victim_semaphore;
+static TX_MUTEX victim_mutex;
+
+static CHAR *victim_stack;
+static CHAR *replacement_stack;
+
+/* What the victim's entry function should do before it returns. */
+
+static volatile UINT victim_hold = TX_FALSE;
+static volatile UINT victim_self_terminate = TX_FALSE;
+static volatile UINT victim_take_mutex = TX_FALSE;
+static volatile UINT victim_wait_semaphore = TX_FALSE;
+static volatile UINT peers_run = TX_FALSE;
+
+/* What the exit callback should attempt the next time it runs. Independent
+ flags rather than one mode, because a delete that unexpectedly succeeds
+ destroys the object and would mask the reset measurement behind it. */
+
+static volatile UINT probe_delete_enabled = TX_FALSE;
+static volatile UINT probe_reset_enabled = TX_FALSE;
+static volatile UINT probe_misc_enabled = TX_FALSE;
+static volatile UINT probe_reuse_enabled = TX_FALSE;
+
+/* What the callback saw the last time it probed. */
+
+static UINT probe_state;
+static UINT probe_suspending;
+static UINT probe_delete_status;
+static UINT probe_core_delete_status;
+static UINT probe_reset_status;
+static UINT probe_core_reset_status;
+static UINT probe_resume_status;
+static UINT probe_suspend_status;
+static UINT probe_terminate_status;
+static UINT probe_create_status;
+static UINT probe_consistent;
+static UINT probe_ran;
+static UINT delete_succeeded;
+
+/* Ordering witness for the notification pair. Entry and exit counts cannot
+ simply be compared: a victim created with TX_DONT_START never reports an
+ entry, and an incarnation that is reset and terminated again reports a
+ second exit with no second entry. What must hold is that an exit for a
+ thread that ran is preceded by that thread's entry. */
+
+static UINT last_notify_type = PROBE_NOT_ATTEMPTED;
+static UINT exit_saw_entry_first = TX_FALSE;
+
+/* Expectation accounting. */
+
+#define FAILED_TAG_LIMIT 32
+
+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 task prototypes. */
+
+static void controller_entry(ULONG thread_input);
+static void victim_entry(ULONG thread_input);
+static void peer_entry(ULONG thread_input);
+static void replacement_entry(ULONG thread_input);
+
+
+/* Prototype for test control return. */
+
+void test_control_return(UINT status);
+
+
+/* Record one expectation. tag identifies it in the failure report; the first
+ failing tag is the number the suite prints, so tags are allocated in the
+ order the expectations are written. */
+
+static void expect(UINT condition, UINT tag)
+{
+
+ expectations_checked++;
+
+ if (condition != TX_TRUE)
+ {
+
+ expectations_failed++;
+
+ if (first_failure == 0)
+ {
+ first_failure = tag;
+ }
+
+ /* Keep the tags, so that a failing kernel reports which expectations
+ it failed rather than only the first and a total. */
+ 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 expect_pointer(void *actual, void *wanted, UINT tag)
+{
+
+ if (actual == wanted)
+ {
+ expect(TX_TRUE, tag);
+ }
+ else
+ {
+ expect(TX_FALSE, tag);
+ }
+}
+
+
+static void expect_count(unsigned long actual, unsigned long wanted, UINT tag)
+{
+
+ if (actual == wanted)
+ {
+ expect(TX_TRUE, tag);
+ }
+ else
+ {
+ expect(TX_FALSE, tag);
+ }
+}
+
+
+/* Walk one ready ring and report whether thread_ptr is a member of it. The
+ walk is bounded, so a ring that does not close is reported as a
+ non-membership rather than spun on for ever. */
+
+static UINT ready_ring_contains(UINT priority, TX_THREAD *thread_ptr)
+{
+
+TX_THREAD *walk_ptr;
+TX_THREAD *head_ptr;
+UINT steps;
+UINT found;
+
+
+ found = TX_FALSE;
+ head_ptr = _tx_thread_priority_list[priority];
+ walk_ptr = head_ptr;
+ steps = 0;
+
+ while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT) && (found == TX_FALSE))
+ {
+
+ if (walk_ptr == thread_ptr)
+ {
+ found = TX_TRUE;
+ }
+
+ walk_ptr = walk_ptr -> tx_thread_ready_next;
+ steps++;
+
+ /* The ring has closed. */
+ if (walk_ptr == head_ptr)
+ {
+ walk_ptr = TX_NULL;
+ }
+ }
+
+ return(found);
+}
+
+
+/* Take one snapshot of the scheduler's ready structures and check that they
+ agree with the control blocks they point at.
+
+ This is the assertion the pre-fix behaviour fails. A callback that deletes
+ the victim and recreates it at another priority leaves the old priority list
+ heading at that control block while the block's own priority field names the
+ new list, and leaves the old priority's map bit set with nothing behind it.
+ Neither disagreement needs a fault to be visible.
+
+ The whole snapshot is taken under interrupt lockout -- which is the global
+ SMP protection on an SMP port -- so that reading the scheduler's state is
+ not itself a race against another core.
+
+ transitioning names the one thread that is allowed to be on a ready list
+ without being ready, which is precisely the thread whose completion or
+ termination transition is in progress: the terminal state is published
+ before the detachment, so inside the callback the victim is legitimately a
+ TX_COMPLETED or TX_TERMINATED member of its ready ring. That single
+ exemption is from the state clause only. Every other clause, and in
+ particular the clause that requires a ready-list member to agree that this
+ is its priority -- the one a callback-side delete and recreate breaks --
+ still applies to it. */
+
+static UINT scheduler_consistent_except(TX_THREAD *transitioning)
+{
+
+TX_INTERRUPT_SAVE_AREA
+
+TX_THREAD *head_ptr;
+TX_THREAD *walk_ptr;
+TX_THREAD *execute_ptr;
+UINT priority;
+UINT steps;
+UINT consistent;
+#ifdef TX_THREAD_SMP_MAX_CORES
+UINT core;
+#endif
+
+
+ consistent = TX_TRUE;
+
+ TX_DISABLE
+
+ for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++)
+ {
+
+ head_ptr = _tx_thread_priority_list[priority];
+
+#if TX_MAX_PRIORITIES <= 32
+
+ /* A priority with a ready list must have its map bit set, and a
+ priority with no ready list must not. */
+ if (head_ptr == TX_NULL)
+ {
+ if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) != ((ULONG) 0))
+ {
+ consistent = TX_FALSE;
+ }
+ }
+ else
+ {
+ if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) == ((ULONG) 0))
+ {
+ consistent = TX_FALSE;
+ }
+ }
+#endif
+
+ walk_ptr = head_ptr;
+ steps = 0;
+
+ while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT))
+ {
+
+ /* Every thread on a ready list must be a live thread, must be
+ ready, and must agree that this is its priority. */
+ if (walk_ptr -> tx_thread_id != TX_THREAD_ID)
+ {
+ consistent = TX_FALSE;
+ }
+
+ if ((walk_ptr -> tx_thread_state != TX_READY) && (walk_ptr != transitioning))
+ {
+ consistent = TX_FALSE;
+ }
+
+ if (walk_ptr -> tx_thread_priority != priority)
+ {
+ consistent = TX_FALSE;
+ }
+
+ /* The ring must be doubly linked. */
+ if (walk_ptr -> tx_thread_ready_next -> tx_thread_ready_previous != walk_ptr)
+ {
+ consistent = TX_FALSE;
+ }
+
+ walk_ptr = walk_ptr -> tx_thread_ready_next;
+ steps++;
+
+ if (walk_ptr == head_ptr)
+ {
+ walk_ptr = TX_NULL;
+ }
+ }
+
+ /* A ring that did not close inside the bound is broken. */
+ if (walk_ptr != TX_NULL)
+ {
+ consistent = TX_FALSE;
+ }
+ }
+
+ /* Whatever the scheduler has selected to execute must be a member of the
+ ready list that its own priority field names. */
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++)
+ {
+
+ execute_ptr = _tx_thread_execute_ptr[core];
+
+ if (execute_ptr != TX_NULL)
+ {
+ if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE)
+ {
+ consistent = TX_FALSE;
+ }
+ }
+ }
+#else
+
+ execute_ptr = _tx_thread_execute_ptr;
+
+ if (execute_ptr != TX_NULL)
+ {
+ if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE)
+ {
+ consistent = TX_FALSE;
+ }
+ }
+#endif
+
+ TX_RESTORE
+
+ return(consistent);
+}
+
+
+static UINT scheduler_consistent(void)
+{
+ return(scheduler_consistent_except(TX_NULL));
+}
+
+
+/* Report whether thread_ptr is absent from every ready list and from every
+ execute slot, which is what a completed or terminated thread must be once
+ its transition has finished. */
+
+static UINT fully_detached(TX_THREAD *thread_ptr)
+{
+
+TX_INTERRUPT_SAVE_AREA
+
+UINT priority;
+UINT detached;
+#ifdef TX_THREAD_SMP_MAX_CORES
+UINT core;
+#endif
+
+
+ detached = TX_TRUE;
+
+ TX_DISABLE
+
+ for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++)
+ {
+ if (ready_ring_contains(priority, thread_ptr) == TX_TRUE)
+ {
+ detached = TX_FALSE;
+ }
+ }
+
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++)
+ {
+ if (_tx_thread_execute_ptr[core] == thread_ptr)
+ {
+ detached = TX_FALSE;
+ }
+ }
+#else
+
+ if (_tx_thread_execute_ptr == thread_ptr)
+ {
+ detached = TX_FALSE;
+ }
+#endif
+
+ TX_RESTORE
+
+ return(detached);
+}
+
+
+static void arm_probe(UINT probe_delete, UINT probe_reset, UINT probe_misc, UINT probe_reuse)
+{
+
+ probe_state = PROBE_NOT_ATTEMPTED;
+ probe_suspending = PROBE_NOT_ATTEMPTED;
+ probe_delete_status = PROBE_NOT_ATTEMPTED;
+ probe_core_delete_status = PROBE_NOT_ATTEMPTED;
+ probe_reset_status = PROBE_NOT_ATTEMPTED;
+ probe_core_reset_status = PROBE_NOT_ATTEMPTED;
+ probe_resume_status = PROBE_NOT_ATTEMPTED;
+ probe_suspend_status = PROBE_NOT_ATTEMPTED;
+ probe_terminate_status = PROBE_NOT_ATTEMPTED;
+ probe_create_status = PROBE_NOT_ATTEMPTED;
+ probe_consistent = PROBE_NOT_ATTEMPTED;
+ probe_ran = TX_FALSE;
+ delete_succeeded = TX_FALSE;
+
+ probe_delete_enabled = probe_delete;
+ probe_reset_enabled = probe_reset;
+ probe_misc_enabled = probe_misc;
+ probe_reuse_enabled = probe_reuse;
+}
+
+
+/* The exit notification callback. Everything it learns is recorded for the
+ controller to check; it prints nothing and it never suspends, so it obeys
+ the one restriction the manual places on a notification callback. */
+
+static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type)
+{
+
+ /* Only the victim's notifications are of interest. Its control block is
+ reused across scenarios, so the pointer is the identity. */
+ if (thread_ptr != &victim)
+ {
+ return;
+ }
+
+ if (type == TX_THREAD_ENTRY)
+ {
+ callback_entry_counter++;
+ last_notify_type = TX_THREAD_ENTRY;
+ return;
+ }
+
+ if (type != TX_THREAD_EXIT)
+ {
+ return;
+ }
+
+ callback_exit_counter++;
+
+ if (last_notify_type == TX_THREAD_ENTRY)
+ {
+ exit_saw_entry_first = TX_TRUE;
+ }
+
+ last_notify_type = TX_THREAD_EXIT;
+
+ if ((probe_delete_enabled == TX_FALSE) && (probe_reset_enabled == TX_FALSE) &&
+ (probe_misc_enabled == TX_FALSE))
+ {
+ return;
+ }
+
+ probe_ran = TX_TRUE;
+
+ /* What the kernel has published about the thread at this point. */
+ probe_state = thread_ptr -> tx_thread_state;
+ probe_suspending = thread_ptr -> tx_thread_suspending;
+
+ /* None of the services below has a suspension option, which is the only
+ thing the manual forbids a callback from using, so every one of them is
+ reachable from here.
+
+ Where a service is expected to refuse, the core service is called after
+ the _txe_ wrapper as well. The wrapper is what an ordinary build
+ reaches; the core service is what a TX_DISABLE_ERROR_CHECKING build
+ reaches directly, and the refusal has to live there so that disabling
+ error checking cannot disable it. The core call is made only when the
+ wrapper already refused, so that a kernel which allows the operation is
+ not asked to perform it twice. */
+
+ if (probe_delete_enabled == TX_TRUE)
+ {
+
+ probe_delete_status = tx_thread_delete(thread_ptr);
+
+ if (probe_delete_status == TX_SUCCESS)
+ {
+ delete_succeeded = TX_TRUE;
+ }
+ else
+ {
+
+ probe_core_delete_status = _tx_thread_delete(thread_ptr);
+
+ if (probe_core_delete_status == TX_SUCCESS)
+ {
+ delete_succeeded = TX_TRUE;
+ }
+ }
+
+ /* Recreate the control block only if the delete unexpectedly
+ succeeded. On a fixed kernel this never runs. On an unfixed one it
+ is the lawful next step after a successful delete, and it is what
+ turns the window into an observable inconsistency: tx_thread_create
+ zeroes the whole control block, and the auto-start then links it
+ onto RECREATE_PRIORITY's ready list while the old incarnation is
+ still the head of VICTIM_PRIORITY's.
+
+ RECREATE_PRIORITY is the lower of the two so that the stale head is
+ the one the scheduler selects from, and so that the ring at the new
+ priority has a single member. Recreating at the same priority would
+ instead have the auto-start write through the control block's
+ freshly zeroed ready_previous pointer, which is a fault rather than
+ an assertion. */
+ if ((delete_succeeded == TX_TRUE) && (probe_reuse_enabled == TX_TRUE))
+ {
+ probe_create_status = tx_thread_create(thread_ptr, "replacement",
+ replacement_entry, 0,
+ replacement_stack,
+ TEST_STACK_SIZE_PRINTF,
+ RECREATE_PRIORITY, RECREATE_PRIORITY,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+ }
+ }
+
+ if ((probe_reset_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE))
+ {
+
+ probe_reset_status = tx_thread_reset(thread_ptr);
+
+ if (probe_reset_status != TX_SUCCESS)
+ {
+ probe_core_reset_status = _tx_thread_reset(thread_ptr);
+ }
+
+ /* Follow the reset with the resume that the reset would have unlocked.
+ On a fixed kernel the state is still terminal and the resume is
+ refused. On an unfixed one the reset has moved the state to
+ TX_SUSPENDED, the resume finds a suspending thread whose state is no
+ longer terminal, clears the flag and puts the thread back on the
+ ready list -- and the outer service then skips the removal and
+ reports success for a termination that did not happen. */
+ probe_resume_status = tx_thread_resume(thread_ptr);
+ }
+
+ if ((probe_misc_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE))
+ {
+ probe_suspend_status = tx_thread_suspend(thread_ptr);
+ probe_terminate_status = tx_thread_terminate(thread_ptr);
+ }
+
+ probe_consistent = scheduler_consistent_except(thread_ptr);
+}
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_thread_exit_callback_transition_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+
+
+ /* Put first available memory address into a character 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;
+
+ status += tx_thread_create(&peer_a, "peer a", peer_entry, 1,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ VICTIM_PRIORITY, VICTIM_PRIORITY,
+ TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ status += tx_thread_create(&peer_b, "peer b", peer_entry, 2,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ VICTIM_PRIORITY, VICTIM_PRIORITY,
+ TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ /* The victim's control block is created and destroyed repeatedly, so its
+ two stacks are set aside here rather than per scenario. */
+ victim_stack = pointer;
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+ replacement_stack = pointer;
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ status += tx_semaphore_create(&victim_semaphore, "victim semaphore", 0);
+ status += tx_mutex_create(&victim_mutex, "victim mutex", TX_INHERIT);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Exit Callback Transition Test............... ERROR #1\n");
+ test_control_return(1);
+ }
+
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ /* Confine every thread this test drives to core 0. The measurements below
+ are of global scheduler structures at chosen moments, and letting the
+ peers and the victim run on other cores would make them a race against
+ the test rather than a property of it. A victim executing on a
+ different core when termination begins is deliberately covered by a
+ separate SMP test, so that the deterministic cases here cannot be made
+ flaky by it. */
+ status = tx_thread_smp_core_exclude(&controller, 0xE);
+ status += tx_thread_smp_core_exclude(&peer_a, 0xE);
+ status += tx_thread_smp_core_exclude(&peer_b, 0xE);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Exit Callback Transition Test............... ERROR #2\n");
+ test_control_return(1);
+ }
+#endif
+}
+
+
+/* Create the victim over its own control block, with the exit callback
+ registered. The notify status is folded into the result according to what
+ the configuration supports. */
+
+static UINT create_victim(UINT priority, UINT auto_start)
+{
+
+UINT status;
+UINT notify_status;
+
+
+ status = tx_thread_create(&victim, "victim", victim_entry, 0,
+ victim_stack, TEST_STACK_SIZE_PRINTF,
+ priority, priority, TX_NO_TIME_SLICE, auto_start);
+
+ 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
+
+ /* Notification is compiled out, so the registration is expected to be
+ refused and the callback never runs. */
+ if (notify_status != TX_FEATURE_NOT_ENABLED)
+ {
+ status = notify_status;
+ }
+#endif
+
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ if (status == TX_SUCCESS)
+ {
+ status = tx_thread_smp_core_exclude(&victim, 0xE);
+ }
+#endif
+
+ return(status);
+}
+
+
+/* The gate between scenarios. A kernel that has already failed an expectation
+ has, in these scenarios, already left the scheduler inconsistent, and
+ driving it further would report nothing more while risking a wedge instead
+ of a verdict. */
+
+static UINT ok_so_far(void)
+{
+
+UINT result;
+
+
+ result = TX_FALSE;
+
+ if (expectations_failed == 0)
+ {
+ result = TX_TRUE;
+ }
+
+ return(result);
+}
+
+
+static void controller_entry(ULONG thread_input)
+{
+
+ULONG expected_exits;
+ULONG expected_entries;
+UINT index;
+
+
+ (void) thread_input;
+
+ controller_counter++;
+ expected_exits = 0;
+ expected_entries = 0;
+
+ /* Inform user. */
+ printf("Running Thread Exit Callback Transition Test............... ");
+
+ /* The scheduler must be self-consistent before anything is measured, so
+ that a later failure is attributable to this test. */
+ expect(scheduler_consistent(), 10);
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario A: terminate a ready target that is not the current thread, */
+ /* with two peers ready at the same priority. The callback attempts */
+ /* the delete, and recreates the control block if it succeeded. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ peers_run = TX_TRUE;
+ victim_hold = TX_TRUE;
+
+ expect_status(tx_thread_resume(&peer_a), TX_SUCCESS, 11);
+ expect_status(tx_thread_resume(&peer_b), TX_SUCCESS, 12);
+
+ arm_probe(TX_TRUE, TX_FALSE, TX_FALSE, TX_TRUE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 13);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ /* Let the victim and both peers become ready and start spinning, so
+ that the victim's unlink is from a multi-member ring. */
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_READY, 14);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &victim), 15);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 16);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 17);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 18);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 19);
+
+ /* The callback runs after the terminal state has been published, and
+ inside the transition, which is what the flag records. */
+ expect_status(probe_state, TX_TERMINATED, 20);
+ expect_status(probe_suspending, TX_TRUE, 21);
+
+ /* Both the wrapper and the core service must refuse. */
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 22);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 23);
+
+ /* So the recreate never happened and nothing is inconsistent. */
+ expect_status(delete_succeeded, TX_FALSE, 24);
+ expect_status(probe_create_status, PROBE_NOT_ATTEMPTED, 25);
+ expect_status(probe_consistent, TX_TRUE, 26);
+ expect_count(replacement_counter, 0, 27);
+#endif
+
+ /* After the service returns the termination is complete: the state is
+ terminal, the marker is clear, the target is on no ready list and in
+ no execute slot, and the peers are undisturbed. */
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 28);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 29);
+ expect(fully_detached(&victim), 30);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 31);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 32);
+ expect(scheduler_consistent(), 33);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ /* The documented behaviour is available again. */
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 34);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario B: the same termination, but the callback attempts a reset */
+ /* and then the resume that a successful reset would have unlocked. */
+ /* */
+ /* This is the leg where the transition check is the only thing */
+ /* standing between the callback and a reset of a thread that is still */
+ /* on the ready list. Without it the reset succeeds, the resume */
+ /* clears the suspending flag and restores TX_READY, the outer service */
+ /* then finds the flag clear and skips the removal, and */
+ /* tx_thread_terminate returns TX_SUCCESS for a runnable thread. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_TRUE, TX_FALSE, TX_FALSE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 40);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_READY, 41);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 42);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 43);
+ expect_status(probe_state, TX_TERMINATED, 44);
+ expect_status(probe_suspending, TX_TRUE, 45);
+
+ /* The discriminating leg. The target is not the current thread and
+ its published state is terminal, so both of the conditions the
+ service tested before this fix are satisfied. */
+ expect_status(probe_reset_status, TX_NOT_DONE, 46);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 47);
+
+ /* With the reset refused, the state is still terminal, so the resume
+ cannot reach the branch that would void the termination. */
+ expect_status(probe_resume_status, TX_RESUME_ERROR, 48);
+ expect_status(probe_consistent, TX_TRUE, 49);
+#endif
+
+ /* And the termination stands. A kernel that let the callback reset
+ and resume the target arrives here with the victim ready to run. */
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 50);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 51);
+ expect(fully_detached(&victim), 52);
+ expect(scheduler_consistent(), 53);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 54);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 55);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 56);
+ }
+
+ /* Retire the peers, so that the ring drops to a single member and then to
+ none, and check consistency across both unlinks. */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ peers_run = TX_FALSE;
+ tx_thread_sleep(3);
+
+ expect_status(peer_a.tx_thread_state, TX_COMPLETED, 57);
+ expect_status(peer_b.tx_thread_state, TX_COMPLETED, 58);
+ expect(scheduler_consistent(), 59);
+ expect_status(tx_thread_delete(&peer_a), TX_SUCCESS, 60);
+ expect_status(tx_thread_delete(&peer_b), TX_SUCCESS, 61);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario C: terminate a target suspended on an object, so that the */
+ /* non-ready branch of _tx_thread_terminate runs -- suspension cleanup, */
+ /* the terminated extension, the callback, and mutex release, in that */
+ /* order. That branch used to clear the marker before the callback */
+ /* rather than after the last dereference of the target. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE);
+ victim_hold = TX_FALSE;
+ victim_take_mutex = TX_TRUE;
+ victim_wait_semaphore = TX_TRUE;
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 70);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ /* Let the victim take the mutex and suspend on the semaphore. */
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_SEMAPHORE_SUSP, 71);
+ expect_pointer(victim_mutex.tx_mutex_owner, &victim, 72);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 73);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 74);
+ expect_status(probe_state, TX_TERMINATED, 75);
+
+ /* The discriminating leg for this branch. The marker used to be
+ cleared before this point, which left the callback free to delete or
+ reset a control block that the service still had mutex-release
+ processing to do against. */
+ expect_status(probe_suspending, TX_TRUE, 76);
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 77);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 78);
+ expect_status(probe_reset_status, TX_NOT_DONE, 79);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 80);
+ expect_status(probe_resume_status, TX_RESUME_ERROR, 81);
+ expect_status(delete_succeeded, TX_FALSE, 82);
+ expect_status(probe_consistent, TX_TRUE, 83);
+#endif
+
+ /* The marker is cleared at the safe point, after the mutex release,
+ and the mutex the terminated thread owned has been given up. The
+ value of the flag during the release itself is not observable from
+ application code; what is observable is that there is exactly one
+ clear site and that it sits downstream of the release. */
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 84);
+ expect_pointer(victim_mutex.tx_mutex_owner, TX_NULL, 85);
+ expect_count((unsigned long) victim_semaphore.tx_semaphore_suspended_count, 0, 86);
+ expect(fully_detached(&victim), 87);
+ expect(scheduler_consistent(), 88);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 89);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario D: natural completion, victim alone at its priority. */
+ /* */
+ /* The callback here runs on the completing thread itself, so a */
+ /* callback-side delete that succeeds destroys the host thread on the */
+ /* simulation ports. This scenario is therefore placed after the ones */
+ /* that reach an assertion, and the gate above stops the run before it */
+ /* if any of them has already failed. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_TRUE, TX_TRUE, TX_TRUE, TX_FALSE);
+ victim_take_mutex = TX_FALSE;
+ victim_wait_semaphore = TX_FALSE;
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 100);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ /* Let the victim run to completion, so that its callback runs inside
+ the completion transition. */
+ tx_thread_sleep(3);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 101);
+ expect_status(probe_state, TX_COMPLETED, 102);
+ expect_status(probe_suspending, TX_TRUE, 103);
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 104);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 105);
+
+ /* Reset of the running thread is refused for two independent reasons
+ here -- it is the current thread, and its transition is in progress
+ -- so this leg is a guard rather than a discriminator. Scenario B
+ is where the transition check is the only thing refusing a reset. */
+ expect_status(probe_reset_status, TX_NOT_DONE, 106);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 107);
+
+ /* A completed thread is neither resumable nor suspendable, and
+ terminating one is a documented no-op. */
+ expect_status(probe_resume_status, TX_RESUME_ERROR, 108);
+ expect_status(probe_suspend_status, TX_SUSPEND_ERROR, 109);
+ expect_status(probe_terminate_status, TX_SUCCESS, 110);
+ expect_status(delete_succeeded, TX_FALSE, 111);
+ expect_status(probe_consistent, TX_TRUE, 112);
+#endif
+
+ /* The transition has finished, so the calls the callback was refused
+ must succeed. The refusal is transient and nothing has become
+ permanently undeletable. */
+ expect_status(victim.tx_thread_state, TX_COMPLETED, 113);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 114);
+ expect(fully_detached(&victim), 115);
+ expect(scheduler_consistent(), 116);
+ expect(((victim_counter > 0) ? TX_TRUE : TX_FALSE), 117);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ /* Reset, so that the documented reset of a completed thread is covered
+ on a target whose transition has finished, and so that the
+ reset/terminate/delete order is exercised on this branch too. */
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 118);
+ expect_status(victim.tx_thread_state, TX_SUSPENDED, 119);
+ expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 120);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 121);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 122);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 123);
+ }
+
+ /* A safe post-completion recreate at a different priority, which is the
+ lifecycle the callback was refused, performed at the point it is
+ legitimate. It exercises the priority map and the list heads for a
+ recreate that moves priority as well as for one that does not. */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(create_victim(RECREATE_PRIORITY, TX_AUTO_START), TX_SUCCESS, 124);
+
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_COMPLETED, 125);
+ expect(scheduler_consistent(), 126);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 127);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario E: self termination. The callback fires once, in the */
+ /* victim's own context, the lifecycle mutation is refused there too, */
+ /* and another thread can complete the lifecycle afterwards. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ victim_self_terminate = TX_TRUE;
+ expected_exits = callback_exit_counter;
+
+ arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 130);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ tx_thread_sleep(3);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ /* Exactly one exit notification, even though the thread terminated
+ itself from inside its own entry function. */
+ expect_count(callback_exit_counter, expected_exits + 1, 131);
+ expect_status(probe_state, TX_TERMINATED, 132);
+ expect_status(probe_suspending, TX_TRUE, 133);
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 134);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 135);
+ expect_status(probe_reset_status, TX_NOT_DONE, 136);
+ expect_status(delete_succeeded, TX_FALSE, 137);
+ expect_status(probe_consistent, TX_TRUE, 138);
+#else
+
+ expect_count(callback_exit_counter, expected_exits, 139);
+#endif
+
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 140);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 141);
+ expect(fully_detached(&victim), 142);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 143);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 144);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 145);
+ expect(scheduler_consistent(), 146);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario F: a benign callback is unaffected. The notification */
+ /* ordering and counts are what they were. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ victim_self_terminate = TX_FALSE;
+ expected_exits = callback_exit_counter;
+ expected_entries = callback_entry_counter;
+ exit_saw_entry_first = TX_FALSE;
+ last_notify_type = PROBE_NOT_ATTEMPTED;
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 150);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ tx_thread_sleep(3);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ /* This incarnation ran and completed, so it reported exactly one entry
+ and one exit, and the entry came first. The transition marker has
+ changed neither how often notifications are delivered nor in what
+ order. */
+ expect_count(callback_exit_counter, expected_exits + 1, 151);
+ expect_count(callback_entry_counter, expected_entries + 1, 152);
+ expect_status(exit_saw_entry_first, TX_TRUE, 153);
+#else
+
+ expect_count(callback_exit_counter, expected_exits, 154);
+ expect_count(callback_entry_counter, expected_entries, 155);
+#endif
+
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 156);
+ expect(scheduler_consistent(), 157);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario G: state and boundary cases. A thread that never entered a */
+ /* terminal state, and one that is already fully detached, behave as */
+ /* documented -- the new refusal applies only while the marker is set. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_DONT_START), TX_SUCCESS, 170);
+
+ /* Suspended, never run: neither state is terminal, so both services
+ are refused for the reason they always were, and the marker is not
+ set on a thread that is not transitioning. */
+ expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 171);
+ expect_status(tx_thread_reset(&victim), TX_NOT_DONE, 172);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 173);
+
+ /* Terminated from a suspended state: deletable immediately, and
+ repeatedly resettable and re-terminable in between. */
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 174);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 175);
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 176);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 177);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 178);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 179);
+ expect(scheduler_consistent(), 180);
+ }
+
+ 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]);
+ }
+
+ if (expectations_failed > ((unsigned long) failed_tag_count))
+ {
+ printf(" ...");
+ }
+
+ 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;
+
+ victim_counter++;
+
+ if (victim_take_mutex == TX_TRUE)
+ {
+ tx_mutex_get(&victim_mutex, TX_NO_WAIT);
+ }
+
+ if (victim_wait_semaphore == TX_TRUE)
+ {
+
+ /* Suspend on an object, so that a termination takes the non-ready
+ branch and runs the suspension cleanup routine. */
+ tx_semaphore_get(&victim_semaphore, TX_WAIT_FOREVER);
+ }
+
+ if (victim_self_terminate == TX_TRUE)
+ {
+
+ /* This does not return: the termination detaches this thread and the
+ preemption check at the end of the service switches away from it. */
+ tx_thread_terminate(tx_thread_identify());
+ }
+
+ while (victim_hold == TX_TRUE)
+ {
+
+ /* Stay ready, so that a terminate issued by another thread finds this
+ thread on the ready list. */
+ tx_thread_relinquish();
+ }
+
+ /* Returning enters the completion transition. */
+}
+
+
+static void peer_entry(ULONG thread_input)
+{
+
+ if (thread_input == 1)
+ {
+ peer_a_counter++;
+ }
+ else
+ {
+ peer_b_counter++;
+ }
+
+ while (peers_run == TX_TRUE)
+ {
+
+ /* Stay ready at the victim's priority, so that the victim's ready ring
+ has more than one member. */
+ tx_thread_relinquish();
+ }
+}
+
+
+static void replacement_entry(ULONG thread_input)
+{
+
+ (void) thread_input;
+
+ replacement_counter++;
+
+ /* Only reached on a kernel that let the callback recreate the control
+ block. Suspend rather than return, so that the run does not depend on a
+ second completion transition over a control block already known to be
+ inconsistent. */
+ tx_thread_suspend(tx_thread_identify());
+}