From 76d943c6786e4709b4aabb29c2dcaed6eda4cfbe Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sat, 8 Aug 2026 23:14:12 -0700
Subject: [PATCH] test(login): own the loop context instead of pumping the
fixture's
EventLoopContext pins itself to the thread that constructed it and refuses
ExecuteTasks from any other. The fixture builds one on whichever thread built
the fixture, and xUnit gives no guarantee a test method runs on that thread,
even inside a sequential collection -- so pumping it was a coin flip. It came
up heads locally and on most CI images, and tails on CentOS 10.
Each pumping test now constructs its own context, which makes the guard pass
by construction rather than by luck, and restores the original afterwards.
Both tests share one helper, since both need the real queue rather than
ComputeInline.
---
.../Tests/Accounting/PasswordWorkerTests.cs | 89 ++++++++++++-------
1 file changed, 59 insertions(+), 30 deletions(-)
diff --git a/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs b/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs
index d25af1d46..5972e5eb9 100644
--- a/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs
+++ b/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading;
using Server.Accounting;
using Server.Accounting.Security;
using Xunit;
@@ -19,6 +20,42 @@ public class PasswordWorkerTests : IDisposable
private static Account CreateAccount(string username) =>
Accounts.GetAccount(username) as Account ?? new Account(username, Password);
+ ///
+ /// Enqueues work, then pumps the loop context until or the deadline.
+ ///
+ /// The context pins itself to the thread that constructed it and refuses ExecuteTasks
+ /// from any other. The fixture's belongs to whichever thread built the fixture, and xUnit gives
+ /// no guarantee that a test method runs on that thread even inside a sequential collection --
+ /// so this owns one for the duration and puts the original back. Pumping the fixture's context
+ /// passed locally and failed on CI.
+ ///
+ private static void PumpUntil(Action enqueue, Func complete, int timeoutSeconds = 20)
+ {
+ var original = Core.LoopContext;
+ var owned = new EventLoopContext();
+ Core.LoopContext = owned;
+
+ try
+ {
+ enqueue();
+
+ var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
+
+ while (!complete() && DateTime.UtcNow < deadline)
+ {
+ owned.ExecuteTasks();
+ Thread.Sleep(5);
+ }
+
+ // Anything that landed between the last pump and the final check.
+ owned.ExecuteTasks();
+ }
+ finally
+ {
+ Core.LoopContext = original;
+ }
+ }
+
private static PasswordJob JobFor(Account account, string submitted) =>
new()
{
@@ -49,15 +86,7 @@ public class PasswordWorkerTests : IDisposable
OnComplete = (_, outcome) => applied = outcome.Hash != null
};
- Assert.True(PasswordWorker.TryEnqueue(job));
-
- // The worker posts its result to the loop context, which no loop is pumping here.
- var deadline = DateTime.UtcNow.AddSeconds(10);
- while (!applied && DateTime.UtcNow < deadline)
- {
- Core.LoopContext.ExecuteTasks();
- System.Threading.Thread.Sleep(5);
- }
+ PumpUntil(() => Assert.True(PasswordWorker.TryEnqueue(job)), () => applied);
Assert.True(applied);
Assert.True(account.CheckPassword("a-queued-password"));
@@ -146,27 +175,27 @@ public class PasswordWorkerTests : IDisposable
var account = CreateAccount("offloop-two-writes-user");
var done = 0;
- for (var i = 1; i <= 2; i++)
- {
- Assert.True(
- PasswordWorker.TryEnqueue(
- new PasswordJob
- {
- Account = account,
- HashPhrase = account.GetRehashPhrase($"password-{i}"),
- TargetAlgorithm = AccountSecurity.CurrentAlgorithm,
- OnComplete = (_, _) => done++
- }
- )
- );
- }
-
- var deadline = DateTime.UtcNow.AddSeconds(20);
- while (done < 2 && DateTime.UtcNow < deadline)
- {
- Core.LoopContext.ExecuteTasks();
- System.Threading.Thread.Sleep(5);
- }
+ PumpUntil(
+ () =>
+ {
+ for (var i = 1; i <= 2; i++)
+ {
+ Assert.True(
+ PasswordWorker.TryEnqueue(
+ new PasswordJob
+ {
+ Account = account,
+ HashPhrase = account.GetRehashPhrase($"password-{i}"),
+ StoredAlgorithm = account.PasswordAlgorithm,
+ TargetAlgorithm = AccountSecurity.CurrentAlgorithm,
+ OnComplete = (_, _) => done++
+ }
+ )
+ );
+ }
+ },
+ () => done >= 2
+ );
Assert.Equal(2, done);
Assert.True(account.CheckPassword("password-2"));