using System.Diagnostics; using XFESystem.Kernel; using Xunit; namespace XFESystem.Desktop.Platform.Tests; public sealed class NativePointerCaptureTests { private static void Drain() { NativePointerCapture.Prepare(); while (NativePointerCapture.TryRead(out _)) { } } private static void Observe(int x, int y, bool down, int wheel = 0, long ticks = 0) => NativePointerCapture.Observe(x, y, down, false, false, wheel, ticks); [Fact] public void SlowFramePreservesBothMouseMakeAndBreakBeforeTheNextDrain() { Drain(); NativePointerCapture.Prepare(); long now = NativePointerCapture.ReadTimestamp(); Observe(300, 200, true, ticks: now); Observe(300, 200, false, ticks: now + Stopwatch.Frequency * 80 / 1000); Assert.True(NativePointerCapture.TryRead(out var make)); Assert.True(make.LeftButton); Assert.False(make.LostReports); Assert.True(NativePointerCapture.TryRead(out var released)); Assert.False(released.LeftButton); Assert.True(released.TimestampTicks > make.TimestampTicks); Assert.Equal((300, 200), (make.X, make.Y)); Assert.False(NativePointerCapture.TryRead(out _)); } [Fact] public void MotionWheelAndZeroWheelPacketsRemainOrderedWithoutWheelOverwrite() { Drain(); Observe(100, 200, false); Observe(120, 240, false, 3); Observe(122, 245, false, 0); Observe(122, 245, false, -1); var actual = new List(); while (NativePointerCapture.TryRead(out var report)) actual.Add(report); Assert.Equal(new[] { 100, 120, 122, 122 }, actual.Select(r => r.X)); Assert.Equal(new[] { 0, 3, 0, -1 }, actual.Select(r => r.Wheel)); Assert.Equal(2, actual.Sum(r => r.Wheel)); } [Fact] public void MultipleButtonsAndEachTransitionArePreserved() { Drain(); NativePointerCapture.Observe(1, 2, true, true, false, 0, 11); NativePointerCapture.Observe(1, 2, false, true, true, 0, 12); Assert.True(NativePointerCapture.TryRead(out var first)); Assert.True(first.LeftButton); Assert.True(first.RightButton); Assert.False(first.MiddleButton); Assert.True(NativePointerCapture.TryRead(out var second)); Assert.False(second.LeftButton); Assert.True(second.RightButton); Assert.True(second.MiddleButton); } [Fact] public void OverflowDiscardsAmbiguousBatchAndReturnsLossWithoutFabricatingRelease() { Drain(); Observe(3, 4, true); for (int i = 0; i < NativePointerCapture.Capacity; i++) Observe(i, i, true, 1, i); Observe(5, 6, false); Assert.True(NativePointerCapture.TryRead(out var loss)); Assert.True(loss.LostReports); Assert.Equal(0, loss.Wheel); Assert.Equal(0, loss.TimestampTicks); Assert.False(NativePointerCapture.TryRead(out _)); Observe(7, 8, true, 0, 100); Observe(7, 8, false, 0, 101); Assert.True(NativePointerCapture.TryRead(out var make)); Assert.False(make.LostReports); Assert.True(make.LeftButton); Assert.True(NativePointerCapture.TryRead(out var released)); Assert.False(released.LostReports); Assert.False(released.LeftButton); } [Fact] public void RingWrapRetainsButtonsAndPacketTimestamps() { Drain(); for (int i = 0; i < NativePointerCapture.Capacity * 3; i++) { Observe(i, i + 1, (i & 1) == 0, i % 3 - 1, i); Assert.True(NativePointerCapture.TryRead(out var report)); Assert.Equal(i, report.X); Assert.Equal(i + 1, report.Y); Assert.Equal((i & 1) == 0, report.LeftButton); Assert.Equal(i, report.TimestampTicks); Assert.False(report.LostReports); } } [Fact] public void InitializedObserveAndDrainAllocateNoManagedObjects() { Drain(); NativePointerCapture.Prepare(); Observe(1, 1, false); NativePointerCapture.TryRead(out _); long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 1000; i++) { Observe(i, i, (i & 1) == 0); NativePointerCapture.TryRead(out _); } Assert.Equal(before, GC.GetAllocatedBytesForCurrentThread()); } [Fact] public void UnpreparedInterruptObservationAndTimestampDoNotInitializeOrAllocateTheRing() { Drain(); var field = typeof(NativePointerCapture).GetField("reports", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!; var buffer = field.GetValue(null); field.SetValue(null, null); long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 1000; i++) { Observe(i, i, true); _ = NativePointerCapture.ReadTimestamp(); NativePointerCapture.TryRead(out _); } long after = GC.GetAllocatedBytesForCurrentThread(); Assert.Equal(before, after); Assert.Null(field.GetValue(null)); Assert.Equal(0, NativePointerCapture.ReadTimestamp()); field.SetValue(null, buffer); } }