118 lines
4.4 KiB
C#
118 lines
4.4 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FluentAssertions;
|
|
using NUnit.Framework;
|
|
|
|
namespace GameCult.Mesh.Tests;
|
|
|
|
public sealed class CultMeshReliabilityDiagnosticsTests
|
|
{
|
|
[Test]
|
|
public void DiagnosticBuffer_IsBoundedAndCarriesServedVersion()
|
|
{
|
|
var clock = new TestClock(new DateTimeOffset(2026, 7, 12, 18, 0, 0, TimeSpan.Zero));
|
|
var diagnostics = new CultMeshDiagnosticBuffer(capacity: 2);
|
|
|
|
for (var sequence = 1; sequence <= 3; sequence++)
|
|
{
|
|
diagnostics.Emit(new CultMeshDiagnosticEvent(
|
|
sequence,
|
|
clock.UtcNow,
|
|
CultMeshReliabilityOrgan.Discovery,
|
|
CultMeshDiagnosticKind.DiscoveryObservation,
|
|
"lookup:aetheria",
|
|
"aetheria",
|
|
sequence == 3 ? "degraded" : "fresh",
|
|
schemaVersion: "gamecult.mesh.discovery_diagnostic.v1"));
|
|
clock.Advance(TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
var snapshot = diagnostics.Snapshot();
|
|
snapshot.Select(item => item.Sequence).Should().Equal(2, 3);
|
|
snapshot[^1].State.Should().Be("degraded");
|
|
snapshot[^1].LibraryVersion.Should().Be(CultMeshLibraryVersion.Current);
|
|
snapshot[^1].LibraryVersion.Should().NotBeNullOrWhiteSpace();
|
|
snapshot[^1].SchemaVersion.Should().Be("gamecult.mesh.discovery_diagnostic.v1");
|
|
}
|
|
|
|
[Test]
|
|
public void DeterministicNetwork_ReplaysLossDuplicationReorderingLatencyAndCorruption()
|
|
{
|
|
var clock = new TestClock(new DateTimeOffset(2026, 7, 12, 18, 0, 0, TimeSpan.Zero));
|
|
var network = new CultMeshDeterministicNetwork(
|
|
() => clock.UtcNow,
|
|
"rudp://first:3076",
|
|
new[]
|
|
{
|
|
new CultMeshFault(1, CultMeshFaultKind.Drop),
|
|
new CultMeshFault(2, CultMeshFaultKind.Duplicate),
|
|
new CultMeshFault(2, CultMeshFaultKind.Delay, TimeSpan.FromSeconds(2)),
|
|
new CultMeshFault(3, CultMeshFaultKind.Reorder),
|
|
new CultMeshFault(3, CultMeshFaultKind.Corrupt)
|
|
});
|
|
|
|
network.Send(Encoding.UTF8.GetBytes("lost"));
|
|
network.Send(Encoding.UTF8.GetBytes("twice"));
|
|
network.Send(new byte[] { 0x01, 0x02 });
|
|
|
|
var immediate = network.ReceiveAvailable();
|
|
immediate.Should().ContainSingle();
|
|
immediate[0].SendOrdinal.Should().Be(3);
|
|
immediate[0].Payload.Should().Equal(0xfe, 0x02);
|
|
|
|
clock.Advance(TimeSpan.FromSeconds(2));
|
|
var delayed = network.ReceiveAvailable();
|
|
delayed.Should().HaveCount(2);
|
|
delayed.Select(packet => Encoding.UTF8.GetString(packet.Payload)).Should().OnlyContain(value => value == "twice");
|
|
}
|
|
|
|
[Test]
|
|
public void DeterministicNetwork_ReplaysPartitionEndpointRotationAndRestart()
|
|
{
|
|
var clock = new TestClock(new DateTimeOffset(2026, 7, 12, 18, 0, 0, TimeSpan.Zero));
|
|
var network = new CultMeshDeterministicNetwork(
|
|
() => clock.UtcNow,
|
|
"rudp://first:3076",
|
|
new[]
|
|
{
|
|
new CultMeshFault(1, CultMeshFaultKind.Partition),
|
|
new CultMeshFault(2, CultMeshFaultKind.Partition),
|
|
new CultMeshFault(2, CultMeshFaultKind.RotateEndpoint, Endpoint: "rudp://second:3076"),
|
|
new CultMeshFault(3, CultMeshFaultKind.Restart)
|
|
});
|
|
|
|
network.Send(new byte[] { 1 });
|
|
network.Send(new byte[] { 2 });
|
|
network.ReceiveAvailable().Should().ContainSingle().Which.Endpoint.Should().Be("rudp://second:3076");
|
|
|
|
network.Send(new byte[] { 3 });
|
|
network.RestartCount.Should().Be(1);
|
|
network.Endpoint.Should().Be("rudp://second:3076");
|
|
network.ReceiveAvailable().Should().ContainSingle().Which.Payload.Should().Equal(3);
|
|
}
|
|
|
|
private sealed class TestClock : ICultMeshClock
|
|
{
|
|
public TestClock(DateTimeOffset utcNow)
|
|
{
|
|
UtcNow = utcNow;
|
|
}
|
|
|
|
public DateTimeOffset UtcNow { get; private set; }
|
|
|
|
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
Advance(delay);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public void Advance(TimeSpan duration)
|
|
{
|
|
UtcNow += duration;
|
|
}
|
|
}
|
|
}
|