544 lines
22 KiB
C#
544 lines
22 KiB
C#
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<InvalidOperationException>().WithMessage("*expired*");
|
|
|
|
var wrongEpoch = Request(descriptor, now);
|
|
wrongEpoch.ProducerEpoch++;
|
|
Action epoch = () => adapter.OpenReadOnly(descriptor, wrongEpoch);
|
|
epoch.Should().Throw<InvalidOperationException>().WithMessage("*epoch*");
|
|
|
|
var writable = Request(descriptor, now);
|
|
writable.AccessMode = CultMeshBodyAccessMode.ReadWrite;
|
|
Action write = () => adapter.OpenReadOnly(descriptor, writable);
|
|
write.Should().Throw<UnauthorizedAccessException>();
|
|
}
|
|
|
|
[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<UnauthorizedAccessException>();
|
|
}
|
|
|
|
[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<InvalidDataException>();
|
|
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<NotSupportedException>();
|
|
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<InvalidOperationException>();
|
|
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<InvalidDataException>().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<UnauthorizedAccessException>();
|
|
}
|
|
|
|
[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<CultMeshLiveBody>));
|
|
}
|
|
|
|
[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.RecordKeys.Should().Equal(
|
|
"eve:view:pilot",
|
|
CultMeshBodyPublicationDocument.CreateLatestRecordKey("eve:body:pilot").Value,
|
|
"eve:field:gravity");
|
|
subscription.SchemaIds.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;
|
|
}
|
|
}
|