diff --git a/tests/AcDream.Headless.Tests/HeadlessProcessSchedulerTests.cs b/tests/AcDream.Headless.Tests/HeadlessProcessSchedulerTests.cs index 00f86c0d..a0a418d8 100644 --- a/tests/AcDream.Headless.Tests/HeadlessProcessSchedulerTests.cs +++ b/tests/AcDream.Headless.Tests/HeadlessProcessSchedulerTests.cs @@ -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() + .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() { diff --git a/tests/AcDream.Headless.Tests/HeadlessSessionIsolationTests.cs b/tests/AcDream.Headless.Tests/HeadlessSessionIsolationTests.cs index a920ddee..a8ea2aef 100644 --- a/tests/AcDream.Headless.Tests/HeadlessSessionIsolationTests.cs +++ b/tests/AcDream.Headless.Tests/HeadlessSessionIsolationTests.cs @@ -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,25 +300,30 @@ 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( sessionCount); - Assert.All( - hosts, - host => - { - Assert.Equal( - (ulong)frameCount, - host.Runtime.Clock.FrameNumber); - Assert.True( - host.Runtime.EntityObjects.Entities.TryGetActive( - sharedPlayerGuid, - out RuntimeEntityRecord finalRecord)); - finalRecords.Add(finalRecord); - Assert.True( - host.Runtime.TransitOwner - .CaptureOwnership() - .IsSessionIdle); - }); + for (int index = 0; index < hosts.Length; index++) + { + HeadlessSessionHost host = hosts[index]; + Assert.Equal( + (ulong)frameCount, + host.Runtime.Clock.FrameNumber); + Assert.True( + host.Runtime.EntityObjects.Entities.TryGetActive( + sharedPlayerGuid, + out RuntimeEntityRecord finalRecord)); + finalRecords.Add(finalRecord); + Assert.True( + 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>( + session, + nameof(session.PlayerKilledReceived))( + new PlayerKilled.Parsed( + $"Headless death {sequence}.", + victimGuid, + 0x5000FFFFu)); + private static WorldSession.EntitySpawn Spawn( uint guid, float positionX)