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); 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] [Fact]
public void ThrowingPolicyQuarantinesOnlyItsOwnSession() public void ThrowingPolicyQuarantinesOnlyItsOwnSession()
{ {

View file

@ -151,6 +151,7 @@ public sealed class HeadlessSessionIsolationTests
.Select(index => CreateHost(index, operations, time)) .Select(index => CreateHost(index, operations, time))
.ToArray(); .ToArray();
var reconnectCounts = new int[sessionCount]; var reconnectCounts = new int[sessionCount];
var deathCounts = new int[sessionCount];
var random = new Random(0xAC2013); var random = new Random(0xAC2013);
try try
{ {
@ -197,6 +198,16 @@ public sealed class HeadlessSessionIsolationTests
2 + reconnectCounts[index] 2 + reconnectCounts[index]
+ (frame / 211)))); + (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) if (frame % 353 == 0)
{ {
int index = random.Next(sessionCount); int index = random.Next(sessionCount);
@ -289,25 +300,30 @@ public sealed class HeadlessSessionIsolationTests
Assert.All( Assert.All(
reconnectCounts, reconnectCounts,
static count => Assert.True(count > 0)); static count => Assert.True(count > 0));
Assert.All(
deathCounts,
static count => Assert.True(count > 0));
var finalRecords = new List<RuntimeEntityRecord>( var finalRecords = new List<RuntimeEntityRecord>(
sessionCount); sessionCount);
Assert.All( for (int index = 0; index < hosts.Length; index++)
hosts, {
host => HeadlessSessionHost host = hosts[index];
{ Assert.Equal(
Assert.Equal( (ulong)frameCount,
(ulong)frameCount, host.Runtime.Clock.FrameNumber);
host.Runtime.Clock.FrameNumber); Assert.True(
Assert.True( host.Runtime.EntityObjects.Entities.TryGetActive(
host.Runtime.EntityObjects.Entities.TryGetActive( sharedPlayerGuid,
sharedPlayerGuid, out RuntimeEntityRecord finalRecord));
out RuntimeEntityRecord finalRecord)); finalRecords.Add(finalRecord);
finalRecords.Add(finalRecord); Assert.True(
Assert.True( host.Runtime.TransitOwner
host.Runtime.TransitOwner .CaptureOwnership()
.CaptureOwnership() .IsSessionIdle);
.IsSessionIdle); Assert.True(
}); host.Runtime.CommunicationOwner.View.Count
>= deathCounts[index]);
}
Assert.Equal( Assert.Equal(
sessionCount, sessionCount,
finalRecords finalRecords
@ -420,6 +436,18 @@ public sealed class HeadlessSessionIsolationTests
ForcePositionSequence: 0)); 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( private static WorldSession.EntitySpawn Spawn(
uint guid, uint guid,
float positionX) float positionX)