acdream/tests/AcDream.App.Tests/Input/SilkInputSourceLifetimeTests.cs
Erik 8b8afeefa3 refactor(input): own pointer and callback lifetime
Move camera pointer, framebuffer resize, and retained/devtools input edges behind focused reversible owners. Preserve input priority while making shutdown deactivate callbacks before live-session retirement and retry physical detach without stranding transport teardown.
2026-07-22 11:59:33 +02:00

284 lines
9.4 KiB
C#

using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Rendering;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
namespace AcDream.App.Tests.Input;
public sealed class SilkInputSourceLifetimeTests
{
[Theory]
[InlineData(1)]
[InlineData(2)]
public void KeyboardAttachFailureRollsBackEveryPossiblyAcquiredPrefix(int failAdd)
{
var surface = new KeyboardSurface { FailAdd = failAdd };
var source = SilkKeyboardSource.CreateDetached(surface, new HostQuiescenceGate());
Assert.ThrowsAny<Exception>(source.Attach);
Assert.True(source.IsDisposalComplete);
Assert.Equal(failAdd, surface.AddCalls);
Assert.Equal(failAdd, surface.RemoveCalls);
Assert.Equal(0, surface.LiveEdges);
}
[Fact]
public void KeyboardDeactivateSilencesCopiedDelegateBeforePhysicalDetach()
{
var surface = new KeyboardSurface();
var source = SilkKeyboardSource.CreateDetached(surface, new HostQuiescenceGate());
int calls = 0;
source.KeyDown += (_, _) => calls++;
source.Attach();
Action<Key> copied = surface.Down!;
surface.RaiseDown(Key.A);
source.Deactivate();
copied(Key.B);
source.Dispose();
Assert.Equal(1, calls);
Assert.True(source.IsDisposalComplete);
}
[Fact]
public void KeyboardEventRaisedReentrantlyDuringAttachCannotEnterPartialSource()
{
var surface = new KeyboardSurface { RaiseDuringAdd = true };
var source = SilkKeyboardSource.CreateDetached(surface, new HostQuiescenceGate());
int calls = 0;
source.KeyDown += (_, _) => calls++;
source.Attach();
Assert.Equal(0, calls);
surface.RaiseDown(Key.A);
Assert.Equal(1, calls);
}
[Fact]
public void KeyboardDetachRetriesOnlyPendingEdge()
{
var surface = new KeyboardSurface();
var source = SilkKeyboardSource.CreateDetached(surface, new HostQuiescenceGate());
source.Attach();
surface.FailRemoveDown = true;
Assert.Throws<AggregateException>(source.Dispose);
int upRemoves = surface.UpRemoveCalls;
surface.FailRemoveDown = false;
source.Dispose();
Assert.Equal(upRemoves, surface.UpRemoveCalls);
Assert.True(source.IsDisposalComplete);
}
[Fact]
public void KeyboardDisposeBeforeAttachMakesSourceTerminal()
{
var surface = new KeyboardSurface();
var source = SilkKeyboardSource.CreateDetached(surface, new HostQuiescenceGate());
source.Dispose();
Assert.Throws<ObjectDisposedException>(source.Attach);
source.Dispose();
Assert.Equal(0, surface.LiveEdges);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void MouseAttachFailureRollsBackEveryPossiblyAcquiredPrefix(int failAdd)
{
var surface = new MouseSurface { FailAdd = failAdd };
var source = SilkMouseSource.CreateDetached(
surface,
new Capture(),
modifierSource: null,
new HostQuiescenceGate());
Assert.ThrowsAny<Exception>(source.Attach);
Assert.True(source.IsDisposalComplete);
Assert.Equal(failAdd, surface.AddCalls);
Assert.Equal(failAdd, surface.RemoveCalls);
Assert.Equal(0, surface.LiveEdges);
}
[Fact]
public void MouseDeactivateSilencesAllCopiedDelegatesBeforePhysicalDetach()
{
var surface = new MouseSurface();
var source = SilkMouseSource.CreateDetached(
surface,
new Capture(),
modifierSource: null,
new HostQuiescenceGate());
int calls = 0;
source.MouseDown += (_, _) => calls++;
source.MouseUp += (_, _) => calls++;
source.MouseMove += (_, _) => calls++;
source.Scroll += _ => calls++;
source.Attach();
var copied = surface.CopyCallbacks();
surface.RaiseAll();
source.Deactivate();
copied.Down(MouseButton.Left);
copied.Up(MouseButton.Left);
copied.Move(new Vector2(5, 6));
copied.Scroll(1);
source.Dispose();
Assert.Equal(4, calls);
Assert.True(source.IsDisposalComplete);
}
[Fact]
public void MouseFailedDetachRetriesOnlyPendingEdge()
{
var surface = new MouseSurface();
var source = SilkMouseSource.CreateDetached(
surface,
new Capture(),
modifierSource: null,
new HostQuiescenceGate());
source.Attach();
surface.FailRemoveMoveOnce = true;
source.Dispose();
int removes = surface.RemoveCalls;
source.Dispose();
Assert.Equal(removes, surface.RemoveCalls);
Assert.Equal(2, surface.MoveRemoveCalls);
Assert.True(source.IsDisposalComplete);
}
[Fact]
public void MouseDisposeBeforeAttachMakesSourceTerminal()
{
var surface = new MouseSurface();
var source = SilkMouseSource.CreateDetached(
surface,
new Capture(),
modifierSource: null,
new HostQuiescenceGate());
source.Dispose();
Assert.Throws<ObjectDisposedException>(source.Attach);
source.Dispose();
Assert.Equal(0, surface.LiveEdges);
}
private sealed class Capture : IInputCaptureSource
{
public bool WantCaptureMouse => false;
public bool WantCaptureKeyboard => false;
public bool DevToolsWantCaptureKeyboard => false;
}
private sealed class KeyboardSurface : IKeyboardEventSurface
{
public int FailAdd { get; init; }
public bool FailRemoveDown { get; set; }
public bool RaiseDuringAdd { get; init; }
public int AddCalls { get; private set; }
public int RemoveCalls { get; private set; }
public int UpRemoveCalls { get; private set; }
public int LiveEdges { get; private set; }
private Action<Key>? _down;
private Action<Key>? _up;
public Action<Key>? Down => _down;
public void AddKeyDown(Action<Key> callback) => Add(ref _down, callback);
public void AddKeyUp(Action<Key> callback) => Add(ref _up, callback);
public void RemoveKeyDown(Action<Key> callback)
{
RemoveCalls++;
if (FailRemoveDown) throw new InvalidOperationException("remove down");
if (ReferenceEquals(_down, callback)) { _down = null; LiveEdges--; }
}
public void RemoveKeyUp(Action<Key> callback)
{
RemoveCalls++;
UpRemoveCalls++;
if (ReferenceEquals(_up, callback)) { _up = null; LiveEdges--; }
}
public bool IsKeyPressed(Key key) => false;
public void RaiseDown(Key key) => _down?.Invoke(key);
private void Add(ref Action<Key>? slot, Action<Key> callback)
{
AddCalls++;
slot = callback;
LiveEdges++;
if (RaiseDuringAdd) callback(Key.A);
if (AddCalls == FailAdd) throw new InvalidOperationException("add");
}
}
private sealed class MouseSurface : IMouseEventSurface
{
public int FailAdd { get; init; }
public bool FailRemoveMoveOnce { get; set; }
public int AddCalls { get; private set; }
public int RemoveCalls { get; private set; }
public int MoveRemoveCalls { get; private set; }
public int LiveEdges { get; private set; }
private Action<MouseButton>? _down;
private Action<MouseButton>? _up;
private Action<Vector2>? _move;
private Action<float>? _scroll;
public void AddMouseDown(Action<MouseButton> callback) => Add(ref _down, callback);
public void AddMouseUp(Action<MouseButton> callback) => Add(ref _up, callback);
public void AddMouseMove(Action<Vector2> callback) => Add(ref _move, callback);
public void AddScroll(Action<float> callback) => Add(ref _scroll, callback);
public void RemoveMouseDown(Action<MouseButton> callback) => Remove(ref _down, callback);
public void RemoveMouseUp(Action<MouseButton> callback) => Remove(ref _up, callback);
public void RemoveMouseMove(Action<Vector2> callback)
{
MoveRemoveCalls++;
if (FailRemoveMoveOnce)
{
FailRemoveMoveOnce = false;
throw new InvalidOperationException("remove move");
}
Remove(ref _move, callback);
}
public void RemoveScroll(Action<float> callback) => Remove(ref _scroll, callback);
public bool IsButtonPressed(MouseButton button) => false;
public (Action<MouseButton> Down, Action<MouseButton> Up, Action<Vector2> Move, Action<float> Scroll)
CopyCallbacks() => (_down!, _up!, _move!, _scroll!);
public void RaiseAll()
{
_down?.Invoke(MouseButton.Left);
_up?.Invoke(MouseButton.Left);
_move?.Invoke(Vector2.One);
_scroll?.Invoke(1);
}
private void Add<T>(ref Action<T>? slot, Action<T> callback)
{
AddCalls++;
slot = callback;
LiveEdges++;
if (AddCalls == FailAdd) throw new InvalidOperationException("add");
}
private void Remove<T>(ref Action<T>? slot, Action<T> callback)
{
RemoveCalls++;
if (ReferenceEquals(slot, callback)) { slot = null; LiveEdges--; }
}
}
}