using System; using System.Collections.Generic; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; namespace ShrinkEventBus.Tests { [TestFixture] public class ShrinkEventBusDeferredDiagnosticsTests { private IShrinkEventBusMainThreadDispatcher _originalDispatcher; [SetUp] public void SetUp() { _originalDispatcher = ShrinkEventBusRuntime.MainThreadDispatcher; } [TearDown] public void TearDown() { ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(_originalDispatcher); } [Test] public void ObserveDiagnostics_ReportsMetadataAndUnsubscribes() { var diagnostics = new List(); using var host = new ShrinkEventBusHost(); var key = new ShrinkBusKey("diagnostics", Guid.NewGuid().ToString("N")); var bus = host.CreateBus(key, ShrinkBusOptions.Inline()); var subscription = EventBus.ObserveDiagnostics(diagnostics.Add); bus.Post(new DeferredEvent(1)); subscription.Dispose(); bus.Post(new DeferredEvent(2)); Assert.AreEqual(1, diagnostics.Count); Assert.AreEqual(typeof(DeferredEvent), diagnostics[0].EventType); Assert.AreEqual(key, diagnostics[0].BusKey); Assert.IsTrue(diagnostics[0].Result.Accepted); Assert.IsFalse(diagnostics[0].IsAsync); Assert.GreaterOrEqual(diagnostics[0].ElapsedTimestampTicks, 0L); } [Test] public void ObserveDiagnostics_ThrowingObserverDoesNotBlockBusinessOrOtherObservers() { var calls = 0; var secondObserverCalls = 0; using var host = new ShrinkEventBusHost(); var key = new ShrinkBusKey("diagnostics", Guid.NewGuid().ToString("N")); var bus = host.CreateBus(key, ShrinkBusOptions.Inline()); using var binding = ShrinkGeneratedBinding.Subscribe(host, key, string.Empty, this, _ => calls++, ShrinkEventPriority.Normal, 0, false); using var first = EventBus.ObserveDiagnostics(_ => throw new InvalidOperationException("observer failure")); using var second = EventBus.ObserveDiagnostics(_ => secondObserverCalls++); var previous = LogAssert.ignoreFailingMessages; LogAssert.ignoreFailingMessages = true; try { Assert.DoesNotThrow(() => bus.Post(new DeferredEvent(3))); } finally { LogAssert.ignoreFailingMessages = previous; } Assert.AreEqual(1, calls); Assert.AreEqual(1, secondObserverCalls); } [Test] public void PostDeferred_DoesNotDispatchBeforePump() { var dispatcher = new ManualDispatcher(); ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher); using var host = new ShrinkEventBusHost(); var key = new ShrinkBusKey("deferred", Guid.NewGuid().ToString("N")); var bus = host.CreateBus(key, new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = 8 }); var calls = 0; using var binding = ShrinkGeneratedBinding.Subscribe(host, key, string.Empty, this, _ => calls++, ShrinkEventPriority.Normal, 0, false); var result = EventBus.PostDeferred(bus, new DeferredEvent(4)); Assert.IsTrue(result.Accepted); Assert.IsFalse(result.Handled); Assert.AreEqual(0, calls); Assert.AreEqual(1, dispatcher.Count); dispatcher.PumpAll(); Assert.AreEqual(1, calls); } [Test] public void PostDeferred_ReentrantPostWaitsForNextPump() { var dispatcher = new ManualDispatcher(); ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher); using var host = new ShrinkEventBusHost(); var key = new ShrinkBusKey("deferred", Guid.NewGuid().ToString("N")); var bus = host.CreateBus(key, new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = 8 }); var calls = 0; using var binding = ShrinkGeneratedBinding.Subscribe(host, key, string.Empty, this, _ => { calls++; if (calls == 1) EventBus.PostDeferred(bus, new DeferredEvent(5)); }, ShrinkEventPriority.Normal, 0, false); EventBus.PostDeferred(bus, new DeferredEvent(4)); dispatcher.PumpAll(); Assert.AreEqual(1, calls); Assert.AreEqual(1, dispatcher.Count); dispatcher.PumpAll(); Assert.AreEqual(2, calls); } [Test] public void PostDeferred_DropOldestKeepsNewest() { var dispatcher = new ManualDispatcher(); ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher); using var host = new ShrinkEventBusHost(); var key = new ShrinkBusKey("deferred", Guid.NewGuid().ToString("N")); var bus = host.CreateBus(key, new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = 1, OverflowPolicy = ShrinkQueueOverflowPolicy.DropOldest }); var values = new List(); using var binding = ShrinkGeneratedBinding.Subscribe(host, key, string.Empty, this, evt => values.Add(evt.Value), ShrinkEventPriority.Normal, 0, false); EventBus.PostDeferred(bus, new DeferredEvent(1)); EventBus.PostDeferred(bus, new DeferredEvent(2)); dispatcher.PumpAll(); CollectionAssert.AreEqual(new[] { 2 }, values); } [Test] public void PostDeferred_InlineBusFailsWithoutDispatch() { using var host = new ShrinkEventBusHost(); var key = new ShrinkBusKey("deferred", Guid.NewGuid().ToString("N")); var bus = host.CreateBus(key, ShrinkBusOptions.Inline()); var calls = 0; using var binding = ShrinkGeneratedBinding.Subscribe(host, key, string.Empty, this, _ => calls++, ShrinkEventPriority.Normal, 0, false); var result = EventBus.PostDeferred(bus, new DeferredEvent(6)); Assert.AreEqual(ShrinkPostFailure.DeferredUnsupported, result.Failure); Assert.AreEqual(0, calls); } [Test] public void PostDeferred_WithoutDeferredDispatcherFailsWithoutDispatch() { ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(new NonDeferredDispatcher()); using var host = new ShrinkEventBusHost(); var key = new ShrinkBusKey("deferred", Guid.NewGuid().ToString("N")); var bus = host.CreateBus(key, new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread }); var calls = 0; using var binding = ShrinkGeneratedBinding.Subscribe(host, key, string.Empty, this, _ => calls++, ShrinkEventPriority.Normal, 0, false); var result = EventBus.PostDeferred(bus, new DeferredEvent(7)); Assert.AreEqual(ShrinkPostFailure.DeferredUnsupported, result.Failure); Assert.AreEqual(0, calls); } [Test] public void PostDeferred_DisposedBusRejects() { var dispatcher = new ManualDispatcher(); ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher); using var host = new ShrinkEventBusHost(); var key = new ShrinkBusKey("deferred", Guid.NewGuid().ToString("N")); var bus = host.CreateBus(key, new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread }); bus.Dispose(); var result = EventBus.PostDeferred(bus, new DeferredEvent(8)); Assert.AreEqual(ShrinkPostFailure.BusStopped, result.Failure); } private readonly struct DeferredEvent : IShrinkEvent { public DeferredEvent(int value) => Value = value; public int Value { get; } } private sealed class ManualDispatcher : IShrinkEventBusMainThreadDispatcher, ShrinkSDK.Runtime.IShrinkDeferredMainThreadDispatcher { private readonly Queue _queue = new(); public bool IsMainThread => true; public int Count => _queue.Count; public bool TryPost(Action action) { _queue.Enqueue(action); return true; } public bool TryPostDeferred(Action action) { _queue.Enqueue(action); return true; } public void PumpAll() { var count = _queue.Count; for (var i = 0; i < count; i++) _queue.Dequeue()(); } } private sealed class NonDeferredDispatcher : IShrinkEventBusMainThreadDispatcher { public bool IsMainThread => true; public bool TryPost(Action action) => true; } } }