using AcDream.UI.Abstractions.Input; using Silk.NET.Input; namespace AcDream.UI.Abstractions.Tests.Input; public sealed class InputDispatcherLifetimeTests { [Theory] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] [InlineData(5)] public void AttachFailureRollsBackEveryPossiblyAcquiredPrefix(int failAdd) { var source = new ThrowingSources { FailAdd = failAdd }; var dispatcher = InputDispatcher.CreateDetached( source, source, new KeyBindings()); Assert.ThrowsAny(dispatcher.Attach); Assert.True(dispatcher.IsDisposalComplete); Assert.Equal(failAdd, source.AddCalls); Assert.Equal(failAdd, source.RemoveCalls); Assert.Equal(0, source.LiveEdges); } [Fact] public void DeactivateSilencesCopiedSourceDelegateBeforePhysicalDetach() { var source = new ThrowingSources(); var bindings = new KeyBindings(); bindings.Add(new Binding( new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward)); var dispatcher = InputDispatcher.CreateDetached(source, source, bindings); int fired = 0; dispatcher.Fired += (_, _) => fired++; dispatcher.Attach(); Action copied = source.KeyDownDelegate!; copied(Key.W, ModifierMask.None); dispatcher.Deactivate(); copied(Key.W, ModifierMask.None); dispatcher.Dispose(); Assert.Equal(1, fired); Assert.True(dispatcher.IsDisposalComplete); } [Fact] public void SourceEventRaisedReentrantlyDuringAttachCannotEnterPartialDispatcher() { var source = new ThrowingSources { RaiseDuringKeyDownAdd = true }; var bindings = new KeyBindings(); bindings.Add(new Binding( new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward)); var dispatcher = InputDispatcher.CreateDetached(source, source, bindings); int fired = 0; dispatcher.Fired += (_, _) => fired++; dispatcher.Attach(); Assert.Equal(0, fired); source.KeyDownDelegate!(Key.W, ModifierMask.None); Assert.Equal(1, fired); } [Fact] public void FailedDetachRetainsOnlyPendingSourceForRetry() { var source = new ThrowingSources(); var dispatcher = InputDispatcher.CreateDetached( source, source, new KeyBindings()); dispatcher.Attach(); source.FailRemoveKeyDown = true; Assert.Throws(dispatcher.Dispose); int completedRemoves = source.RemoveCalls - source.KeyDownRemoveCalls; source.FailRemoveKeyDown = false; dispatcher.Dispose(); Assert.Equal(completedRemoves, source.RemoveCalls - source.KeyDownRemoveCalls); Assert.True(dispatcher.IsDisposalComplete); } [Fact] public void DisposeBeforeAttachMakesDispatcherTerminal() { var source = new ThrowingSources(); var dispatcher = InputDispatcher.CreateDetached( source, source, new KeyBindings()); dispatcher.Dispose(); Assert.Throws(dispatcher.Attach); dispatcher.Dispose(); Assert.Equal(0, source.AddCalls); Assert.Equal(0, source.LiveEdges); } private sealed class ThrowingSources : IKeyboardSource, IMouseSource { private Action? _keyDown; private Action? _keyUp; private Action? _mouseDown; private Action? _mouseUp; private Action? _scroll; public int FailAdd { get; init; } public bool FailRemoveKeyDown { get; set; } public bool RaiseDuringKeyDownAdd { get; init; } public int AddCalls { get; private set; } public int RemoveCalls { get; private set; } public int KeyDownRemoveCalls { get; private set; } public int LiveEdges { get; private set; } public Action? KeyDownDelegate => _keyDown; public event Action? KeyDown { add { Add(ref _keyDown, value!); if (RaiseDuringKeyDownAdd) value?.Invoke(Key.W, ModifierMask.None); } remove { KeyDownRemoveCalls++; if (FailRemoveKeyDown) throw new InvalidOperationException("remove key down"); Remove(ref _keyDown, value!); } } public event Action? KeyUp { add => Add(ref _keyUp, value!); remove => Remove(ref _keyUp, value!); } public event Action? MouseDown { add => Add(ref _mouseDown, value!); remove => Remove(ref _mouseDown, value!); } public event Action? MouseUp { add => Add(ref _mouseUp, value!); remove => Remove(ref _mouseUp, value!); } #pragma warning disable CS0067 public event Action? MouseMove; #pragma warning restore CS0067 public event Action? Scroll { add => Add(ref _scroll, value!); remove => Remove(ref _scroll, value!); } public ModifierMask CurrentModifiers => ModifierMask.None; public bool WantCaptureMouse => false; public bool WantCaptureKeyboard => false; public bool IsHeld(Key key) => false; public bool IsHeld(MouseButton button) => false; private void Add(ref T? slot, T value) where T : Delegate { AddCalls++; slot = (T?)Delegate.Combine(slot, value); LiveEdges++; if (AddCalls == FailAdd) throw new InvalidOperationException("add"); } private void Remove(ref T? slot, T value) where T : Delegate { RemoveCalls++; if (slot is null) return; slot = (T?)Delegate.Remove(slot, value); LiveEdges--; } } }