From 9942c7f0c804920c5afb055b23fa20708cd95b23 Mon Sep 17 00:00:00 2001 From: Alain van den Berg Date: Mon, 15 Apr 2024 21:29:11 +0200 Subject: [PATCH] Merge buffering with generic Debouncer --- Debounce/BufferedEventArgs.cs | 27 -- Debounce/Bufferer.cs | 124 ------- Debounce/DebouncedEventArgs.cs | 26 +- Debounce/Debouncer.cs | 88 ++++- Debounce/IBufferer.cs | 27 -- Debounce/IDebounceSettings.cs | 2 +- Debounce/IDebouncer.cs | 10 +- .../Components/Pages/Counter.razor | 2 +- Examples/Testable/TestableClass.cs | 3 +- .../TestableUnitTests/TestableClassTests.cs | 18 +- PerformanceTests/Program.cs | 5 +- UnitTests/BuffererTests.cs | 347 ------------------ UnitTests/DebouncedEventArgsTest.cs | 8 +- UnitTests/DebouncerTests.cs | 280 +++++++++++++- UnitTests/VerifyingHandlerWrapper.cs | 23 +- 15 files changed, 401 insertions(+), 589 deletions(-) delete mode 100644 Debounce/BufferedEventArgs.cs delete mode 100644 Debounce/Bufferer.cs delete mode 100644 Debounce/IBufferer.cs delete mode 100644 UnitTests/BuffererTests.cs diff --git a/Debounce/BufferedEventArgs.cs b/Debounce/BufferedEventArgs.cs deleted file mode 100644 index 7cc5d16..0000000 --- a/Debounce/BufferedEventArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Alain van den Berg -// -// SPDX-License-Identifier: MIT - -namespace Dorssel.Utilities; - -/// -/// Event arguments for the event. -/// -public class BufferedEventArgs : EventArgs -{ - /// - /// Initializes a new instance of the class. - /// - /// - /// The original data accumulated since the previous buffered event was sent. - /// - public BufferedEventArgs(IReadOnlyList bufferedData) - { - Buffer = bufferedData; - } - - /// - /// List of data accumulated in this buffered event. - /// - public IReadOnlyList Buffer { get; } -} diff --git a/Debounce/Bufferer.cs b/Debounce/Bufferer.cs deleted file mode 100644 index 97061d8..0000000 --- a/Debounce/Bufferer.cs +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Alain van den Berg -// -// SPDX-License-Identifier: MIT - -using System.Collections.ObjectModel; - -namespace Dorssel.Utilities; - -/// -/// A that buffers data and sends events with the accumulated data. -/// -/// Data to buffer per trigger -/// -/// This is not as performant as the due to allocations. -/// -public sealed class Bufferer : IDisposable, IDebouncer> -{ - IDebouncer debouncer; - List eventList = new(); - object eventListLock = new(); - - /// - /// Create Bufferer with the default . - /// -#pragma warning disable CA2000 // Dispose objects before losing scope - public Bufferer() : this(new Debouncer()) { } -#pragma warning restore CA2000 // Dispose objects before losing scope - - /// - /// Create Bufferer with a specific instance. - /// - /// The debouncer instance to use - /// If debouncer is null - public Bufferer(IDebouncer debouncer) - { - this.debouncer = debouncer ?? throw new ArgumentNullException(nameof(debouncer)); - debouncer.Debounced += Debouncer_Debounced; - } - - /// - /// Wrap the Debounced event and invoke the Buffered event instead. - /// - private void Debouncer_Debounced(object sender, DebouncedEventArgs e) - { - IReadOnlyList debouncedEvents; - lock (eventListLock) - { - debouncedEvents = new ReadOnlyCollection(eventList); - eventList = new(); - } - - Debounced?.Invoke(this, new BufferedEventArgs(debouncedEvents)); - } - - /// - public void Trigger(TData? data) - { - if (data is not null) - { - lock (eventListLock) - { - eventList.Add(data); - } - } - debouncer.Trigger(); - } - - /// - public long Reset() - { - int count; - lock (eventListLock) - { - count = eventList.Count; - eventList = new(); - } - debouncer.Reset(); - return count; - } - - /// - public TimeSpan DebounceWindow - { - get => debouncer.DebounceWindow; - set => debouncer.DebounceWindow = value; - } - - /// - public TimeSpan DebounceTimeout - { - get => debouncer.DebounceTimeout; - set => debouncer.DebounceTimeout = value; - } - - /// - public TimeSpan EventSpacing - { - get => debouncer.EventSpacing; - set => debouncer.EventSpacing = value; - } - - /// - public TimeSpan HandlerSpacing - { - get => debouncer.HandlerSpacing; - set => debouncer.HandlerSpacing = value; - } - - /// - public TimeSpan TimingGranularity - { - get => debouncer.TimingGranularity; - set => debouncer.TimingGranularity = value; - } - - /// - public event EventHandler>? Debounced; - - /// - public void Dispose() - { - ((IDisposable)debouncer).Dispose(); - } -} diff --git a/Debounce/DebouncedEventArgs.cs b/Debounce/DebouncedEventArgs.cs index 413e45e..01aeca5 100644 --- a/Debounce/DebouncedEventArgs.cs +++ b/Debounce/DebouncedEventArgs.cs @@ -5,31 +5,37 @@ namespace Dorssel.Utilities; /// -/// Provides data for the event. +/// Provides data for the event. /// -public class DebouncedEventArgs : EventArgs +public class DebouncedEventArgs : EventArgs { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The number of triggers accumulated since the previous event was sent. /// Must be greater than 0. /// + /// + /// Accumulated data from each individual trigger, or empty when buffering is disabled in the + /// /// Thrown when is not greater than 0. - public DebouncedEventArgs(long count) - : this(count, true) + public DebouncedEventArgs(long count, IReadOnlyList triggerData) + : this(count, true, triggerData) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The number of triggers accumulated since the previous event was sent. /// Must be greater than 0 if is true. /// /// If true, is checked to be within its valid range. + /// + /// Accumulated data from each individual trigger, or empty when buffering is disabled in the + /// /// Thrown when is true and is not greater than 0. - protected DebouncedEventArgs(long count, bool boundsCheck) + protected DebouncedEventArgs(long count, bool boundsCheck, IReadOnlyList triggerData) { if (boundsCheck) { @@ -40,6 +46,7 @@ protected DebouncedEventArgs(long count, bool boundsCheck) } Count = count; + TriggerData = triggerData; } /// @@ -49,4 +56,9 @@ protected DebouncedEventArgs(long count, bool boundsCheck) /// The value will always greater than 0. /// public long Count { get; } + + /// + /// List of data accumulated in this buffered event. + /// + public IReadOnlyList TriggerData { get; } } diff --git a/Debounce/Debouncer.cs b/Debounce/Debouncer.cs index 5dcd92d..c3b3f16 100644 --- a/Debounce/Debouncer.cs +++ b/Debounce/Debouncer.cs @@ -2,24 +2,49 @@ // // SPDX-License-Identifier: MIT +using System.Collections.ObjectModel; using System.Diagnostics; using System.Runtime.CompilerServices; namespace Dorssel.Utilities; /// -/// This class implements the interface. +/// Object which debounces events, i.e., accumulating multiple incoming events into one. /// -public sealed class Debouncer - : IDebouncer - , IDisposable +public sealed class Debouncer : Debouncer, IDebouncer { /// /// Initializes a new instance of the class. /// public Debouncer() + : base(false) + { + } +} + +/// +/// Object which debounces events, i.e., accumulating multiple incoming events into one with the possibility of +/// keeping track of the incoming trigger data. +/// +public class Debouncer : IDebouncer + , IDisposable +{ + /// + /// Initializes a new instance of the class with buffering enabled. + /// + public Debouncer() + : this(true) + { + } + + /// + /// Initializes a new instance of the class with enableBuffering option. + /// + /// Whether to buffer trigger data or not + protected Debouncer(bool enableBuffering) { Timer = new Timer(OnTimer, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + EnableBuffering = enableBuffering; } long InterlockedCountMinusOne = -1; @@ -53,6 +78,9 @@ internal BenchmarkCounters Benchmark readonly Timer Timer; bool TimerActive; bool SendingEvent; + bool EnableBuffering; + List TriggerData = new(); + object TriggerDataLock = new(); internal static long AddWithClamp(long left, long right) { @@ -137,6 +165,15 @@ void LockedReschedule() { // Sending event now, so accumulate all coalesced triggers. var count = AddWithClamp(Count, Interlocked.Exchange(ref InterlockedCountMinusOne, -1) + 1); + IReadOnlyList triggerData = []; + if (EnableBuffering) + { + lock (TriggerDataLock) + { + triggerData = new ReadOnlyCollection(TriggerData); + TriggerData = new(); + } + } FirstTrigger.Reset(); LastTrigger.Reset(); @@ -146,7 +183,7 @@ void LockedReschedule() // Must call handler asynchronously and outside the lock. Task.Run(() => { - Debounced?.Invoke(this, new DebouncedEventArgs((long)count)); + Debounced?.Invoke(this, new DebouncedEventArgs((long)count, triggerData)); lock (LockObject) { // Handler has finished. @@ -213,14 +250,18 @@ void LockedReschedule() #region IDebounce Support /// - public event EventHandler? Debounced; - - /// - public void Trigger(Void data) => Trigger(); + public event EventHandler>? Debounced; /// - public void Trigger() + public void Trigger(TData data = default!) { + if (EnableBuffering) + { + lock (TriggerDataLock) + { + TriggerData.Add(data); + } + } var newCountMinusOne = Interlocked.Increment(ref InterlockedCountMinusOne); if (newCountMinusOne > 0) { @@ -250,6 +291,10 @@ public long Reset() { lock (LockObject) { + lock (TriggerDataLock) + { + TriggerData = new(); + } if (!IsDisposed) { Count = AddWithClamp(Count, Interlocked.Exchange(ref InterlockedCountMinusOne, -1) + 1); @@ -356,13 +401,26 @@ void ThrowIfDisposed() /// public void Dispose() { - lock (LockObject) + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Perform object cleanup. + /// + /// Indicates whether the method call comes from a Dispose method (true) or from a finalizer (false) + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (!IsDisposed) + lock (LockObject) { - Count = AddWithClamp(Count, Interlocked.Exchange(ref InterlockedCountMinusOne, long.MinValue) + 1); - Timer.Dispose(); - IsDisposed = true; + if (!IsDisposed) + { + Count = AddWithClamp(Count, Interlocked.Exchange(ref InterlockedCountMinusOne, long.MinValue) + 1); + Timer.Dispose(); + IsDisposed = true; + } } } } diff --git a/Debounce/IBufferer.cs b/Debounce/IBufferer.cs deleted file mode 100644 index b84f965..0000000 --- a/Debounce/IBufferer.cs +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Alain van den Berg -// -// SPDX-License-Identifier: MIT - -namespace Dorssel.Utilities; - -/// -/// This interface specifies the public API for the class. -/// -public interface IBufferer : IDebounceSettings -{ - /// - /// This event will be sent when has been called one or more times and - /// the debounce timer times out and will contain all events that have been triggered. - /// - public event EventHandler> Buffered; - - /// Accumulates the data of one more trigger. - /// Data to be buffered. - /// The object has been disposed. - public void Trigger(TData data); - - /// Resets the accumulated data to an empty collection and cancels any ongoing buffering. - /// This method may be called even after has been called. - /// The number of data that had been accumulated since the last event handler was called. - public long Reset(); -} diff --git a/Debounce/IDebounceSettings.cs b/Debounce/IDebounceSettings.cs index c7c4d03..5ed73e1 100644 --- a/Debounce/IDebounceSettings.cs +++ b/Debounce/IDebounceSettings.cs @@ -5,7 +5,7 @@ namespace Dorssel.Utilities; /// -/// Debounce settings for and . +/// Settings for and . /// public interface IDebounceSettings { diff --git a/Debounce/IDebouncer.cs b/Debounce/IDebouncer.cs index e2182ba..252cd54 100644 --- a/Debounce/IDebouncer.cs +++ b/Debounce/IDebouncer.cs @@ -12,7 +12,7 @@ public struct Void { } /// /// This interface specifies the public API for the class. /// -public interface IDebouncer : IDebouncer +public interface IDebouncer : IDebouncer { } @@ -20,20 +20,18 @@ public interface IDebouncer : IDebouncer /// Interface for debouncers accumulating triggers and debouncing. /// /// Data to accumulate when triggering -/// Type of event arguments for the event -public interface IDebouncer : IDebounceSettings - where TEventArgs : EventArgs +public interface IDebouncer : IDebounceSettings { /// /// This event will be sent when has been called one or more times and /// the debounce timer times out. /// - public event EventHandler Debounced; + public event EventHandler> Debounced; /// Accumulates one more trigger. /// Data that accompanies the trigger, or null if no data. /// The object has been disposed. - public void Trigger(TData? data = default); + public void Trigger(TData data = default!); /// Resets the accumulated trigger count to 0 and cancels any ongoing debouncing. /// This method may be called even after has been called. diff --git a/Examples/BlazorServerPush/Components/Pages/Counter.razor b/Examples/BlazorServerPush/Components/Pages/Counter.razor index 1b72064..dd6b24a 100644 --- a/Examples/BlazorServerPush/Components/Pages/Counter.razor +++ b/Examples/BlazorServerPush/Components/Pages/Counter.razor @@ -36,7 +36,7 @@ Debouncer.Trigger(); } - void OnDebounced(object? sender, DebouncedEventArgs ev) + void OnDebounced(object? sender, DebouncedEventArgs ev) { // now it is time to render the new state InvokeAsync(() => StateHasChanged()).Wait(); diff --git a/Examples/Testable/TestableClass.cs b/Examples/Testable/TestableClass.cs index 566ec03..f04b5ce 100644 --- a/Examples/Testable/TestableClass.cs +++ b/Examples/Testable/TestableClass.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Dorssel.Utilities; +using Void = Dorssel.Utilities.Void; namespace Testable; @@ -15,7 +16,7 @@ public TestableClass(IDebouncer debounce) Debounce.Debounced += OnDebouncedEvents; } - void OnDebouncedEvents(object? sender, DebouncedEventArgs debouncedEventArgs) + void OnDebouncedEvents(object? sender, DebouncedEventArgs debouncedEventArgs) { if (sender == null) { diff --git a/Examples/TestableUnitTests/TestableClassTests.cs b/Examples/TestableUnitTests/TestableClassTests.cs index 289b8db..4ad3f17 100644 --- a/Examples/TestableUnitTests/TestableClassTests.cs +++ b/Examples/TestableUnitTests/TestableClassTests.cs @@ -2,9 +2,11 @@ // // SPDX-License-Identifier: MIT +using Void = Dorssel.Utilities.Void; + namespace TestableUnitTests; -sealed class MockDebouncedEventArgs(long count) : DebouncedEventArgs(count, false) +sealed class MockDebouncedEventArgs(long count) : DebouncedEventArgs(count, false, []) { } @@ -16,11 +18,11 @@ public class TestableClassTests public void ConstructorHappyFlow() { var debounce = new Mock(); - debounce.SetupAdd(m => m.Debounced += It.IsAny>()); + debounce.SetupAdd(m => m.Debounced += It.IsAny>>()); using var _ = new TestableClass(debounce.Object); - debounce.VerifyAdd(m => m.Debounced += It.IsAny>(), Times.Once()); + debounce.VerifyAdd(m => m.Debounced += It.IsAny>>(), Times.Once()); } [TestMethod] @@ -36,11 +38,11 @@ public void ConstructorThrowsOnNull() public void DisposeUnregisters() { var debounce = new Mock(); - debounce.SetupRemove(m => m.Debounced -= It.IsAny>()); + debounce.SetupRemove(m => m.Debounced -= It.IsAny>>()); using (new TestableClass(debounce.Object)) { } - debounce.VerifyRemove(m => m.Debounced -= It.IsAny>(), Times.Once()); + debounce.VerifyRemove(m => m.Debounced -= It.IsAny>>(), Times.Once()); } [TestMethod] @@ -82,7 +84,7 @@ public void HandlerAcceptsNullSender() using var _ = new TestableClass(debounce.Object); - debounce.Raise(m => m.Debounced += null, null!, new DebouncedEventArgs(1)); + debounce.Raise(m => m.Debounced += null, null!, new DebouncedEventArgs(1, [])); } [TestMethod] @@ -122,7 +124,7 @@ public void HandlerMaxCount() using var _ = new TestableClass(debounce.Object); - debounce.Raise(m => m.Debounced += null, debounce.Object, new DebouncedEventArgs(long.MaxValue)); + debounce.Raise(m => m.Debounced += null, debounce.Object, new DebouncedEventArgs(long.MaxValue, [])); } [TestMethod] @@ -132,6 +134,6 @@ public void HandlerHappyFlow() using var _ = new TestableClass(debounce.Object); - debounce.Raise(m => m.Debounced += null, debounce.Object, new DebouncedEventArgs(1)); + debounce.Raise(m => m.Debounced += null, debounce.Object, new DebouncedEventArgs(1, [])); } } diff --git a/PerformanceTests/Program.cs b/PerformanceTests/Program.cs index e4783d1..062fc52 100644 --- a/PerformanceTests/Program.cs +++ b/PerformanceTests/Program.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Dorssel.Utilities; +using Void = Dorssel.Utilities.Void; namespace PerformanceTests; @@ -133,7 +134,7 @@ static void Main() { Console.WriteLine("Handler speed"); using var debouncer = new Debouncer(); - void handler(object? s, DebouncedEventArgs e) + void handler(object? s, DebouncedEventArgs e) { // each handler triggers the next debouncer.Trigger(); @@ -155,7 +156,7 @@ void handler(object? s, DebouncedEventArgs e) DebounceWindow = TimeSpan.FromTicks(1), TimingGranularity = TimeSpan.FromTicks(1) }; - void handler(object? s, DebouncedEventArgs e) + void handler(object? s, DebouncedEventArgs e) { // each handler triggers the next debouncer.Trigger(); diff --git a/UnitTests/BuffererTests.cs b/UnitTests/BuffererTests.cs deleted file mode 100644 index f3ff0ad..0000000 --- a/UnitTests/BuffererTests.cs +++ /dev/null @@ -1,347 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Alain van den Berg -// -// SPDX-License-Identifier: MIT - -namespace UnitTests; - -[TestClass] -[TestCategory("Production")] -public sealed class BuffererTests : IDisposable -{ - static TimeSpan TimingUnits(double count) => TimeSpan.FromMilliseconds(50 * count); - - static void Sleep(double count) => Thread.Sleep(TimingUnits(count)); - - Bufferer debouncer; - List> buffersCaptured = new(); - - public BuffererTests() - { - debouncer = new Bufferer(); - debouncer.Debounced += Debouncer_Buffered; - } - - private void Debouncer_Buffered(object? sender, BufferedEventArgs e) - { - buffersCaptured.Add(e.Buffer); - } - - [TestCleanup] - public void Dispose() - { - debouncer.Debounced -= Debouncer_Buffered; - debouncer.Dispose(); - } - - #region Dispose - [TestMethod] - public void DisposeNoThrow() - { - debouncer.Dispose(); - } - - [TestMethod] - public void DisposeMultipleNoThrow() - { - debouncer.Dispose(); - debouncer.Dispose(); - } - #endregion - - #region Trigger - [TestMethod] - public void TriggerWithoutHandler() - { - debouncer.Trigger(1); - Sleep(1); - } - - [TestMethod] - public void TriggerSingle() - { - debouncer.Trigger(1); - Sleep(1); - Assert.AreEqual(1, buffersCaptured.Count); - Assert.IsTrue(buffersCaptured[0].SequenceEqual([1])); - } - - [TestMethod] - public void TriggerSingleDelay() - { - debouncer.DebounceWindow = TimingUnits(2); - debouncer.Trigger(1); - Sleep(1); - Assert.AreEqual(0, buffersCaptured.Count); - Sleep(2); - Assert.AreEqual(1, buffersCaptured.Count); - Assert.IsTrue(buffersCaptured[0].SequenceEqual([1])); - } - - [TestMethod] - public void TriggersWithTimeout() - { - debouncer.DebounceWindow = TimingUnits(2); - debouncer.DebounceTimeout = TimingUnits(4); - - for (var i = 0; i < 6; ++i) - { - debouncer.Trigger(i); - Sleep(1); - } - Assert.AreEqual(1, buffersCaptured.Count); - Assert.IsTrue(buffersCaptured.Last().SequenceEqual([0, 1, 2, 3]), $"Was: [{string.Join(",",buffersCaptured.Last())}]"); - - Sleep(2); - Assert.AreEqual(2, buffersCaptured.Count); - Assert.IsTrue(buffersCaptured.Last().SequenceEqual([4, 5]), $"Was: [{string.Join(",", buffersCaptured.Last())}]"); - } - - [TestMethod] - public void TriggerCoalescence() - { - debouncer.DebounceWindow = TimingUnits(1); - debouncer.TimingGranularity = TimingUnits(1); - List expectedEvents = new(); - for (var i = 0; i < 10; ++i) - { - debouncer.Trigger(i); - expectedEvents.Add(i); - } - Sleep(4); - - Assert.AreEqual(1, buffersCaptured.Count); - Assert.IsTrue(buffersCaptured.Last().SequenceEqual(expectedEvents), $"Was: [{string.Join(",", buffersCaptured.Last())}]"); - } - - [TestMethod] - public void TriggerDuringHandlerSpacing() - { - debouncer.HandlerSpacing = TimingUnits(3); - - debouncer.Trigger(1); - Sleep(1); - Assert.AreEqual(1, buffersCaptured.Count); - Assert.IsTrue(buffersCaptured.Last().SequenceEqual([1])); - debouncer.Trigger(2); - Sleep(1); - Assert.AreEqual(1, buffersCaptured.Count); - Sleep(2); - Assert.AreEqual(2, buffersCaptured.Count); - Assert.IsTrue(buffersCaptured.Last().SequenceEqual([2])); - } - #endregion - - #region Reset - [TestMethod] - public void ResetWhileIdle() - { - Assert.AreEqual(0L, debouncer.Reset()); - } - - [TestMethod] - public void ResetAfterDispose() - { - debouncer.Dispose(); - Assert.AreEqual(0L, debouncer.Reset()); - } - - [TestMethod] - public void ResetDuringDebounce() - { - debouncer.DebounceWindow = TimingUnits(1); - - debouncer.Trigger(1); - Assert.AreEqual(1L, debouncer.Reset()); - Sleep(2); - Assert.AreEqual(0L, debouncer.Reset()); - } - #endregion - - [TestMethod] - public void ConstructorWithNullDebouncer() - { - Assert.ThrowsException(() => new Bufferer(null!)); - } - - #region DebounceTimeout - [TestMethod] - public void DebounceTimeoutDefault() - { - Assert.AreEqual(Timeout.InfiniteTimeSpan, debouncer.DebounceTimeout); - } - - [TestMethod] - [DynamicData(nameof(TimeSpanData.NonNegative), typeof(TimeSpanData))] - [DynamicData(nameof(TimeSpanData.Infinite), typeof(TimeSpanData))] - public void DebounceTimeoutValid(TimeSpan debounceTimeout) - { - debouncer.DebounceTimeout = TimeSpanData.ArbitraryNonDefault; - - debouncer.DebounceTimeout = debounceTimeout; - Assert.AreEqual(debounceTimeout, debouncer.DebounceTimeout); - } - - [TestMethod] - [DynamicData(nameof(TimeSpanData.Negative), typeof(TimeSpanData))] - public void DebounceTimeoutInvalid(TimeSpan debounceTimeout) - { - debouncer.DebounceTimeout = TimeSpan.FromMilliseconds(1); - Assert.ThrowsException(() => debouncer.DebounceTimeout = debounceTimeout); - Assert.AreEqual(TimeSpan.FromMilliseconds(1), debouncer.DebounceTimeout); - } - - [TestMethod] - public void DebounceTimeoutUnchanged() - { - debouncer.DebounceTimeout = TimeSpan.FromMilliseconds(1); - Assert.AreEqual(TimeSpan.FromMilliseconds(1), debouncer.DebounceTimeout); - debouncer.DebounceTimeout = TimeSpan.FromMilliseconds(1); - Assert.AreEqual(TimeSpan.FromMilliseconds(1), debouncer.DebounceTimeout); - } - - [TestMethod] - public void DebounceTimeoutAfterDispose() - { - var debouncer = new Bufferer(); - debouncer.Dispose(); - Assert.ThrowsException(() => debouncer.DebounceTimeout = TimeSpan.Zero); - } - #endregion - - #region EventSpacing - [TestMethod] - public void EventSpacingDefault() - { - Assert.AreEqual(TimeSpan.Zero, debouncer.EventSpacing); - } - - [TestMethod] - [DynamicData(nameof(TimeSpanData.NonNegative), typeof(TimeSpanData))] - public void EventSpacingValid(TimeSpan eventSpacing) - { - debouncer.EventSpacing = TimeSpanData.ArbitraryNonDefault; - - debouncer.EventSpacing = eventSpacing; - Assert.AreEqual(eventSpacing, debouncer.EventSpacing); - } - - [TestMethod] - [DynamicData(nameof(TimeSpanData.Negative), typeof(TimeSpanData))] - [DynamicData(nameof(TimeSpanData.Infinite), typeof(TimeSpanData))] - public void EventSpacingInvalid(TimeSpan eventSpacing) - { - debouncer.EventSpacing = TimeSpan.FromMilliseconds(1); - Assert.ThrowsException(() => debouncer.EventSpacing = eventSpacing); - Assert.AreEqual(TimeSpan.FromMilliseconds(1), debouncer.EventSpacing); - } - - [TestMethod] - public void EventSpacingUnchanged() - { - debouncer.EventSpacing = TimeSpan.FromMilliseconds(1); - Assert.AreEqual(TimeSpan.FromMilliseconds(1), debouncer.EventSpacing); - debouncer.EventSpacing = TimeSpan.FromMilliseconds(1); - Assert.AreEqual(TimeSpan.FromMilliseconds(1), debouncer.EventSpacing); - } - - [TestMethod] - public void EventSpacingAfterDispose() - { - debouncer.Dispose(); - Assert.ThrowsException(() => debouncer.EventSpacing = TimeSpan.Zero); - } - #endregion - - #region HandlerSpacing - [TestMethod] - public void HandlerSpacingDefault() - { - Assert.AreEqual(TimeSpan.Zero, debouncer.HandlerSpacing); - } - - [TestMethod] - [DynamicData(nameof(TimeSpanData.NonNegative), typeof(TimeSpanData))] - public void HandlerSpacingValid(TimeSpan HandlerSpacing) - { - debouncer.HandlerSpacing = TimeSpanData.ArbitraryNonDefault; - - debouncer.HandlerSpacing = HandlerSpacing; - Assert.AreEqual(HandlerSpacing, debouncer.HandlerSpacing); - } - - [TestMethod] - [DynamicData(nameof(TimeSpanData.Negative), typeof(TimeSpanData))] - [DynamicData(nameof(TimeSpanData.Infinite), typeof(TimeSpanData))] - public void HandlerSpacingInvalid(TimeSpan HandlerSpacing) - { - debouncer.HandlerSpacing = TimeSpan.FromMilliseconds(1); - Assert.ThrowsException(() => debouncer.HandlerSpacing = HandlerSpacing); - Assert.AreEqual(TimeSpan.FromMilliseconds(1), debouncer.HandlerSpacing); - } - - [TestMethod] - public void HandlerSpacingUnchanged() - { - debouncer.HandlerSpacing = TimeSpan.FromMilliseconds(1); - Assert.AreEqual(TimeSpan.FromMilliseconds(1), debouncer.HandlerSpacing); - debouncer.HandlerSpacing = TimeSpan.FromMilliseconds(1); - Assert.AreEqual(TimeSpan.FromMilliseconds(1), debouncer.HandlerSpacing); - } - - [TestMethod] - public void HandlerSpacingAfterDispose() - { - debouncer.Dispose(); - Assert.ThrowsException(() => debouncer.HandlerSpacing = TimeSpan.Zero); - } - #endregion - - #region TimingGranularity - [TestMethod] - public void TimingGranularityDefault() - { - Assert.AreEqual(TimeSpan.Zero, debouncer.TimingGranularity); - } - - [TestMethod] - [DynamicData(nameof(TimeSpanData.NonNegative), typeof(TimeSpanData))] - public void TimingGranularityValid(TimeSpan timingGranularity) - { - debouncer.DebounceWindow = TimeSpan.MaxValue; - debouncer.TimingGranularity = TimeSpanData.ArbitraryNonDefault; - - debouncer.TimingGranularity = timingGranularity; - Assert.AreEqual(timingGranularity, debouncer.TimingGranularity); - } - - [TestMethod] - [DynamicData(nameof(TimeSpanData.Negative), typeof(TimeSpanData))] - [DynamicData(nameof(TimeSpanData.Infinite), typeof(TimeSpanData))] - public void TimingGranularityInvalid(TimeSpan timingGranularity) - { - debouncer.DebounceWindow = TimeSpan.MaxValue; - debouncer.TimingGranularity = TimeSpan.FromMilliseconds(2); - - Assert.ThrowsException(() => debouncer.TimingGranularity = timingGranularity); - Assert.AreEqual(TimeSpan.FromMilliseconds(2), debouncer.TimingGranularity); - } - - [TestMethod] - public void TimingGranularityUnchanged() - { - debouncer.DebounceWindow = TimeSpan.MaxValue; - debouncer.TimingGranularity = TimeSpan.FromMilliseconds(2); - - Assert.AreEqual(TimeSpan.FromMilliseconds(2), debouncer.TimingGranularity); - debouncer.TimingGranularity = TimeSpan.FromMilliseconds(2); - Assert.AreEqual(TimeSpan.FromMilliseconds(2), debouncer.TimingGranularity); - } - - [TestMethod] - public void TimingGranularityAfterDispose() - { - debouncer.Dispose(); - Assert.ThrowsException(() => debouncer.TimingGranularity = TimeSpan.Zero); - } - #endregion -} diff --git a/UnitTests/DebouncedEventArgsTest.cs b/UnitTests/DebouncedEventArgsTest.cs index 45a717d..52367f6 100644 --- a/UnitTests/DebouncedEventArgsTest.cs +++ b/UnitTests/DebouncedEventArgsTest.cs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: MIT +using Void = Dorssel.Utilities.Void; + namespace UnitTests; [TestClass] @@ -34,7 +36,7 @@ public static IEnumerable InvalidCounts [DynamicData(nameof(ValidCounts))] public void ConstructorCountValid(long count) { - var debouncedEventArgs = new DebouncedEventArgs(count); + var debouncedEventArgs = new DebouncedEventArgs(count, []); Assert.AreEqual(count, debouncedEventArgs.Count); } @@ -42,10 +44,10 @@ public void ConstructorCountValid(long count) [DynamicData(nameof(InvalidCounts))] public void ConstructorCountInvalid(long count) { - Assert.ThrowsException(() => _ = new DebouncedEventArgs(count)); + Assert.ThrowsException(() => _ = new DebouncedEventArgs(count, [])); } - sealed class DerivedDebouncedEventArgs(long count, bool boundsCheck) : DebouncedEventArgs(count, boundsCheck) + sealed class DerivedDebouncedEventArgs(long count, bool boundsCheck) : DebouncedEventArgs(count, boundsCheck, []) { } diff --git a/UnitTests/DebouncerTests.cs b/UnitTests/DebouncerTests.cs index ac6275b..95661eb 100644 --- a/UnitTests/DebouncerTests.cs +++ b/UnitTests/DebouncerTests.cs @@ -2,6 +2,9 @@ // // SPDX-License-Identifier: MIT +using System.Linq; +using Void = Dorssel.Utilities.Void; + namespace UnitTests; [TestClass] @@ -20,6 +23,14 @@ public void ConstructorDefault() _ = new Debouncer(); #pragma warning restore CA2000 // Dispose objects before losing scope } + + [TestMethod] + public void ConstructorDefaultGeneric() + { +#pragma warning disable CA2000 // Dispose objects before losing scope + _ = new Debouncer(); +#pragma warning restore CA2000 // Dispose objects before losing scope + } #endregion #region Dispose @@ -45,7 +56,7 @@ public void DisposeDuringTimer() { DebounceWindow = TimingUnits(2) }; - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); debouncer.Trigger(); Sleep(1); debouncer.Dispose(); @@ -56,7 +67,7 @@ public void DisposeDuringTimer() public void DisposeDuringHandler() { var debouncer = new Debouncer(); - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); using var done = new ManualResetEventSlim(); wrapper.Debounced += (s, e) => { @@ -75,7 +86,7 @@ public void DisposeDuringHandler() public void DisposeFromHandler() { using var debouncer = new Debouncer(); - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); using var done = new ManualResetEventSlim(); wrapper.Debounced += (s, e) => { @@ -366,7 +377,7 @@ public void TimingGranularityAfterDispose() [TestMethod] public void EventHandlerAcceptsDebouncedEventArgs() { - static void Handler(object? sender, DebouncedEventArgs debouncedEventArgs) { } + static void Handler(object? sender, DebouncedEventArgs debouncedEventArgs) { } using var debouncer = new Debouncer(); debouncer.Debounced += Handler; @@ -396,11 +407,26 @@ public void TriggerAfterDispose() public void TriggerSingle() { using var debouncer = new Debouncer(); - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); debouncer.Trigger(); Sleep(1); Assert.AreEqual(1L, wrapper.TriggerCount); Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + } + + [TestMethod] + public void TriggerSingleGeneric() + { + using var debouncer = new Debouncer(); + using var wrapper = new VerifyingHandlerWrapper(debouncer); + debouncer.Trigger(1); + Sleep(1); + Assert.AreEqual(1L, wrapper.TriggerCount); + Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); } [TestMethod] @@ -410,14 +436,39 @@ public void TriggerSingleDelay() { DebounceWindow = TimingUnits(2) }; - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); debouncer.Trigger(); Sleep(1); Assert.AreEqual(0L, wrapper.TriggerCount); Assert.AreEqual(0L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + Sleep(2); + Assert.AreEqual(1L, wrapper.TriggerCount); + Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + } + + [TestMethod] + public void TriggerSingleDelayGeneric() + { + using var debouncer = new Debouncer() + { + DebounceWindow = TimingUnits(2) + }; + using var wrapper = new VerifyingHandlerWrapper(debouncer); + debouncer.Trigger(99); + Sleep(1); + Assert.AreEqual(0L, wrapper.TriggerCount); + Assert.AreEqual(0L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); Sleep(2); Assert.AreEqual(1L, wrapper.TriggerCount); Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([99]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([99]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); } [TestMethod] @@ -428,7 +479,7 @@ public void TriggersWithTimeout() DebounceWindow = TimingUnits(2), DebounceTimeout = TimingUnits(4) }; - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); for (var i = 0; i < 6; ++i) { debouncer.Trigger(); @@ -438,6 +489,30 @@ public void TriggersWithTimeout() Sleep(2); Assert.AreEqual(6L, wrapper.TriggerCount); Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + } + + [TestMethod] + public void TriggersWithTimeoutGeneric() + { + using var debouncer = new Debouncer() + { + DebounceWindow = TimingUnits(2), + DebounceTimeout = TimingUnits(4) + }; + using var wrapper = new VerifyingHandlerWrapper(debouncer); + for (var i = 0; i < 6; ++i) + { + debouncer.Trigger(i); + Sleep(1); + } + Assert.AreEqual(1L, wrapper.HandlerCount); + Sleep(2); + Assert.AreEqual(6L, wrapper.TriggerCount); + Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([0, 1, 2, 3, 4, 5]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([4, 5]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); } [TestMethod] @@ -448,7 +523,7 @@ public void TriggerCoalescence() DebounceWindow = TimingUnits(1), TimingGranularity = TimingUnits(1) }; - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); for (var i = 0; i < 10; ++i) { debouncer.Trigger(); @@ -456,13 +531,35 @@ public void TriggerCoalescence() Sleep(4); Assert.AreEqual(10L, wrapper.TriggerCount); Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + } + + [TestMethod] + public void TriggerCoalescenceGeneric() + { + using var debouncer = new Debouncer() + { + DebounceWindow = TimingUnits(1), + TimingGranularity = TimingUnits(1) + }; + using var wrapper = new VerifyingHandlerWrapper(debouncer); + for (var i = 0; i < 10; ++i) + { + debouncer.Trigger(i); + } + Sleep(4); + Assert.AreEqual(10L, wrapper.TriggerCount); + Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); } [TestMethod] public void TriggerFromHandler() { using var debouncer = new Debouncer(); - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); wrapper.Debounced += (s, e) => { @@ -475,6 +572,29 @@ public void TriggerFromHandler() Sleep(1); Assert.AreEqual(2L, wrapper.TriggerCount); Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + } + + [TestMethod] + public void TriggerFromHandlerGeneric() + { + using var debouncer = new Debouncer(); + using var wrapper = new VerifyingHandlerWrapper(debouncer); + + wrapper.Debounced += (s, e) => + { + if (wrapper.HandlerCount == 1) + { + debouncer.Trigger(99); + } + }; + debouncer.Trigger(1); + Sleep(1); + Assert.AreEqual(2L, wrapper.TriggerCount); + Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1, 99]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([99]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); } [TestMethod] @@ -484,18 +604,51 @@ public void TriggerDuringHandlerSpacing() { HandlerSpacing = TimingUnits(3) }; - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); debouncer.Trigger(); Sleep(1); Assert.AreEqual(1L, wrapper.TriggerCount); Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); debouncer.Trigger(); Sleep(1); Assert.AreEqual(1L, wrapper.TriggerCount); Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); Sleep(2); Assert.AreEqual(2L, wrapper.TriggerCount); Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + } + + [TestMethod] + public void TriggerDuringHandlerSpacingGeneric() + { + using var debouncer = new Debouncer() + { + HandlerSpacing = TimingUnits(3) + }; + using var wrapper = new VerifyingHandlerWrapper(debouncer); + debouncer.Trigger(1); + Sleep(1); + Assert.AreEqual(1L, wrapper.TriggerCount); + Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); + debouncer.Trigger(2); + Sleep(1); + Assert.AreEqual(1L, wrapper.TriggerCount); + Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); + Sleep(2); + Assert.AreEqual(2L, wrapper.TriggerCount); + Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1, 2]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([2]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); } [TestMethod] @@ -505,18 +658,51 @@ public void TriggerDuringEventSpacing() { EventSpacing = TimingUnits(3) }; - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); debouncer.Trigger(); Sleep(1); Assert.AreEqual(1L, wrapper.TriggerCount); Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); debouncer.Trigger(); Sleep(1); Assert.AreEqual(1L, wrapper.TriggerCount); Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + Sleep(2); + Assert.AreEqual(2L, wrapper.TriggerCount); + Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + } + + [TestMethod] + public void TriggerDuringEventSpacingGeneric() + { + using var debouncer = new Debouncer() + { + EventSpacing = TimingUnits(3) + }; + using var wrapper = new VerifyingHandlerWrapper(debouncer); + debouncer.Trigger(1); + Sleep(1); + Assert.AreEqual(1L, wrapper.TriggerCount); + Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); + debouncer.Trigger(2); + Sleep(1); + Assert.AreEqual(1L, wrapper.TriggerCount); + Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); Sleep(2); Assert.AreEqual(2L, wrapper.TriggerCount); Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1, 2]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([2]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); } [TestMethod] @@ -527,7 +713,7 @@ public void CoalesceDuringHandler() DebounceWindow = TimingUnits(1), TimingGranularity = TimingUnits(0.1), }; - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); wrapper.Debounced += (s, e) => Sleep(2); debouncer.Trigger(); Sleep(2); @@ -536,6 +722,29 @@ public void CoalesceDuringHandler() Sleep(5); Assert.AreEqual(3L, wrapper.TriggerCount); Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + } + + [TestMethod] + public void CoalesceDuringHandlerGeneric() + { + using var debouncer = new Debouncer() + { + DebounceWindow = TimingUnits(1), + TimingGranularity = TimingUnits(0.1), + }; + using var wrapper = new VerifyingHandlerWrapper(debouncer); + wrapper.Debounced += (s, e) => Sleep(2); + debouncer.Trigger(1); + Sleep(2); + debouncer.Trigger(2); + debouncer.Trigger(3); + Sleep(5); + Assert.AreEqual(3L, wrapper.TriggerCount); + Assert.AreEqual(2L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1, 2, 3]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([2, 3]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); } #endregion @@ -564,7 +773,7 @@ public void ResetDuringDebounce() { DebounceWindow = TimingUnits(1) }; - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); debouncer.Trigger(); Assert.AreEqual(1L, debouncer.Reset()); Sleep(2); @@ -572,11 +781,28 @@ public void ResetDuringDebounce() Assert.AreEqual(0L, wrapper.HandlerCount); } + [TestMethod] + public void ResetDuringDebounceGeneric() + { + using var debouncer = new Debouncer() + { + DebounceWindow = TimingUnits(1) + }; + using var wrapper = new VerifyingHandlerWrapper(debouncer); + debouncer.Trigger(1); + Assert.AreEqual(1L, debouncer.Reset()); + Sleep(2); + Assert.AreEqual(0L, wrapper.TriggerCount); + Assert.AreEqual(0L, wrapper.HandlerCount); + Assert.AreEqual(0L, wrapper.TriggerData.Count); + Assert.AreEqual(0L, wrapper.LastTriggerData.Count); + } + [TestMethod] public void ResetFromHandler() { using var debouncer = new Debouncer(); - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); wrapper.Debounced += (s, e) => { @@ -590,6 +816,30 @@ public void ResetFromHandler() Sleep(1); Assert.AreEqual(1L, wrapper.TriggerCount); Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.AreEqual(0, wrapper.TriggerData.Count); + Assert.AreEqual(0, wrapper.LastTriggerData.Count); + } + + [TestMethod] + public void ResetFromHandlerGeneric() + { + using var debouncer = new Debouncer(); + using var wrapper = new VerifyingHandlerWrapper(debouncer); + + wrapper.Debounced += (s, e) => + { + if (wrapper.HandlerCount == 1) + { + debouncer.Trigger(2); + Assert.AreEqual(1L, debouncer.Reset()); + } + }; + debouncer.Trigger(1); + Sleep(1); + Assert.AreEqual(1L, wrapper.TriggerCount); + Assert.AreEqual(1L, wrapper.HandlerCount); + Assert.IsTrue(wrapper.TriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.TriggerData)}]"); + Assert.IsTrue(wrapper.LastTriggerData.SequenceEqual([1]), $"Was: [{string.Join(",", wrapper.LastTriggerData)}]"); } #endregion @@ -601,7 +851,7 @@ public void TimingMaximum() DebounceWindow = TimeSpan.MaxValue, TimingGranularity = TimeSpan.MaxValue, }; - using var wrapper = new VerifyingHandlerWrapper(debouncer); + using var wrapper = new VerifyingHandlerWrapper(debouncer); debouncer.Trigger(); Sleep(1); Assert.AreEqual(0L, wrapper.HandlerCount); diff --git a/UnitTests/VerifyingHandlerWrapper.cs b/UnitTests/VerifyingHandlerWrapper.cs index a0c0673..03edc2b 100644 --- a/UnitTests/VerifyingHandlerWrapper.cs +++ b/UnitTests/VerifyingHandlerWrapper.cs @@ -4,20 +4,31 @@ namespace UnitTests; -sealed class VerifyingHandlerWrapper : IDisposable +sealed class VerifyingHandlerWrapper : IDisposable { - public VerifyingHandlerWrapper(IDebouncer debouncer) + public VerifyingHandlerWrapper(IDebouncer debouncer) { Debouncer = debouncer; Debouncer.Debounced += OnDebounced; } - public event EventHandler? Debounced; + public event EventHandler>? Debounced; public long HandlerCount { get; private set; } public long TriggerCount { get; private set; } - void OnDebounced(object? sender, DebouncedEventArgs debouncedEventArgs) + /// + /// TriggerData of the last Debounced call. + /// + public IReadOnlyList LastTriggerData { get; private set; } = []; + + private List triggerData = []; + /// + /// Concatenation of all TriggerData from all Debounced calls. + /// + public IReadOnlyList TriggerData { get => triggerData; } + + void OnDebounced(object? sender, DebouncedEventArgs debouncedEventArgs) { // sender *must* be the original debouncer object Assert.AreSame(Debouncer, sender); @@ -28,6 +39,8 @@ void OnDebounced(object? sender, DebouncedEventArgs debouncedEventArgs) ++HandlerCount; TriggerCount += debouncedEventArgs.Count; + LastTriggerData = debouncedEventArgs.TriggerData; + triggerData.AddRange(debouncedEventArgs.TriggerData); Debounced?.Invoke(this, debouncedEventArgs); @@ -35,7 +48,7 @@ void OnDebounced(object? sender, DebouncedEventArgs debouncedEventArgs) Assert.AreEqual(Interlocked.Decrement(ref ReentrancyCount), 0); } - readonly IDebouncer Debouncer; + readonly IDebouncer Debouncer; int ReentrancyCount; #region IDisposable Support