feat: integrate performance upgrade and agent inspection tooling
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using NUnit.Framework;
|
||||
using ShrinkCommand;
|
||||
using ShrinkCommand.Integration;
|
||||
using ShrinkNetwork;
|
||||
|
||||
public class BridgeRetentionTests
|
||||
{
|
||||
[Test]
|
||||
public void CommandBridgeDoesNotKeepRetiredNetworkServiceAlive()
|
||||
{
|
||||
var weak = Register(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
Assert.That(weak.IsAlive, Is.False);
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static WeakReference Register()
|
||||
{
|
||||
var service = new ShrinkNetworkService();
|
||||
ShrinkNetworkCommandBridge.RegisterService(service, new ShrinkCommandService());
|
||||
return new WeakReference(service);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using MessagePack;
|
||||
using NUnit.Framework;
|
||||
using ShrinkNetwork;
|
||||
using ShrinkNetwork.MessagePack;
|
||||
|
||||
[MessagePackObject]
|
||||
public partial class CodecMessage : IShrinkNetworkMessage
|
||||
{
|
||||
[Key(0)] public int Value { get; set; }
|
||||
[Key(1)] public string Text { get; set; } = "";
|
||||
}
|
||||
|
||||
[GeneratedMessagePackResolver]
|
||||
public partial class TestMessageResolver { }
|
||||
|
||||
public class NetworkCodecTests
|
||||
{
|
||||
[TestCase(false)] [TestCase(true)]
|
||||
public void EnvelopeRoundTripsPayloadWithoutEncodingItAgain(bool messagePack)
|
||||
{
|
||||
IShrinkNetworkBufferSerializer serializer;
|
||||
if (messagePack) { var mp = new ShrinkMessagePackNetworkSerializer(TestMessageResolver.Instance); mp.Register<CodecMessage>(); serializer = mp; }
|
||||
else serializer = new ShrinkJsonNetworkSerializer();
|
||||
var counting = new CountingSerializer(serializer);
|
||||
var message = new CodecMessage { Value = 42, Text = "跨宿主消息" };
|
||||
using var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 123, Route = "中文/route", SessionToken = "secret", RequestToken = new(9) }, counting, message);
|
||||
var packet = ShrinkPacketCodec.Decode(encoded.WrittenMemory);
|
||||
var decoded = (CodecMessage)serializer.Deserialize(packet.Payload, typeof(CodecMessage));
|
||||
Assert.That(counting.Count, Is.EqualTo(1));
|
||||
Assert.That(decoded.Text, Is.EqualTo(message.Text));
|
||||
Assert.That(decoded.Value, Is.EqualTo(42));
|
||||
Assert.That(packet.Route, Is.EqualTo("中文/route"));
|
||||
Assert.That(packet.SessionToken, Is.EqualTo("secret"));
|
||||
Assert.That(packet.RequestToken.Value, Is.EqualTo(9));
|
||||
Assert.That(encoded.WrittenCount, Is.EqualTo(ShrinkPacketCodec.HeaderSize + System.Text.Encoding.UTF8.GetByteCount(packet.Route!) + 6 + packet.Payload.Length));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RejectsOldVersionMalformedLengthsAndTrailingData()
|
||||
{
|
||||
Assert.Throws<ShrinkProtocolException>(() => ShrinkPacketCodec.Decode(System.Text.Encoding.UTF8.GetBytes("{\"ProtocolVersion\":1}")));
|
||||
using var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Payload = new byte[] { 1, 2, 3 } });
|
||||
var bytes = encoded.WrittenMemory.ToArray();
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bytes.AsSpan(4), 1);
|
||||
Assert.Throws<ShrinkProtocolException>(() => ShrinkPacketCodec.Decode(bytes));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bytes.AsSpan(4), 2);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(29), int.MaxValue);
|
||||
Assert.Throws<ShrinkProtocolException>(() => ShrinkPacketCodec.Decode(bytes));
|
||||
Assert.Throws<ShrinkProtocolException>(() => ShrinkPacketCodec.Decode(encoded.WrittenMemory[..^1]));
|
||||
Assert.Throws<ShrinkProtocolException>(() => ShrinkPacketCodec.Decode(encoded.WrittenMemory.ToArray().Concat(new byte[1]).ToArray()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InvalidUtf8HeaderIsAProtocolRejection()
|
||||
{
|
||||
using var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Route = "r", SessionToken = "t" });
|
||||
foreach (var offset in new[] { ShrinkPacketCodec.HeaderSize, ShrinkPacketCodec.HeaderSize + 1 })
|
||||
{
|
||||
var bytes = encoded.WrittenMemory.ToArray(); bytes[offset] = 0xff;
|
||||
Assert.Throws<ShrinkProtocolException>(() => ShrinkPacketCodec.Decode(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BorrowedDecodeAndWriterDisposeHaveExplicitLifetimes()
|
||||
{
|
||||
var writer = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Payload = new byte[] { 7 } });
|
||||
var packet = ShrinkPacketCodec.Decode(writer.WrittenMemory);
|
||||
Assert.That(packet.Payload.Span[0], Is.EqualTo(7));
|
||||
writer.Dispose(); writer.Dispose();
|
||||
Assert.Throws<ObjectDisposedException>(() => writer.GetMemory());
|
||||
}
|
||||
|
||||
[TestCase(false)] [TestCase(true)]
|
||||
public async Task ServiceKeepsBufferAliveUntilTransportCompletes(bool fail)
|
||||
{
|
||||
var transport = new DeferredTransport();
|
||||
var service = new ShrinkNetworkService();
|
||||
service.RegisterMessage<CodecMessage>(1);
|
||||
service.BindTransport(transport);
|
||||
transport.Connect();
|
||||
var sending = service.SendAsync(service.Sessions[1], new CodecMessage { Text = "alive" }).AsTask();
|
||||
Assert.That(sending.IsCompleted, Is.False);
|
||||
Assert.That(((CodecMessage)new ShrinkJsonNetworkSerializer().Deserialize(ShrinkPacketCodec.Decode(transport.Memory).Payload, typeof(CodecMessage))).Text, Is.EqualTo("alive"));
|
||||
if (fail) transport.Completion.TrySetException(new IOException("disconnect")); else transport.Completion.TrySetResult();
|
||||
if (fail) Assert.ThrowsAsync<IOException>(async () => await sending); else await sending;
|
||||
Assert.That(transport.Memory.Span.ToArray(), Is.All.EqualTo(0), "pooled buffer is cleared and released after completion/failure");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueuedLoopbackReceiverOwnsPacketAfterSenderReturns()
|
||||
{
|
||||
using var queue = new ShrinkNetworkDispatchQueue(4);
|
||||
var client = new ShrinkNetworkService();
|
||||
var server = new ShrinkNetworkService { DispatchScheduler = queue };
|
||||
client.RegisterMessage<CodecMessage>(1); server.RegisterMessage<CodecMessage>(1);
|
||||
var received = "";
|
||||
server.RegisterHandler<CodecMessage>((ctx, message) => { received = message.Text; return UniTask.CompletedTask; });
|
||||
var left = new ShrinkLoopbackTransport(); var right = new ShrinkLoopbackTransport(); left.LinkPeer(right);
|
||||
client.BindTransport(left); server.BindTransport(right); left.OpenSession(1);
|
||||
await client.SendAsync(client.Sessions[1], new CodecMessage { Text = "retained" });
|
||||
Assert.That(received, Is.Empty);
|
||||
await queue.PumpAsync(4);
|
||||
Assert.That(received, Is.EqualTo("retained"));
|
||||
}
|
||||
|
||||
private sealed class CountingSerializer(IShrinkNetworkBufferSerializer inner) : IShrinkNetworkBufferSerializer
|
||||
{
|
||||
public int Count;
|
||||
public void Serialize(IBufferWriter<byte> writer, object value) { Count++; inner.Serialize(writer, value); }
|
||||
public object Deserialize(ReadOnlyMemory<byte> payload, Type type) => inner.Deserialize(payload, type);
|
||||
public byte[] Serialize(object value) => throw new AssertionException("Legacy allocation path used");
|
||||
public object Deserialize(byte[] payload, Type type) => inner.Deserialize(payload, type);
|
||||
public T Deserialize<T>(byte[] payload) => inner.Deserialize<T>(payload);
|
||||
}
|
||||
private sealed class DeferredTransport : IShrinkNetworkMemoryTransport
|
||||
{
|
||||
public bool IsStarted { get; private set; }
|
||||
public event Action<ShrinkNetworkTransportEvent>? OnEvent;
|
||||
public UniTaskCompletionSource Completion = new();
|
||||
public ReadOnlyMemory<byte> Memory;
|
||||
public void Start() => IsStarted = true;
|
||||
public void Stop() => IsStarted = false;
|
||||
public void Connect() => OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(1, "test"));
|
||||
public void Send(long id, byte[] data) => throw new AssertionException("Legacy transport used");
|
||||
public UniTask SendAsync(long id, byte[] data) => throw new AssertionException("Legacy transport used");
|
||||
public UniTask SendAsync(long id, ReadOnlyMemory<byte> data, CancellationToken ct = default) { Memory = data; return Completion.Task; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using ShrinkNetwork;
|
||||
|
||||
public class NetworkQueueTests
|
||||
{
|
||||
[Test]
|
||||
public async Task StateReplacementIsScopedToSessionAndMovesAfterReliableWork()
|
||||
{
|
||||
using var queue = new ShrinkNetworkWorkQueue(10, 100);
|
||||
var output = new List<int>();
|
||||
UniTask Write(int i) { output.Add(i); return UniTask.CompletedTask; }
|
||||
var old = queue.EnqueueAsync(1, "position", 10, () => Write(1), "player");
|
||||
var reliable = queue.EnqueueAsync(1, "position", 10, () => Write(2));
|
||||
var other = queue.EnqueueAsync(2, "position", 10, () => Write(3), "player");
|
||||
var latest = queue.EnqueueAsync(1, "position", 20, () => Write(4), "player");
|
||||
Assert.That(await old, Is.EqualTo(ShrinkNetworkQueueResult.Replaced));
|
||||
Assert.That(queue.CaptureDiagnostics().PendingBytes, Is.EqualTo(40));
|
||||
Assert.That(await queue.PumpAsync(10), Is.EqualTo(3));
|
||||
Assert.That(output, Is.EqualTo(new[] { 2, 3, 4 }));
|
||||
foreach (var result in new[] { await reliable, await other, await latest }) Assert.That(result, Is.EqualTo(ShrinkNetworkQueueResult.Completed));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CapacityRejectionDoesNotLoseExistingReliableWorkOrState()
|
||||
{
|
||||
using var queue = new ShrinkNetworkWorkQueue(2, 20, 1);
|
||||
var a = queue.EnqueueAsync(1, "a", 10, () => UniTask.CompletedTask, "x");
|
||||
Assert.That(await queue.EnqueueAsync(1, "a", 1, () => UniTask.CompletedTask), Is.EqualTo(ShrinkNetworkQueueResult.Rejected));
|
||||
Assert.That(await queue.EnqueueAsync(1, "a", 21, () => UniTask.CompletedTask, "x"), Is.EqualTo(ShrinkNetworkQueueResult.Rejected));
|
||||
await queue.PumpAsync(1);
|
||||
Assert.That(await a, Is.EqualTo(ShrinkNetworkQueueResult.Completed));
|
||||
Assert.That(queue.CaptureDiagnostics().PendingBytes, Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BudgetDoesNotPreemptHandlerAndConcurrentPumpsDoNotRunHandlersInParallel()
|
||||
{
|
||||
using var queue = new ShrinkNetworkWorkQueue(4, 100);
|
||||
var completion = new UniTaskCompletionSource();
|
||||
var first = queue.EnqueueAsync(1, "a", 10, () => completion.Task);
|
||||
var second = queue.EnqueueAsync(1, "a", 10, () => UniTask.CompletedTask);
|
||||
var pump = queue.PumpAsync(4, 100, TimeSpan.FromMilliseconds(1)).AsTask();
|
||||
await Task.Delay(10);
|
||||
Assert.That(pump.IsCompleted, Is.False);
|
||||
Assert.That(await queue.PumpAsync(4), Is.Zero);
|
||||
completion.TrySetResult();
|
||||
Assert.That(await pump, Is.EqualTo(1));
|
||||
Assert.That(await first, Is.EqualTo(ShrinkNetworkQueueResult.Completed));
|
||||
queue.Dispose();
|
||||
Assert.That(await second, Is.EqualTo(ShrinkNetworkQueueResult.Canceled));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ByteBudgetMakesProgressForLargePacketsAndHandlerFailureDoesNotLeakQueue()
|
||||
{
|
||||
using var queue = new ShrinkNetworkWorkQueue(4, 100);
|
||||
var failed = queue.EnqueueAsync(1, "a", 30, () => UniTask.FromException(new IOException("handler"))).AsTask();
|
||||
var next = queue.EnqueueAsync(1, "a", 10, () => UniTask.CompletedTask);
|
||||
Assert.That(await queue.PumpAsync(4, 1), Is.EqualTo(1));
|
||||
Assert.ThrowsAsync<IOException>(async () => await failed);
|
||||
Assert.That(await queue.PumpAsync(4), Is.EqualTo(1));
|
||||
Assert.That(await next, Is.EqualTo(ShrinkNetworkQueueResult.Completed));
|
||||
Assert.That(queue.CaptureDiagnostics().PendingCount, Is.Zero);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using ShrinkNetwork;
|
||||
|
||||
[NonParallelizable]
|
||||
public class NetworkTransportTests
|
||||
{
|
||||
[Test]
|
||||
public async Task TcpRpcRoundTripAndCanceledSendPreserveNextFrame()
|
||||
{
|
||||
var reservation = new TcpListener(IPAddress.Loopback, 0); reservation.Start();
|
||||
var port = ((IPEndPoint)reservation.LocalEndpoint).Port; reservation.Stop();
|
||||
var serverTransport = new ShrinkTcpServerTransport(IPAddress.Loopback, port);
|
||||
var clientTransport = new ShrinkTcpClientTransport("127.0.0.1", port);
|
||||
var server = Service(); var client = Service();
|
||||
var connected = new TaskCompletionSource<ShrinkNetworkSession>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
client.OnSessionConnected += session => connected.TrySetResult(session);
|
||||
server.RegisterRequestHandler<Request, Response>((_, request) => UniTask.FromResult(new Response { Text = request.Text }));
|
||||
try
|
||||
{
|
||||
server.BindTransport(serverTransport); client.BindTransport(clientTransport);
|
||||
var session = await connected.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
using var cancellation = new CancellationTokenSource(); cancellation.Cancel();
|
||||
Assert.CatchAsync<OperationCanceledException>(async () => await clientTransport.SendAsync(1, new ReadOnlyMemory<byte>(new byte[100]), cancellation.Token));
|
||||
var reply = await client.CallAsync<Request, Response>(session, new Request { Text = new string('t', 32768) }).AsTask().WaitAsync(TimeSpan.FromSeconds(5));
|
||||
Assert.That(reply.Text.Length, Is.EqualTo(32768));
|
||||
Assert.That(client.GetDiagnosticsSnapshot().RpcCompleted, Is.EqualTo(1));
|
||||
}
|
||||
finally { clientTransport.Stop(); serverTransport.Stop(); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NestedRpcResponseBypassesSerialHandlerQueue()
|
||||
{
|
||||
var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b);
|
||||
var client = Service(); var server = Service();
|
||||
client.RegisterRequestHandler<Request, Response>((_, req) => UniTask.FromResult(new Response { Text = "nested:" + req.Text }));
|
||||
server.RegisterRequestHandler<Request, Response>(async (ctx, req) => await server.CallAsync<Request, Response>(ctx.Session, req));
|
||||
client.BindTransport(a); server.BindTransport(b); a.OpenSession(1);
|
||||
var response = await client.CallAsync<Request, Response>(client.Sessions[1], new Request { Text = "ok" }).AsTask().WaitAsync(TimeSpan.FromSeconds(3));
|
||||
Assert.That(response.Text, Is.EqualTo("nested:ok"));
|
||||
a.Stop(); b.Stop();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CongestionDisconnectsReliablePeerAndDoesNotResurrectSession()
|
||||
{
|
||||
var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b);
|
||||
using var queue = new ShrinkNetworkDispatchQueue(1, byteCapacity: 1024);
|
||||
var client = Service(); var server = Service(); server.DispatchScheduler = queue;
|
||||
client.BindTransport(a); server.BindTransport(b); a.OpenSession(1);
|
||||
var session = client.Sessions[1];
|
||||
var rejected = 0; server.OnDispatchRejected += _ => rejected++;
|
||||
await client.SendAsync(session, new Request());
|
||||
await client.SendAsync(session, new Request());
|
||||
Assert.That(rejected, Is.EqualTo(1));
|
||||
Assert.That(server.Sessions, Is.Empty);
|
||||
await queue.PumpAsync(1);
|
||||
Assert.That(server.Sessions, Is.Empty);
|
||||
a.Stop(); b.Stop();
|
||||
}
|
||||
private static ShrinkNetworkService Service()
|
||||
{
|
||||
var service = new ShrinkNetworkService(); service.RegisterMessage<Request>(1); service.RegisterMessage<Response>(2); return service;
|
||||
}
|
||||
public sealed class Request : IShrinkNetworkRequest { public string Text { get; set; } = ""; }
|
||||
public sealed class Response : ShrinkRpcResponseBase { public string Text { get; set; } = ""; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="UniTask" Version="2.5.10" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Kcp-CSharp" Version="1.0.8" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.8" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkNetwork/Adapters~/MessagePack/Runtime/**/*.cs" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkRuntime.Abstractions/Runtime/**/*.cs" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkContext.Core/Runtime/**/*.cs" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkContext.Core/Tests/**/*.cs" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkNetwork/Runtime/**/*.cs" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkEventBus/Runtime/**/*.cs" Exclude="../../../Assets/Modules/ShrinkEventBus/Runtime/ShrinkMonoEventScope.cs" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkCommand/Runtime/**/*.cs" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkCommand.Integration.Network/ShrinkNetworkCommand*.cs" />
|
||||
<Compile Include="../../../Assets/ShrinkInspection/Runtime/**/*.cs" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkDataSaver/Runtime/*.cs" Exclude="../../../Assets/Modules/ShrinkDataSaver/Runtime/ShrinkDataSaverRuntime.cs;../../../Assets/Modules/ShrinkDataSaver/Runtime/ShrinkDataSaverSettings.cs;../../../Assets/Modules/ShrinkDataSaver/Runtime/ShrinkDataSaverBootstrap.cs;../../../Assets/Modules/ShrinkDataSaver/Runtime/ShrinkModuleRegistrar.cs" />
|
||||
<Compile Include="../../../Assets/Modules/ShrinkDataSaver/DotNet~/Runtime/*.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,79 @@
|
||||
using NUnit.Framework;
|
||||
using ShrinkDataSaver;
|
||||
using ShrinkSDK.Inspection;
|
||||
using ShrinkNetwork;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
[NonParallelizable]
|
||||
public class SaveSnapshotTests
|
||||
{
|
||||
private string _directory = null!;
|
||||
[SetUp] public void Setup()
|
||||
{
|
||||
_directory = Path.Combine(Path.GetTempPath(), "ShrinkSaveTests", Guid.NewGuid().ToString("N"));
|
||||
ShrinkSave.Initialize(new LocalStorageProvider(_directory), "saves", ".json", 1);
|
||||
}
|
||||
[TearDown] public void Cleanup()
|
||||
{
|
||||
ShrinkSave.UnregisterModule("test-snapshot");
|
||||
if (Directory.Exists(_directory)) Directory.Delete(_directory, true);
|
||||
}
|
||||
|
||||
[TestCase(false)] [TestCase(true)]
|
||||
public async Task CaptureRunsOnCallerAndWorkerEncodingPreservesFormat(bool encrypt)
|
||||
{
|
||||
var owner = Environment.CurrentManagedThreadId;
|
||||
var captured = 0;
|
||||
var value = new SaveDto { Value = "original" };
|
||||
ShrinkSave.RegisterModule("test-snapshot", () => { captured = Environment.CurrentManagedThreadId; return value; }, _ => { });
|
||||
var options = new SaveOptions { EncodeInBackground = true, CollectTimings = true, Encrypt = encrypt, EncryptionKey = "test-only" };
|
||||
var saving = ShrinkSave.SaveSlotAsync(0, options).AsTask();
|
||||
value.Value = "changed-after-capture";
|
||||
await saving;
|
||||
var file = Directory.GetFiles(Path.Combine(_directory, "saves"), "*.json").Single();
|
||||
var packet = DataSerializer.Deserialize<SavePacket>(await File.ReadAllBytesAsync(file));
|
||||
var text = encrypt ? System.Text.Encoding.UTF8.GetString(SaveEncryptor.Decrypt(Convert.FromBase64String(packet.EncryptedModules), "test-only")) : packet.Modules["test-snapshot"].ToString();
|
||||
Assert.That(captured, Is.EqualTo(owner));
|
||||
Assert.That(text, Does.Contain("original").And.Not.Contain("changed-after-capture"));
|
||||
Assert.That(options.LastTimings.EncodedBytes, Is.GreaterThan(0));
|
||||
Assert.That(options.LastTimings.CaptureMilliseconds, Is.GreaterThanOrEqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CancellationLeavesExistingFileAndBackupsRecoverCorruption()
|
||||
{
|
||||
var storage = new LocalStorageProvider(_directory);
|
||||
await storage.WriteAsync("slot.json", new byte[] { 1 });
|
||||
await storage.WriteAsync("slot.json", new byte[] { 2 });
|
||||
using var cancellation = new CancellationTokenSource(); cancellation.Cancel();
|
||||
Assert.CatchAsync<OperationCanceledException>(async () => await storage.WriteAsync("slot.json", new byte[] { 3 }, cancellation.Token));
|
||||
Assert.That(await storage.ReadAsync("slot.json"), Is.EqualTo(new byte[] { 2 }));
|
||||
Assert.That(await storage.ReadAsync("slot.json.bak1"), Is.EqualTo(new byte[] { 1 }));
|
||||
Assert.That(File.Exists(Path.Combine(_directory, "slot.json.tmp")), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CorruptPrimarySaveFallsBackToPreviousValidPacket()
|
||||
{
|
||||
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "previous" });
|
||||
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "latest" });
|
||||
var primary = Directory.GetFiles(Path.Combine(_directory, "saves"), "*.json").Single();
|
||||
await File.WriteAllTextAsync(primary, "corrupt");
|
||||
Assert.That((await ShrinkSave.GetMetaAsync(0)).SlotName, Is.EqualTo("previous"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RuntimeSnapshotExportsBindingsWithoutSessionSecrets()
|
||||
{
|
||||
var service = new ShrinkNetworkService(); service.RegisterMessage<CodecMessage>(42, "test/message");
|
||||
service.RegisterHandler<CodecMessage>(Receive);
|
||||
var snapshot = new ShrinkInspectionSnapshot("build-a");
|
||||
snapshot.AddNetwork("server", service);
|
||||
var json = snapshot.ToJson();
|
||||
Assert.That(snapshot.Relations.Any(r => r.From == "opcode:42" && r.To == "type:CodecMessage"));
|
||||
Assert.That(snapshot.Relations.Any(r => r.To == "M:SaveSnapshotTests.Receive(ShrinkNetwork.ShrinkNetworkContext,CodecMessage)"));
|
||||
Assert.That(json, Does.Not.Contain("Payload").And.Not.Contain("SessionToken"));
|
||||
}
|
||||
private static UniTask Receive(ShrinkNetworkContext ctx, CodecMessage message) => UniTask.CompletedTask;
|
||||
public sealed class SaveDto { public string Value { get; set; } = ""; }
|
||||
}
|
||||
Reference in New Issue
Block a user