test(headless): cover K4 death and cancellation

This commit is contained in:
Erik 2026-07-27 11:08:31 +02:00
parent 97c174fbb2
commit bd236ce553
2 changed files with 115 additions and 17 deletions

View file

@ -358,6 +358,76 @@ public sealed class HeadlessProcessSchedulerTests
StringComparison.Ordinal);
}
[Fact]
public async Task ThirtySessionsConvergeAcrossRandomizedCancellationEdges()
{
const int sessionCount = 30;
var random = new Random(0xAC2013);
for (int iteration = 0; iteration < 5; iteration++)
{
var configuration = new HeadlessConfiguration
{
Version = 1,
Sessions = Enumerable.Range(0, sessionCount)
.Select(index => Descriptor(
$"cancel-{iteration}-{index}",
$"cancel-{iteration}-{index}-stdin"))
.Cast<HeadlessSessionDescriptor?>()
.ToList(),
};
var operations = new FixtureSessionOperations();
using var diagnostics = new StringWriter();
using var host = new HeadlessProcessHost(
configuration,
HeadlessPathSet.Resolve(
new HeadlessPathOverrides()),
new System.IO.StringReader(string.Concat(
Enumerable.Repeat(
"random-cancel-password"
+ Environment.NewLine,
sessionCount))),
diagnostics,
operations);
using var cancellation = new CancellationTokenSource(
TimeSpan.FromMilliseconds(random.Next(37, 144)));
HeadlessExitCode result =
await host.RunAsync(cancellation.Token);
Assert.True(cancellation.IsCancellationRequested);
Assert.Equal(HeadlessExitCode.Success, result);
Assert.Equal(sessionCount, operations.CreatedSessionCount);
Assert.All(
host.Sessions,
static session =>
Assert.False(session.IsFaulted));
host.Dispose();
Assert.Equal(sessionCount, operations.DisposedSessionCount);
Assert.All(
host.Sessions,
static session =>
Assert.True(
session.Runtime
.CaptureOwnership()
.IsConverged));
string output = diagnostics.ToString();
Assert.Contains(
"\"state\":\"disposed\"",
output,
StringComparison.Ordinal);
Assert.Contains(
$"\"convergedRuntimeCount\":{sessionCount}",
output,
StringComparison.Ordinal);
Assert.DoesNotContain(
"random-cancel-password",
output,
StringComparison.Ordinal);
}
}
[Fact]
public void ThrowingPolicyQuarantinesOnlyItsOwnSession()
{

View file

@ -151,6 +151,7 @@ public sealed class HeadlessSessionIsolationTests
.Select(index => CreateHost(index, operations, time))
.ToArray();
var reconnectCounts = new int[sessionCount];
var deathCounts = new int[sessionCount];
var random = new Random(0xAC2013);
try
{
@ -197,6 +198,16 @@ public sealed class HeadlessSessionIsolationTests
2 + reconnectCounts[index]
+ (frame / 211))));
}
if (frame % 5003 == 0)
{
int index = (frame / 5003) % sessionCount;
Death(
operations.ActiveSession(
hosts[index].SessionId),
sharedPlayerGuid,
frame);
deathCounts[index]++;
}
if (frame % 353 == 0)
{
int index = random.Next(sessionCount);
@ -289,12 +300,14 @@ public sealed class HeadlessSessionIsolationTests
Assert.All(
reconnectCounts,
static count => Assert.True(count > 0));
Assert.All(
deathCounts,
static count => Assert.True(count > 0));
var finalRecords = new List<RuntimeEntityRecord>(
sessionCount);
Assert.All(
hosts,
host =>
for (int index = 0; index < hosts.Length; index++)
{
HeadlessSessionHost host = hosts[index];
Assert.Equal(
(ulong)frameCount,
host.Runtime.Clock.FrameNumber);
@ -307,7 +320,10 @@ public sealed class HeadlessSessionIsolationTests
host.Runtime.TransitOwner
.CaptureOwnership()
.IsSessionIdle);
});
Assert.True(
host.Runtime.CommunicationOwner.View.Count
>= deathCounts[index]);
}
Assert.Equal(
sessionCount,
finalRecords
@ -420,6 +436,18 @@ public sealed class HeadlessSessionIsolationTests
ForcePositionSequence: 0));
}
private static void Death(
WorldSession session,
uint victimGuid,
int sequence) =>
EventDelegate<Action<PlayerKilled.Parsed>>(
session,
nameof(session.PlayerKilledReceived))(
new PlayerKilled.Parsed(
$"Headless death {sequence}.",
victimGuid,
0x5000FFFFu));
private static WorldSession.EntitySpawn Spawn(
uint guid,
float positionX)