using System; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using NUnit.Framework; using System.IO.MemoryMappedFiles; using GameCult.Networking; namespace GameCult.Mesh.Tests; public sealed class CultMeshBodyTransportTests { private string _root = null!; [SetUp] public void SetUp() { _root = Path.Combine(Path.GetTempPath(), "cultmesh-body-tests", Guid.NewGuid().ToString("N")); } [TearDown] public void TearDown() { if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true); } [Test] public void MappedBody_IsReadOnlyAndPreservesLogicalIdentityAcrossTransportMaterial() { var publisher = new CultMeshMappedBodyPublisher(_root, TimeSpan.FromMinutes(1)); var now = DateTimeOffset.UtcNow; var firstBytes = BitConverter.GetBytes(12.5f); var secondBytes = BitConverter.GetBytes(13.5f); var first = publisher.Publish("aetheria:pilot-entities", "eve.entity_soa.v1", 1, 128, 7, 40, firstBytes, now); var second = publisher.Publish("aetheria:pilot-entities", "eve.entity_soa.v1", 1, 128, 7, 41, secondBytes, now); first.BodyId.Should().Be(second.BodyId); first.CapabilityToken.Should().NotBe(second.CapabilityToken); first.AccessMode.Should().Be(CultMeshBodyAccessMode.ReadOnly); using var lease = new CultMeshMappedBodyAdapter(_root).OpenReadOnly(first, Request(first, now)); lease.ReadSingle(0).Should().Be(12.5f); lease.Descriptor.Sequence.Should().Be(40); } [Test] public void Validation_RejectsExpiredEpochLayoutAndWritableConsumption() { var publisher = new CultMeshMappedBodyPublisher(_root, TimeSpan.FromSeconds(1)); var now = DateTimeOffset.UtcNow; var descriptor = publisher.Publish("body", "schema", 2, 4, 9, 1, new byte[4], now); var adapter = new CultMeshMappedBodyAdapter(_root); Action expired = () => adapter.OpenReadOnly(descriptor, Request(descriptor, now.AddSeconds(2))); expired.Should().Throw().WithMessage("*expired*"); var wrongEpoch = Request(descriptor, now); wrongEpoch.ProducerEpoch++; Action epoch = () => adapter.OpenReadOnly(descriptor, wrongEpoch); epoch.Should().Throw().WithMessage("*epoch*"); var writable = Request(descriptor, now); writable.AccessMode = CultMeshBodyAccessMode.ReadWrite; Action write = () => adapter.OpenReadOnly(descriptor, writable); write.Should().Throw(); } [Test] public void CapabilityToken_CannotEscapeBrokerRoot() { var descriptor = new CultMeshBodyDescriptor { BodyId = "body", SchemaId = "schema", LayoutVersion = 1, ByteSize = 4, Capacity = 4, ProducerEpoch = 1, Sequence = 1, LeaseExpiresAtUnixMs = DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeMilliseconds(), TransportKind = CultMeshBodyTransportKind.SharedFileMapping, CapabilityToken = ".." + Path.DirectorySeparatorChar + "outside" }; Action open = () => new CultMeshMappedBodyAdapter(_root).OpenReadOnly( descriptor, Request(descriptor, DateTimeOffset.UtcNow)); open.Should().Throw(); } [Test] public void Negotiation_FallsBackToEquivalentNetworkBody_WhenLocalMappingFails() { var now = DateTimeOffset.UtcNow; var bytes = BitConverter.GetBytes(42.25f); var publisher = new CultMeshMappedBodyPublisher(_root, TimeSpan.FromMinutes(1)); var local = publisher.Publish("body", "schema", 1, bytes.Length, 3, 8, bytes, now); publisher.Revoke(local); var network = new CultMeshBodyDescriptor { BodyId = local.BodyId, SchemaId = local.SchemaId, LayoutVersion = local.LayoutVersion, ByteSize = local.ByteSize, Capacity = local.Capacity, ProducerEpoch = local.ProducerEpoch, Sequence = local.Sequence, AccessMode = CultMeshBodyAccessMode.ReadOnly, Synchronization = local.Synchronization, LeaseExpiresAtUnixMs = local.LeaseExpiresAtUnixMs, TransportKind = CultMeshBodyTransportKind.Network, SemanticHash = local.SemanticHash }; var service = new CultMeshBodyTransportService( new ICultMeshBodyTransportAdapter[] { new CultMeshMappedBodyAdapter(_root), new CultMeshNetworkBodyAdapter(_ => bytes) }, _ => true); var result = service.NegotiateReadOnly(local, network, Request(local, now)); using var lease = result.Lease; result.PreferredFailure.Should().BeOfType(); result.UsedFallback.Should().BeTrue(); result.PreferredTransport.Should().Be(CultMeshBodyTransportKind.SharedFileMapping); result.SelectedTransport.Should().Be(CultMeshBodyTransportKind.Network); lease.TransportKind.Should().Be(CultMeshBodyTransportKind.Network); lease.Descriptor.BodyId.Should().Be(local.BodyId); lease.ReadSingle(0).Should().Be(42.25f); } [Test] public void LocalAndNetworkRepresentationsExposeTheSameLogicalContractAndSemanticBytes() { var now = DateTimeOffset.UtcNow; var bytes = new byte[] { 1, 3, 5, 7, 9, 11 }; var local = new CultMeshMappedBodyPublisher(_root, TimeSpan.FromMinutes(1)) .Publish("body", "schema.v2", 2, 64, 5, 12, bytes, now); var network = NetworkRepresentation(local); var request = Request(local, now); using var localLease = new CultMeshMappedBodyAdapter(_root).OpenReadOnly(local, request); using var networkLease = new CultMeshNetworkBodyAdapter(_ => bytes).OpenReadOnly(network, request); networkLease.Descriptor.Should().BeEquivalentTo(localLease.Descriptor, options => options .Excluding(value => value.TransportKind) .Excluding(value => value.CapabilityToken)); ReadAll(localLease).Should().Equal(bytes); ReadAll(networkLease).Should().Equal(bytes); } [Test] public void MissingPreferredAdapterProducesObservableFallbackReason() { var now = DateTimeOffset.UtcNow; var bytes = new byte[] { 4, 8, 15, 16, 23, 42 }; var local = new CultMeshMappedBodyPublisher(_root, TimeSpan.FromMinutes(1)) .Publish("body", "schema", 1, bytes.Length, 3, 8, bytes, now); var network = NetworkRepresentation(local); var service = new CultMeshBodyTransportService( new ICultMeshBodyTransportAdapter[] { new CultMeshNetworkBodyAdapter(_ => bytes) }, _ => true); var result = service.NegotiateReadOnly(local, network, Request(local, now)); using var lease = result.Lease; result.UsedFallback.Should().BeTrue(); result.PreferredFailure.Should().BeOfType(); result.PreferredFailure!.Message.Should().Contain("adapter is available"); lease.TransportKind.Should().Be(CultMeshBodyTransportKind.Network); } [TestCase("schema")] [TestCase("epoch")] [TestCase("sequence")] public void NegotiationRejectsStaleLogicalGenerationBeforeFetchingNetworkBytes(string staleField) { var now = DateTimeOffset.UtcNow; var bytes = new byte[] { 2, 4, 6, 8 }; var local = new CultMeshMappedBodyPublisher(_root, TimeSpan.FromMinutes(1)) .Publish("body", "schema", 1, 4, 3, 8, bytes, now); var network = NetworkRepresentation(local); var request = Request(local, now); if (staleField == "schema") request.SchemaId = "stale"; if (staleField == "epoch") request.ProducerEpoch--; if (staleField == "sequence") request.Sequence--; var fetched = false; var service = new CultMeshBodyTransportService( new ICultMeshBodyTransportAdapter[] { new CultMeshMappedBodyAdapter(_root), new CultMeshNetworkBodyAdapter(_ => { fetched = true; return bytes; }) }, _ => true); Action open = () => service.NegotiateReadOnly(local, network, request); open.Should().Throw(); fetched.Should().BeFalse(); } [Test] public void NetworkRepresentationRejectsCorruptBytesBeforeCreatingAReadableLease() { var now = DateTimeOffset.UtcNow; var bytes = new byte[] { 10, 20, 30, 40 }; var local = new CultMeshMappedBodyPublisher(_root, TimeSpan.FromMinutes(1)) .Publish("body", "schema", 1, 4, 3, 8, bytes, now); var network = NetworkRepresentation(local); var corrupt = new byte[] { 10, 20, 30, 41 }; Action open = () => new CultMeshNetworkBodyAdapter(_ => corrupt) .OpenReadOnly(network, Request(network, now)); open.Should().Throw().WithMessage("*semantic digest*"); } [Test] public void Negotiation_RejectsUnauthorizedProducerBeforeOpeningAnyRepresentation() { var now = DateTimeOffset.UtcNow; var bytes = new byte[4]; var local = new CultMeshMappedBodyPublisher(_root, TimeSpan.FromMinutes(1)) .Publish("body", "schema", 1, 4, 3, 8, bytes, now); var network = new CultMeshBodyDescriptor { BodyId = local.BodyId, SchemaId = local.SchemaId, LayoutVersion = 1, ByteSize = 4, Capacity = 4, ProducerEpoch = 3, Sequence = 8, LeaseExpiresAtUnixMs = local.LeaseExpiresAtUnixMs, TransportKind = CultMeshBodyTransportKind.Network, SemanticHash = local.SemanticHash }; var service = new CultMeshBodyTransportService( new ICultMeshBodyTransportAdapter[] { new CultMeshMappedBodyAdapter(_root), new CultMeshNetworkBodyAdapter(_ => bytes) }, _ => false); Action open = () => service.OpenReadOnly(local, network, Request(local, now), out _); open.Should().Throw(); } [Test] [Platform("Win")] public void SharedMemoryAdapter_OpensOpaqueCapabilityReadOnly() { var token = "CultMesh.Body.Test." + Guid.NewGuid().ToString("N"); using var mapping = MemoryMappedFile.CreateNew(token, 8, MemoryMappedFileAccess.ReadWrite); using (var writer = mapping.CreateViewAccessor(0, 8, MemoryMappedFileAccess.Write)) writer.Write(0, 99L); var now = DateTimeOffset.UtcNow; var descriptor = new CultMeshBodyDescriptor { BodyId = "body", SchemaId = "schema", LayoutVersion = 1, ByteSize = 8, Capacity = 8, ProducerEpoch = 4, Sequence = 2, LeaseExpiresAtUnixMs = now.AddMinutes(1).ToUnixTimeMilliseconds(), TransportKind = CultMeshBodyTransportKind.SharedMemory, CapabilityToken = token }; using var lease = new CultMeshSharedMemoryBodyAdapter().OpenReadOnly(descriptor, Request(descriptor, now)); lease.ReadInt64(0).Should().Be(99); lease.TransportKind.Should().Be(CultMeshBodyTransportKind.SharedMemory); } [Test] public void FramePublisher_DirectWriteLeaseCommitsMappedBytesWithoutCompatibilityCopy() { using var publisher = new CultMeshFrameBodyPublisher( "aetheria:entities", "gamecult.eve.entity_soa.body.v2", layoutVersion: 2, capacity: 128, producerEpoch: 7, slotByteLength: 64, leaseDuration: TimeSpan.FromMinutes(1)); publisher.TryAcquireWrite(out var write).Should().BeTrue(); using (write) { BitConverter.TryWriteBytes(write.Span, 42.5f).Should().BeTrue(); var descriptor = write.Commit(sizeof(float), DateTimeOffset.UtcNow); using var read = new CultMeshSharedMemoryBodyAdapter().OpenReadOnly( descriptor, Request(descriptor, DateTimeOffset.UtcNow)); descriptor.Sequence.Should().Be(0); descriptor.SemanticHash.Should().NotBeNullOrWhiteSpace(); read.ReadSingle(0).Should().Be(42.5f); } } [Test] public void FramePublisher_AbandonedWriteDoesNotAdvanceGenerationOrHoldSlot() { using var publisher = new CultMeshFrameBodyPublisher( "aetheria:entities", "gamecult.eve.entity_soa.body.v2", layoutVersion: 2, capacity: 128, producerEpoch: 7, slotByteLength: 64); publisher.TryAcquireWrite(out var abandoned).Should().BeTrue(); abandoned.Dispose(); publisher.TryAcquireWrite(out var replacement).Should().BeTrue(); using (replacement) { replacement.Span[0] = 23; var descriptor = replacement.Commit(1, DateTimeOffset.UtcNow); descriptor.Sequence.Should().Be(0); } } [Test] public void BodyDemand_SelectsSharedMemoryLocallyAndNetworkRemotelyWithoutDuplicateFallbackWork() { using var tracker = new CultMeshBodyDemandTracker(); tracker.Plan("eve:entity-soa:aetheria.daemon:pilot").Generation.Should().Be(0); tracker.Observe(Demand("unity-local", "local", sameMachine: true)); tracker.Plan("eve:entity-soa:aetheria.daemon:pilot").Generation.Should().Be(1); tracker.Observe(Demand("unity-remote", "remote", sameMachine: false)); var mixed = tracker.Plan("eve:entity-soa:aetheria.daemon:pilot"); mixed.HasConsumers.Should().BeTrue(); mixed.RequiresSharedMemory.Should().BeTrue(); mixed.RequiresNetwork.Should().BeTrue(); mixed.Consumers.Should().ContainSingle(value => value.ConsumerRuntimeId == "unity-local" && value.Transport == CultMeshBodyTransportKind.SharedMemory); mixed.Consumers.Should().ContainSingle(value => value.ConsumerRuntimeId == "unity-remote" && value.Transport == CultMeshBodyTransportKind.Network); tracker.Observe(Demand("unity-remote", "remote", sameMachine: false, active: false)); var localOnly = tracker.Plan("eve:entity-soa:aetheria.daemon:pilot"); localOnly.Generation.Should().Be(3); localOnly.RequiresSharedMemory.Should().BeTrue(); localOnly.RequiresNetwork.Should().BeFalse(); tracker.Observe(Demand("unity-local", "local", sameMachine: true, active: false)); tracker.Plan("eve:entity-soa:aetheria.daemon:pilot").Generation.Should().Be(4); tracker.Observe(Demand("unity-local", "local", sameMachine: true)); tracker.Plan("eve:entity-soa:aetheria.daemon:pilot").Generation.Should().Be(5); } [Test] public async Task BodyDemand_GenerationGuardLinearizesCommitAgainstWithdrawal() { const string bodyId = "eve:entity-soa:aetheria.daemon:pilot"; using var tracker = new CultMeshBodyDemandTracker(); tracker.Observe(Demand("unity-local", "local", sameMachine: true)); var activeGeneration = tracker.Plan(bodyId).Generation; using var commitEntered = new ManualResetEventSlim(); using var releaseCommit = new ManualResetEventSlim(); var committed = false; var commit = Task.Run(() => tracker.TryExecuteAtGeneration( bodyId, activeGeneration, () => { commitEntered.Set(); releaseCommit.Wait(); committed = true; })); commitEntered.Wait(TimeSpan.FromSeconds(5)).Should().BeTrue(); var withdrawal = Task.Run(() => tracker.Observe(Demand("unity-local", "local", sameMachine: true, active: false))); await Task.Delay(50); withdrawal.IsCompleted.Should().BeFalse( "a withdrawal must serialize after an already-linearized synchronous commit"); releaseCommit.Set(); (await commit).Should().BeTrue(); await withdrawal; committed.Should().BeTrue(); tracker.Plan(bodyId).Generation.Should().BeGreaterThan(activeGeneration); var staleCallbackRan = false; tracker.TryExecuteAtGeneration(bodyId, activeGeneration, () => staleCallbackRan = true) .Should().BeFalse(); staleCallbackRan.Should().BeFalse( "work prepared for a withdrawn demand generation must be discarded before commit"); } [Test] public void BodyDemand_UnknownLocalityFailsTowardNetworkAndUnsupportedDemandCreatesNoRoute() { using var tracker = new CultMeshBodyDemandTracker(); tracker.Observe(Demand("unknown", "network", sameMachine: false)); tracker.Observe(new CultNetDatabaseSubscriptionDemand( "unsupported", "unsupported", new[] { "eve:entity-soa:aetheria.daemon:pilot" }, new[] { CultMeshBodyTransportKind.SharedMemory.ToString() }, sameMachine: false, active: true)); var plan = tracker.Plan("eve:entity-soa:aetheria.daemon:pilot"); plan.RequiresSharedMemory.Should().BeFalse(); plan.RequiresNetwork.Should().BeTrue(); plan.Consumers.Should().ContainSingle().Which.ConsumerRuntimeId.Should().Be("unknown"); } [Test] public void BodyDemand_UsesSharedFileMappingAsTheSameMachineFallback() { using var tracker = new CultMeshBodyDemandTracker(); tracker.Observe(new CultNetDatabaseSubscriptionDemand( "unity-local", "mapped-local", new[] { "eve:entity-soa:aetheria.daemon:pilot" }, new[] { CultMeshBodyTransportKind.SharedFileMapping.ToString(), CultMeshBodyTransportKind.Network.ToString() }, sameMachine: true, active: true)); var plan = tracker.Plan("eve:entity-soa:aetheria.daemon:pilot"); plan.RequiresSharedMemory.Should().BeFalse(); plan.RequiresSharedFileMapping.Should().BeTrue(); plan.RequiresNetwork.Should().BeFalse(); plan.Consumers.Should().ContainSingle().Which.Transport .Should().Be(CultMeshBodyTransportKind.SharedFileMapping); } [Test] public void LiveBodySubscriptionNamesOnlyTheReactiveDescriptorAndLogicalBodyDemand() { var subscription = new CultMeshLiveBodySubscription( "pilot-entities", "eve-unity", "eve:body:pilot"); subscription.SubscriptionId.Should().Be("pilot-entities"); subscription.ConsumerRuntimeId.Should().Be("eve-unity"); subscription.BodyId.Should().Be("eve:body:pilot"); subscription.PublicationRecordKey.Should().Be( CultMeshBodyPublicationDocument.CreateLatestRecordKey("eve:body:pilot").Value); typeof(CultMesh).GetMethod(nameof(CultMesh.SubscribeLiveBodyAsync))! .ReturnType.Should().Be(typeof(Task)); } [Test] public void HotBodySubscriptionKeepsViewPublicationAndDemandInOneExactContract() { var subscription = new CultMeshHotBodySubscription( "pilot-world", "eve-unity", "eve:view:pilot", "gamecult.eve.entity_soa_view.v2", "eve:body:pilot", new[] { "eve:field:gravity" }, new[] { "gamecult.eve.fields_splats.v1" }); subscription.Selection.Keys.Should().Equal( "eve:view:pilot", CultMeshBodyPublicationDocument.CreateLatestRecordKey("eve:body:pilot").Value, "eve:field:gravity"); subscription.Selection.Schemas.Should().Equal( "gamecult.eve.entity_soa_view.v2", CultMeshBodyPublicationSchemaVersions.Publication, "gamecult.eve.fields_splats.v1"); subscription.BodyId.Should().Be("eve:body:pilot"); subscription.ConsumerRuntimeId.Should().Be("eve-unity"); } [Test] public void HotBodySubscriptionsDefaultToEphemeralLiveDelivery() { var overloads = typeof(CultMesh).GetMethods() .Where(method => method.Name == nameof(CultMesh.SubscribeHotBodyAsync)) .ToArray(); overloads.Should().HaveCount(2); foreach (var overload in overloads) { var deliveryMode = overload.GetParameters().Single(parameter => parameter.ParameterType == typeof(CultNetDatabaseSubscriptionDeliveryMode)); deliveryMode.DefaultValue.Should().Be(CultNetDatabaseSubscriptionDeliveryMode.Live); } } private static CultMeshBodyValidationRequest Request(CultMeshBodyDescriptor descriptor, DateTimeOffset now) => new() { BodyId = descriptor.BodyId, SchemaId = descriptor.SchemaId, LayoutVersion = descriptor.LayoutVersion, ProducerEpoch = descriptor.ProducerEpoch, Sequence = descriptor.Sequence, Capacity = descriptor.Capacity, AccessMode = CultMeshBodyAccessMode.ReadOnly, NowUtc = now }; private static CultNetDatabaseSubscriptionDemand Demand( string runtimeId, string subscriptionId, bool sameMachine, bool active = true) => new( runtimeId, subscriptionId, new[] { "eve:entity-soa:aetheria.daemon:pilot" }, new[] { CultMeshBodyTransportKind.SharedMemory.ToString(), CultMeshBodyTransportKind.Network.ToString() }, sameMachine, active); private static CultMeshBodyDescriptor NetworkRepresentation(CultMeshBodyDescriptor descriptor) => new() { BodyId = descriptor.BodyId, SchemaId = descriptor.SchemaId, LayoutVersion = descriptor.LayoutVersion, ByteSize = descriptor.ByteSize, Capacity = descriptor.Capacity, ProducerEpoch = descriptor.ProducerEpoch, Sequence = descriptor.Sequence, AccessMode = descriptor.AccessMode, Synchronization = descriptor.Synchronization, LeaseExpiresAtUnixMs = descriptor.LeaseExpiresAtUnixMs, TransportKind = CultMeshBodyTransportKind.Network, SemanticHash = descriptor.SemanticHash }; private static byte[] ReadAll(ICultMeshBodyReadLease lease) { var bytes = new byte[lease.Descriptor.ByteSize]; lease.CopyTo(0, bytes, 0, bytes.Length); return bytes; } }