using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Cysharp.Threading.Tasks; using NUnit.Framework; using ShrinkContext; using ShrinkNetwork; using ShrinkEventBus; using UnityEngine.TestTools; public sealed class PlayerContracts { [Test] public void DotNetGoldenFrameIsIdenticalInPlayer() { var bytes = UnityEngine.Resources.Load("ShrinkV2Golden").bytes; var packet = ShrinkPacketCodec.Decode(bytes); var codec = new ShrinkJsonNetworkSerializer(); var message = (Message)codec.Deserialize(packet.Payload, typeof(Message)); Assert.That(message.Text, Is.EqualTo("跨宿主-v2")); using var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 42, Route = "跨宿主" }, codec, message); Assert.That(encoded.WrittenMemory.ToArray(), Is.EqualTo(bytes)); } [Test] public void BinaryEnvelopeAndLegacyRejection() { var codec = new ShrinkJsonNetworkSerializer(); foreach (var size in new[] { 64, 1024, 32768 }) { var message = new Message { Text = new string('x', size) }; using var frame = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 42, Route = "跨宿主" }, codec, message); var packet = ShrinkPacketCodec.Decode(frame.WrittenMemory); Assert.That(((Message)codec.Deserialize(packet.Payload, typeof(Message))).Text, Is.EqualTo(message.Text)); var copy = frame.WrittenMemory.ToArray(); copy[4] = 1; Assert.Throws(() => ShrinkPacketCodec.Decode(copy)); } } [UnityTest] public IEnumerator ContextTenThousandRetirements() => UniTask.ToCoroutine(async () => { var runtime = new ShrinkContextRuntime(16); var watch = Stopwatch.StartNew(); for (var i = 0; i < 10000; i++) await runtime.RetireAsync(runtime.Use(new Provider())); Assert.That(runtime.Fibers.Count, Is.Zero); Assert.That(runtime.CaptureDiagnostic().Fibers.Count, Is.EqualTo(16)); UnityEngine.Debug.Log("PLAYER Context 10000 retirements ms=" + watch.Elapsed.TotalMilliseconds); await runtime.ShutdownAsync(); }); [Test] public void EventDispatchAllocations() { using var binding = ShrinkGeneratedBinding.Subscribe(ShrinkGeneratedBinding.RuntimeResolver, null, null, null, _ => { }, ShrinkEventPriority.Normal, 0, false); for (var i = 0; i < 100; i++) EventBus.Post(new Event()); var watch = Stopwatch.StartNew(); for (var i = 0; i < 10000; i++) EventBus.Post(new Event()); watch.Stop(); var events=PlayerPerformance.AllocationEvents(() => EventBus.Post(new Event()),10000); UnityEngine.Debug.Log("PLAYER EventBus 10000 posts allocationEvents="+events+" ms="+watch.Elapsed.TotalMilliseconds); using var host=new ShrinkEventBusHost(); var local=host.CreateBus(ShrinkBusKey.Game,ShrinkBusOptions.Inline()); using var localBinding=ShrinkGeneratedBinding.Subscribe(host,null,null,null,_=>{},ShrinkEventPriority.Normal,0,false); for(var i=0;i<100;i++) local.Post(new Event()); var isolatedEvents=PlayerPerformance.AllocationEvents(()=>local.Post(new Event()),10000); UnityEngine.Debug.Log("PLAYER EventBus isolated 10000 posts allocationEvents="+isolatedEvents); Assert.That(isolatedEvents,Is.Zero); Assert.Throws(()=>local.Post(null)); } [UnityTest] public IEnumerator LoopbackRpc() => UniTask.ToCoroutine(async () => { var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b); var client = new ShrinkNetworkService(); var server = new ShrinkNetworkService(); foreach (var service in new[] { client, server }) { service.RegisterMessage(1); service.RegisterMessage(2); } server.RegisterRequestHandler((_, r) => UniTask.FromResult(new Response { Text = r.Text })); client.BindTransport(a); server.BindTransport(b); a.OpenSession(1); try { Assert.That((await client.CallAsync(client.Sessions[1], new Request { Text = "Unity Player" })).Text, Is.EqualTo("Unity Player")); } finally { a.Stop(); b.Stop(); } }); [UnityTest] public IEnumerator SavePhaseMatrix() => UniTask.ToCoroutine(async () => { var root = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ShrinkPlayerSave-" + Guid.NewGuid().ToString("N")); var storage = new ShrinkDataSaver.LocalStorageProvider(root); var initialize = typeof(ShrinkDataSaver.ShrinkSave).GetMethod("Initialize", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); initialize.Invoke(null, new object[] { storage, System.IO.Path.Combine(root, "saves"), ".sav", 1 }); try { foreach (var size in new[] { 100 * 1024, 1024 * 1024, 10 * 1024 * 1024 }) foreach (var background in new[] { false, true }) { var value = new string('s', size); var owner = System.Threading.Thread.CurrentThread.ManagedThreadId; var captureThread = -1; ShrinkDataSaver.ShrinkSave.RegisterModule("player-benchmark", () => { captureThread = System.Threading.Thread.CurrentThread.ManagedThreadId; return value; }, _ => { }); var options = new ShrinkDataSaver.SaveOptions { EncodeInBackground = background, CollectTimings = true }; await ShrinkDataSaver.ShrinkSave.SaveSlotAsync(0, options); Assert.That(captureThread, Is.EqualTo(owner)); Assert.That(options.LastTimings.EncodedBytes, Is.GreaterThan(size)); UnityEngine.Debug.Log("PLAYER Save " + size + " background=" + background + " " + "captureMs=" + options.LastTimings.CaptureMilliseconds + " encodeMs=" + options.LastTimings.EncodeMilliseconds + " encryptMs=" + options.LastTimings.EncryptMilliseconds + " writeMs=" + options.LastTimings.WriteMilliseconds + " bytes=" + options.LastTimings.EncodedBytes); } } finally { ShrinkDataSaver.ShrinkSave.UnregisterModule("player-benchmark"); if (System.IO.Directory.Exists(root)) System.IO.Directory.Delete(root, true); } }); public sealed class Message : IShrinkNetworkMessage { public string Text { get; set; } } public sealed class Request : IShrinkNetworkRequest { public string Text { get; set; } } public sealed class Response : ShrinkRpcResponseBase { public string Text { get; set; } } public readonly struct Event : IShrinkEvent { } public sealed class ReferenceEvent : IShrinkEvent { } private sealed class Provider : IShrinkComponent { public string Name => "test"; public IReadOnlyList Inject => Array.Empty(); public IReadOnlyList Provide => new[] { "value" }; public UniTask ApplyAsync(ShrinkCtx ctx, object config) { ctx.Set("value", this); return UniTask.CompletedTask; } } }