From 23139f4522f49333b1ab5c54fe0d8a58ab81e236 Mon Sep 17 00:00:00 2001 From: beauchama Date: Wed, 10 Jun 2026 13:18:13 -0400 Subject: [PATCH 1/7] Added ProgressReporter --- src/KappaDuck.Quack/Progress/IProgressSink.cs | 22 ++++ .../Progress/ProgressReporter.cs | 79 +++++++++++ tests/Directory.Build.props | 3 + tests/Directory.Packages.props | 6 +- .../Progress/ProgressReporterTests.cs | 124 ++++++++++++++++++ 5 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 src/KappaDuck.Quack/Progress/IProgressSink.cs create mode 100644 src/KappaDuck.Quack/Progress/ProgressReporter.cs create mode 100644 tests/Unit.Tests/Progress/ProgressReporterTests.cs diff --git a/src/KappaDuck.Quack/Progress/IProgressSink.cs b/src/KappaDuck.Quack/Progress/IProgressSink.cs new file mode 100644 index 0000000..aa6cbb3 --- /dev/null +++ b/src/KappaDuck.Quack/Progress/IProgressSink.cs @@ -0,0 +1,22 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +namespace KappaDuck.Quack.Progress; + +/// +/// Represents a progress operation that can be reported or cancelled. +/// +public interface IProgressSink +{ + /// + /// Reports a normalized progress value between 0 and 1. + /// + /// The normalized value. Values outside the range are clamped by the sink. + void Report(float value); + + /// + /// Requests cancellation of the current progress operation. + /// + void Cancel(); + +} diff --git a/src/KappaDuck.Quack/Progress/ProgressReporter.cs b/src/KappaDuck.Quack/Progress/ProgressReporter.cs new file mode 100644 index 0000000..d89b2f2 --- /dev/null +++ b/src/KappaDuck.Quack/Progress/ProgressReporter.cs @@ -0,0 +1,79 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +namespace KappaDuck.Quack.Progress; + +/// +/// Synchronous determinate progress reporter handed to . +/// +public sealed class ProgressReporter +{ + private readonly IProgressSink _sink; + private readonly int _total; + + private bool _isCancelled; + private int _current; + + internal ProgressReporter(IProgressSink sink, int total) + { + _sink = sink; + _total = total; + } + + /// + /// Reports progress by incrementing by 1. + /// + /// + /// This is a shorthand for with steps of 1. + /// + public void Advance() => Increment(1); + + /// + /// Requests cancellation of the progress operation. + /// + /// + /// Stops any further reporting, triggers . + /// + public void Cancel() + { + if (_isCancelled) + return; + + _isCancelled = true; + _sink.Cancel(); + } + + /// + /// Reports progress by incrementing by a step. + /// + /// The number of steps to increment. + /// is negative. + public void Increment(int steps) + { + if (_isCancelled) + return; + + ArgumentOutOfRangeException.ThrowIfNegative(steps); + Report(_current + steps); + } + + /// + /// Reports the absolute current progress. + /// + /// + /// The total provided to is used as the maximum + /// limit if is greater than the total. + /// + /// The current progress. + /// is negative. + public void Report(int current) + { + if (_isCancelled) + return; + + ArgumentOutOfRangeException.ThrowIfNegative(current); + + _current = Math.Min(current, _total); + _sink.Report((float)_current / _total); + } +} diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index daeaa51..226a507 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -14,6 +14,7 @@ + @@ -22,6 +23,8 @@ + + diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index de069a0..f6c2293 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -1,7 +1,9 @@ - - + + + + diff --git a/tests/Unit.Tests/Progress/ProgressReporterTests.cs b/tests/Unit.Tests/Progress/ProgressReporterTests.cs new file mode 100644 index 0000000..58d124b --- /dev/null +++ b/tests/Unit.Tests/Progress/ProgressReporterTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Progress; + +namespace Unit.Tests.Progress; + +internal sealed class ProgressReporterTests +{ + private readonly Mock _sink = IProgressSink.Mock(); + private readonly ProgressReporter _reporter; + + public ProgressReporterTests() + { + _reporter = new ProgressReporter(_sink.Object, 100); + } + + [Test] + public void CancelShouldCancelTheProgressSink() + { + _reporter.Cancel(); + _sink.Cancel().WasCalled(Times.Once); + } + + [Test] + public void CancelShouldNotCancelTheProgressSinkTwiceWhenIsAlreadyCancelled() + { + _reporter.Cancel(); + _reporter.Cancel(); + + _sink.Cancel().WasCalled(Times.Once); + } + + [Test] + public void ReportShouldReportValueToProgressSink() + { + _reporter.Report(100); + _sink.Report(1).WasCalled(Times.Once); + } + + [Test] + public async Task ReportShouldThrowExceptionWhenValueIsNegative() + { + await Assert.That(() => _reporter.Report(-100)) + .ThrowsExactly(); + + _sink.Report(Any()).WasNeverCalled(); + } + + [Test] + public void ReportShouldClampToTheTotalWhenValueIsGreaterThanTotal() + { + _reporter.Report(999); + _sink.Report(1).WasCalled(Times.Once); + } + + [Test] + public void ReportShouldDoNothingWhenIsCancelled() + { + _reporter.Cancel(); + _reporter.Report(100); + + _sink.Report(Any()).WasNeverCalled(); + } + + [Test] + public void IncrementShouldIncrementByStep() + { + _reporter.Increment(25); + _sink.Report(0.25f).WasCalled(Times.Once); + } + + [Test] + public void IncrementShouldCumulateAndIncrementTheStepsWhenCallingMultipleTimes() + { + _reporter.Increment(25); + _reporter.Increment(25); + + _sink.Report(0.5f).WasCalled(Times.Once); + } + + [Test] + public async Task IncrementShouldThrowExceptionWhenValueIsNegative() + { + await Assert.That(() => _reporter.Increment(-25)) + .ThrowsExactly(); + + _sink.Report(Any()).WasNeverCalled(); + } + + [Test] + public void IncrementShouldDoNothingWhenIsCancelled() + { + _reporter.Cancel(); + _reporter.Increment(25); + + _sink.Report(Any()).WasNeverCalled(); + } + + [Test] + public void AdvanceShouldIncrementByOneStep() + { + _reporter.Advance(); + _sink.Report(0.01f).WasCalled(Times.Once); + } + + [Test] + public void AdvanceShouldCumulateAndAdvanceByOneStepWhenCallingMultipleTimes() + { + _reporter.Advance(); + _reporter.Advance(); + + _sink.Report(0.02f).WasCalled(Times.Once); + } + + [Test] + public void AdvanceShouldDoNothingWhenIsCancelled() + { + _reporter.Cancel(); + _reporter.Advance(); + + _sink.Report(Any()).WasNeverCalled(); + } +} From b6910673162773c7aa8182e08d777631c74607ff Mon Sep 17 00:00:00 2001 From: beauchama Date: Wed, 10 Jun 2026 14:04:10 -0400 Subject: [PATCH 2/7] Added AsyncProgressReporter --- analyzers/sonar.editorconfig | 4 + analyzers/vsthrd.editorconfig | 6 + quack.slnx | 1 + .../Progress/AsyncProgressReporter.cs | 117 ++++++++++++++ .../Progress/ProgressReporter.cs | 6 +- src/KappaDuck.Quack/Progress/ProgressState.cs | 30 ++++ tests/.editorconfig | 5 + .../Progress/AsyncProgressReporterTests.cs | 150 ++++++++++++++++++ 8 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 analyzers/vsthrd.editorconfig create mode 100644 src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs create mode 100644 src/KappaDuck.Quack/Progress/ProgressState.cs create mode 100644 tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs diff --git a/analyzers/sonar.editorconfig b/analyzers/sonar.editorconfig index c659aaf..bebc659 100644 --- a/analyzers/sonar.editorconfig +++ b/analyzers/sonar.editorconfig @@ -7,3 +7,7 @@ dotnet_diagnostic.S101.severity = none # S4200: Make this wrapper for native method less trivial # Some method are trivial and should be kept as is dotnet_diagnostic.S4200.severity = none + +# S6966: Use async method instead. +# Is a duplicate of CA1849: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1849 +dotnet_diagnostic.S6966.severity = none diff --git a/analyzers/vsthrd.editorconfig b/analyzers/vsthrd.editorconfig new file mode 100644 index 0000000..c45f7cc --- /dev/null +++ b/analyzers/vsthrd.editorconfig @@ -0,0 +1,6 @@ +is_global = true + +# VSTHRD103: Call async methods when in an async method +# Is a duplicate of CA1849: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1849 +# https://microsoft.github.io/vs-threading/analyzers/VSTHRD103.html +dotnet_diagnostic.VSTHRD103.severity = none diff --git a/quack.slnx b/quack.slnx index 8483613..4ad6bd0 100644 --- a/quack.slnx +++ b/quack.slnx @@ -11,6 +11,7 @@ + diff --git a/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs b/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs new file mode 100644 index 0000000..bbc5f6d --- /dev/null +++ b/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs @@ -0,0 +1,117 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +namespace KappaDuck.Quack.Progress; + +/// +/// Asynchronous determinate progress reporter handed to +/// . +/// +internal sealed class AsyncProgressReporter : IDisposable +{ + private readonly IProgressSink _sink; + private readonly CancellationTokenSource _cts = new(); + private readonly int _total; + + private int _current; + + internal AsyncProgressReporter(IProgressSink sink, int total) + { + _sink = sink; + _total = total; + } + + /// + /// Gets the token to observe for cancellation during the reporting. + /// + public CancellationToken CancellationToken => _cts.Token; + + /// + /// Reports progress by incrementing by 1. + /// + /// + /// This is a shorthand for with steps of 1. + /// + /// Cancellation has been requested. + public void Advance() => Increment(1); + + /// + /// Requests cancellation of the progress operation. + /// + /// + /// The next call to or throws + /// , which the base catches to trigger + /// and reset the bar. + /// + public void Cancel() + { + if (CancellationToken.IsCancellationRequested) + return; + + _cts.Cancel(); + } + + /// + /// Requests cancellation of the progress operation after a delay. + /// + /// The delay after which to cancel. + public void CancelAfter(TimeSpan delay) + { + if (CancellationToken.IsCancellationRequested) + return; + + _cts.CancelAfter(delay); + } + + /// + public void CancelAfter(int millisecondsDelay) => CancelAfter(TimeSpan.FromMilliseconds(millisecondsDelay)); + + /// + /// Asynchronously requests cancellation of the progress operation. + /// + /// A task that represents the asynchronous cancellation. + public Task CancelAsync() + { + if (CancellationToken.IsCancellationRequested) + return Task.CompletedTask; + + return _cts.CancelAsync(); + } + + /// + public void Dispose() => _cts.Dispose(); + + /// + /// Reports progress by incrementing by a step. + /// + /// The number of steps to increment. + /// is negative. + /// Cancellation has been requested. + public void Increment(int steps) + { + CancellationToken.ThrowIfCancellationRequested(); + + ArgumentOutOfRangeException.ThrowIfNegative(steps); + Report(_current + steps); + } + + /// + /// Reports the absolute current progress. + /// + /// + /// The total provided to is + /// used as the maximum limit if is greater than the total. + /// + /// The current progress. + /// is negative. + /// Cancellation has been requested. + public void Report(int current) + { + CancellationToken.ThrowIfCancellationRequested(); + + ArgumentOutOfRangeException.ThrowIfNegative(current); + + _current = Math.Min(current, _total); + _sink.Report((float)_current / _total); + } +} diff --git a/src/KappaDuck.Quack/Progress/ProgressReporter.cs b/src/KappaDuck.Quack/Progress/ProgressReporter.cs index d89b2f2..a9b2256 100644 --- a/src/KappaDuck.Quack/Progress/ProgressReporter.cs +++ b/src/KappaDuck.Quack/Progress/ProgressReporter.cs @@ -4,7 +4,7 @@ namespace KappaDuck.Quack.Progress; /// -/// Synchronous determinate progress reporter handed to . +/// Synchronous determinate progress reporter handed to . /// public sealed class ProgressReporter { @@ -32,7 +32,7 @@ internal ProgressReporter(IProgressSink sink, int total) /// Requests cancellation of the progress operation. /// /// - /// Stops any further reporting, triggers . + /// Stops any further reporting, triggers and resets the bar. /// public void Cancel() { @@ -61,7 +61,7 @@ public void Increment(int steps) /// Reports the absolute current progress. /// /// - /// The total provided to is used as the maximum + /// The total provided to is used as the maximum /// limit if is greater than the total. /// /// The current progress. diff --git a/src/KappaDuck.Quack/Progress/ProgressState.cs b/src/KappaDuck.Quack/Progress/ProgressState.cs new file mode 100644 index 0000000..7cdf039 --- /dev/null +++ b/src/KappaDuck.Quack/Progress/ProgressState.cs @@ -0,0 +1,30 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +namespace KappaDuck.Quack.Progress; + +/// +/// Represents the state of a . +/// +public enum ProgressState +{ + /// + /// No progress is shown; the bar is hidden or cleared. + /// + None = 0, + + /// + /// A normal, determinate progress value is shown. + /// + Normal = 1, + + /// + /// An ongoing operation with no measurable progress is shown. + /// + Indeterminate = 2, + + /// + /// An error state is shown. + /// + Error = 3 +} diff --git a/tests/.editorconfig b/tests/.editorconfig index 2102252..83a4850 100644 --- a/tests/.editorconfig +++ b/tests/.editorconfig @@ -2,6 +2,11 @@ # Disabled Microsoft.VisualStudio.Threading.Analyzers rules +# VSTHRD111: Consider calling ConfigureAwait on the awaited task +# Reason: don't need to configure the await context +# https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD111.md +dotnet_diagnostic.VSTHRD111.severity = none + # VSTHRD200: Use "Async" suffix for async methods # Reason: Don't need to use Async suffix for test methods and give better clarity # https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD200.md diff --git a/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs b/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs new file mode 100644 index 0000000..a6062df --- /dev/null +++ b/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs @@ -0,0 +1,150 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Progress; +using System.Diagnostics.CodeAnalysis; + +namespace Unit.Tests.Progress; + +internal sealed class AsyncProgressReporterTests : IDisposable +{ + private readonly Mock _sink = IProgressSink.Mock(); + private readonly AsyncProgressReporter _reporter; + + public AsyncProgressReporterTests() + { + _reporter = new AsyncProgressReporter(_sink.Object, 100); + } + + public void Dispose() => _reporter.Dispose(); + + [Test] + [SuppressMessage("Performance", "CA1849:Call async methods when in an async method", Justification = "The test requires to test the method Cancel")] + public async Task CancelShouldRequestToCancelTheToken() + { + _reporter.Cancel(); + await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); + } + + [Test] + public async Task CancelAfterShouldRequestToCancelTheToken() + { + _reporter.CancelAfter(TimeSpan.FromMilliseconds(1)); + await Task.Delay(1); + + await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); + } + + [Test] + public async Task CancelAfterByMilliSecondsShouldRequestToCancelTheToken() + { + _reporter.CancelAfter(1); + await Task.Delay(1); + + await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); + } + + [Test] + public async Task CancelAsyncShouldRequestToCancelTheToken() + { + await _reporter.CancelAsync(); + await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); + } + + [Test] + public void ReportShouldReportValueToProgressSink() + { + _reporter.Report(100); + _sink.Report(1).WasCalled(Times.Once); + } + + [Test] + public async Task ReportShouldThrowExceptionWhenValueIsNegative() + { + await Assert.That(() => _reporter.Report(-100)) + .ThrowsExactly(); + + _sink.Report(Any()).WasNeverCalled(); + } + + [Test] + public void ReportShouldClampToTheTotalWhenValueIsGreaterThanTotal() + { + _reporter.Report(999); + _sink.Report(1).WasCalled(Times.Once); + } + + [Test] + public async Task ReportShouldThrowOperationCanceledExceptionWhenIsCancelled() + { + await _reporter.CancelAsync(); + + await Assert.That(() => _reporter.Report(100)) + .ThrowsExactly(); + + _sink.Report(Any()).WasNeverCalled(); + } + + [Test] + public void IncrementShouldIncrementByStep() + { + _reporter.Increment(25); + _sink.Report(0.25f).WasCalled(Times.Once); + } + + [Test] + public void IncrementShouldCumulateAndIncrementTheStepsWhenCallingMultipleTimes() + { + _reporter.Increment(25); + _reporter.Increment(25); + + _sink.Report(0.5f).WasCalled(Times.Once); + } + + [Test] + public async Task IncrementShouldThrowExceptionWhenValueIsNegative() + { + await Assert.That(() => _reporter.Increment(-25)) + .ThrowsExactly(); + + _sink.Report(Any()).WasNeverCalled(); + } + + [Test] + public async Task IncrementShouldThrowOperationCanceledExceptionWhenIsCancelled() + { + await _reporter.CancelAsync(); + + await Assert.That(() => _reporter.Increment(-25)) + .ThrowsExactly(); + + _sink.Report(Any()).WasNeverCalled(); + } + + [Test] + public void AdvanceShouldIncrementByOneStep() + { + _reporter.Advance(); + _sink.Report(0.01f).WasCalled(Times.Once); + } + + [Test] + public void AdvanceShouldCumulateAndAdvanceByOneStepWhenCallingMultipleTimes() + { + _reporter.Advance(); + _reporter.Advance(); + + _sink.Report(0.02f).WasCalled(Times.Once); + } + + [Test] + public async Task AdvanceShouldThrowOperationCanceledExceptionWhenIsCancelled() + { + await _reporter.CancelAsync(); + + await Assert.That(_reporter.Advance) + .ThrowsExactly(); + + _sink.Report(Any()).WasNeverCalled(); + } +} From 305f96de889b077c984ffdd2a39c4bdd48da485c Mon Sep 17 00:00:00 2001 From: beauchama Date: Wed, 10 Jun 2026 14:10:53 -0400 Subject: [PATCH 3/7] Added IndeterminateProgressReporter --- .../Progress/IndeterminateProgressReporter.cs | 27 +++++++++++++++++++ .../IndeterminateProgressReporterTests.cs | 18 +++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/KappaDuck.Quack/Progress/IndeterminateProgressReporter.cs create mode 100644 tests/Unit.Tests/Progress/IndeterminateProgressReporterTests.cs diff --git a/src/KappaDuck.Quack/Progress/IndeterminateProgressReporter.cs b/src/KappaDuck.Quack/Progress/IndeterminateProgressReporter.cs new file mode 100644 index 0000000..5e3c768 --- /dev/null +++ b/src/KappaDuck.Quack/Progress/IndeterminateProgressReporter.cs @@ -0,0 +1,27 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +namespace KappaDuck.Quack.Progress; + +/// +/// Synchronous indeterminate progress reporter handed to +/// . +/// +public sealed class IndeterminateProgressReporter +{ + internal IndeterminateProgressReporter() + { + } + + /// + /// Requests cancellation of the progress operation. + /// + /// + /// Throws , which the base catches to trigger + /// and reset the bar. + /// + /// Always thrown. + [DoesNotReturn] + [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "The method is part of the instance API.")] + public void Cancel() => throw new OperationCanceledException("The indeterminate progress operation has been cancelled."); +} diff --git a/tests/Unit.Tests/Progress/IndeterminateProgressReporterTests.cs b/tests/Unit.Tests/Progress/IndeterminateProgressReporterTests.cs new file mode 100644 index 0000000..8dc9981 --- /dev/null +++ b/tests/Unit.Tests/Progress/IndeterminateProgressReporterTests.cs @@ -0,0 +1,18 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Progress; + +namespace Unit.Tests.Progress; + +internal sealed class IndeterminateProgressReporterTests +{ + private readonly IndeterminateProgressReporter _reporter = new(); + + [Test] + public async Task CancelShouldThrowOperationCanceledException() + { + await Assert.That(_reporter.Cancel) + .ThrowsExactly(); + } +} From 8fc7b29d7dd46e76d19699e65b173398330e7794 Mon Sep 17 00:00:00 2001 From: beauchama Date: Wed, 10 Jun 2026 14:14:19 -0400 Subject: [PATCH 4/7] Added AsyncIndeterminateProgressReporter --- .../AsyncIndeterminateProgressReporter.cs | 67 +++++++++++++++++++ ...AsyncIndeterminateProgressReporterTests.cs | 47 +++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/KappaDuck.Quack/Progress/AsyncIndeterminateProgressReporter.cs create mode 100644 tests/Unit.Tests/Progress/AsyncIndeterminateProgressReporterTests.cs diff --git a/src/KappaDuck.Quack/Progress/AsyncIndeterminateProgressReporter.cs b/src/KappaDuck.Quack/Progress/AsyncIndeterminateProgressReporter.cs new file mode 100644 index 0000000..548338e --- /dev/null +++ b/src/KappaDuck.Quack/Progress/AsyncIndeterminateProgressReporter.cs @@ -0,0 +1,67 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +namespace KappaDuck.Quack.Progress; + +/// +/// Asynchronous indeterminate progress reporter handed to +/// . +/// +public sealed class AsyncIndeterminateProgressReporter : IDisposable +{ + private readonly CancellationTokenSource _cts = new(); + + internal AsyncIndeterminateProgressReporter() + { + } + + /// + /// Gets the token to observe for cancellation during the reporting. + /// + public CancellationToken CancellationToken => _cts.Token; + + /// + /// Requests cancellation of the progress operation. + /// + /// + /// Stops any further reporting, triggers and resets the bar. + /// + public void Cancel() + { + if (CancellationToken.IsCancellationRequested) + return; + + _cts.Cancel(); + } + + /// + /// Requests cancellation of the progress operation after a delay. + /// + /// The delay after which to cancel. + public void CancelAfter(TimeSpan delay) + { + if (CancellationToken.IsCancellationRequested) + return; + + _cts.CancelAfter(delay); + } + + /// + public void CancelAfter(int millisecondsDelay) => CancelAfter(TimeSpan.FromMilliseconds(millisecondsDelay)); + + /// + /// Asynchronously requests cancellation of the progress operation. + /// + /// A task that represents the asynchronous cancellation. + public Task CancelAsync() + { + if (CancellationToken.IsCancellationRequested) + return Task.CompletedTask; + + return _cts.CancelAsync(); + } + + /// + public void Dispose() => _cts.Dispose(); +} + diff --git a/tests/Unit.Tests/Progress/AsyncIndeterminateProgressReporterTests.cs b/tests/Unit.Tests/Progress/AsyncIndeterminateProgressReporterTests.cs new file mode 100644 index 0000000..a6814b6 --- /dev/null +++ b/tests/Unit.Tests/Progress/AsyncIndeterminateProgressReporterTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Progress; +using System.Diagnostics.CodeAnalysis; + +namespace Unit.Tests.Progress; + +internal sealed class AsyncIndeterminateProgressReporterTests : IDisposable +{ + private readonly AsyncIndeterminateProgressReporter _reporter = new(); + + public void Dispose() => _reporter.Dispose(); + + [Test] + [SuppressMessage("Performance", "CA1849:Call async methods when in an async method", Justification = "The test requires to test the method Cancel")] + public async Task CancelShouldRequestToCancelTheToken() + { + _reporter.Cancel(); + await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); + } + + [Test] + public async Task CancelAfterShouldRequestToCancelTheToken() + { + _reporter.CancelAfter(TimeSpan.FromMilliseconds(1)); + await Task.Delay(1); + + await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); + } + + [Test] + public async Task CancelAfterByMilliSecondsShouldRequestToCancelTheToken() + { + _reporter.CancelAfter(1); + await Task.Delay(1); + + await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); + } + + [Test] + public async Task CancelAsyncShouldRequestToCancelTheToken() + { + await _reporter.CancelAsync(); + await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); + } +} From 43516157260133f5db4c53ecfe6ea2c1a28debef Mon Sep 17 00:00:00 2001 From: beauchama Date: Wed, 10 Jun 2026 14:26:51 -0400 Subject: [PATCH 5/7] Renaming IProgressSink to IProgressOperation --- .../Progress/AsyncProgressReporter.cs | 8 ++--- ...IProgressSink.cs => IProgressOperation.cs} | 2 +- .../Progress/ProgressReporter.cs | 10 +++--- .../Progress/AsyncProgressReporterTests.cs | 28 +++++++-------- .../Progress/ProgressReporterTests.cs | 36 +++++++++---------- 5 files changed, 42 insertions(+), 42 deletions(-) rename src/KappaDuck.Quack/Progress/{IProgressSink.cs => IProgressOperation.cs} (94%) diff --git a/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs b/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs index bbc5f6d..68637c7 100644 --- a/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs +++ b/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs @@ -9,15 +9,15 @@ namespace KappaDuck.Quack.Progress; /// internal sealed class AsyncProgressReporter : IDisposable { - private readonly IProgressSink _sink; + private readonly IProgressOperation _operation; private readonly CancellationTokenSource _cts = new(); private readonly int _total; private int _current; - internal AsyncProgressReporter(IProgressSink sink, int total) + internal AsyncProgressReporter(IProgressOperation operation, int total) { - _sink = sink; + _operation = operation; _total = total; } @@ -112,6 +112,6 @@ public void Report(int current) ArgumentOutOfRangeException.ThrowIfNegative(current); _current = Math.Min(current, _total); - _sink.Report((float)_current / _total); + _operation.Report((float)_current / _total); } } diff --git a/src/KappaDuck.Quack/Progress/IProgressSink.cs b/src/KappaDuck.Quack/Progress/IProgressOperation.cs similarity index 94% rename from src/KappaDuck.Quack/Progress/IProgressSink.cs rename to src/KappaDuck.Quack/Progress/IProgressOperation.cs index aa6cbb3..ea13c29 100644 --- a/src/KappaDuck.Quack/Progress/IProgressSink.cs +++ b/src/KappaDuck.Quack/Progress/IProgressOperation.cs @@ -6,7 +6,7 @@ namespace KappaDuck.Quack.Progress; /// /// Represents a progress operation that can be reported or cancelled. /// -public interface IProgressSink +public interface IProgressOperation { /// /// Reports a normalized progress value between 0 and 1. diff --git a/src/KappaDuck.Quack/Progress/ProgressReporter.cs b/src/KappaDuck.Quack/Progress/ProgressReporter.cs index a9b2256..81ae248 100644 --- a/src/KappaDuck.Quack/Progress/ProgressReporter.cs +++ b/src/KappaDuck.Quack/Progress/ProgressReporter.cs @@ -8,15 +8,15 @@ namespace KappaDuck.Quack.Progress; /// public sealed class ProgressReporter { - private readonly IProgressSink _sink; + private readonly IProgressOperation _operation; private readonly int _total; private bool _isCancelled; private int _current; - internal ProgressReporter(IProgressSink sink, int total) + internal ProgressReporter(IProgressOperation operation, int total) { - _sink = sink; + _operation = operation; _total = total; } @@ -40,7 +40,7 @@ public void Cancel() return; _isCancelled = true; - _sink.Cancel(); + _operation.Cancel(); } /// @@ -74,6 +74,6 @@ public void Report(int current) ArgumentOutOfRangeException.ThrowIfNegative(current); _current = Math.Min(current, _total); - _sink.Report((float)_current / _total); + _operation.Report((float)_current / _total); } } diff --git a/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs b/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs index a6062df..83c684a 100644 --- a/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs +++ b/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs @@ -8,12 +8,12 @@ namespace Unit.Tests.Progress; internal sealed class AsyncProgressReporterTests : IDisposable { - private readonly Mock _sink = IProgressSink.Mock(); + private readonly Mock _operation = IProgressOperation.Mock(); private readonly AsyncProgressReporter _reporter; public AsyncProgressReporterTests() { - _reporter = new AsyncProgressReporter(_sink.Object, 100); + _reporter = new AsyncProgressReporter(_operation.Object, 100); } public void Dispose() => _reporter.Dispose(); @@ -52,10 +52,10 @@ public async Task CancelAsyncShouldRequestToCancelTheToken() } [Test] - public void ReportShouldReportValueToProgressSink() + public void ReportShouldReportValueToOperation() { _reporter.Report(100); - _sink.Report(1).WasCalled(Times.Once); + _operation.Report(1).WasCalled(Times.Once); } [Test] @@ -64,14 +64,14 @@ public async Task ReportShouldThrowExceptionWhenValueIsNegative() await Assert.That(() => _reporter.Report(-100)) .ThrowsExactly(); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } [Test] public void ReportShouldClampToTheTotalWhenValueIsGreaterThanTotal() { _reporter.Report(999); - _sink.Report(1).WasCalled(Times.Once); + _operation.Report(1).WasCalled(Times.Once); } [Test] @@ -82,14 +82,14 @@ public async Task ReportShouldThrowOperationCanceledExceptionWhenIsCancelled() await Assert.That(() => _reporter.Report(100)) .ThrowsExactly(); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } [Test] public void IncrementShouldIncrementByStep() { _reporter.Increment(25); - _sink.Report(0.25f).WasCalled(Times.Once); + _operation.Report(0.25f).WasCalled(Times.Once); } [Test] @@ -98,7 +98,7 @@ public void IncrementShouldCumulateAndIncrementTheStepsWhenCallingMultipleTimes( _reporter.Increment(25); _reporter.Increment(25); - _sink.Report(0.5f).WasCalled(Times.Once); + _operation.Report(0.5f).WasCalled(Times.Once); } [Test] @@ -107,7 +107,7 @@ public async Task IncrementShouldThrowExceptionWhenValueIsNegative() await Assert.That(() => _reporter.Increment(-25)) .ThrowsExactly(); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } [Test] @@ -118,14 +118,14 @@ public async Task IncrementShouldThrowOperationCanceledExceptionWhenIsCancelled( await Assert.That(() => _reporter.Increment(-25)) .ThrowsExactly(); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } [Test] public void AdvanceShouldIncrementByOneStep() { _reporter.Advance(); - _sink.Report(0.01f).WasCalled(Times.Once); + _operation.Report(0.01f).WasCalled(Times.Once); } [Test] @@ -134,7 +134,7 @@ public void AdvanceShouldCumulateAndAdvanceByOneStepWhenCallingMultipleTimes() _reporter.Advance(); _reporter.Advance(); - _sink.Report(0.02f).WasCalled(Times.Once); + _operation.Report(0.02f).WasCalled(Times.Once); } [Test] @@ -145,6 +145,6 @@ public async Task AdvanceShouldThrowOperationCanceledExceptionWhenIsCancelled() await Assert.That(_reporter.Advance) .ThrowsExactly(); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } } diff --git a/tests/Unit.Tests/Progress/ProgressReporterTests.cs b/tests/Unit.Tests/Progress/ProgressReporterTests.cs index 58d124b..9afa79d 100644 --- a/tests/Unit.Tests/Progress/ProgressReporterTests.cs +++ b/tests/Unit.Tests/Progress/ProgressReporterTests.cs @@ -7,35 +7,35 @@ namespace Unit.Tests.Progress; internal sealed class ProgressReporterTests { - private readonly Mock _sink = IProgressSink.Mock(); + private readonly Mock _operation = IProgressOperation.Mock(); private readonly ProgressReporter _reporter; public ProgressReporterTests() { - _reporter = new ProgressReporter(_sink.Object, 100); + _reporter = new ProgressReporter(_operation.Object, 100); } [Test] - public void CancelShouldCancelTheProgressSink() + public void CancelShouldCancelTheOperation() { _reporter.Cancel(); - _sink.Cancel().WasCalled(Times.Once); + _operation.Cancel().WasCalled(Times.Once); } [Test] - public void CancelShouldNotCancelTheProgressSinkTwiceWhenIsAlreadyCancelled() + public void CancelShouldNotCancelTheOperationTwiceWhenIsAlreadyCancelled() { _reporter.Cancel(); _reporter.Cancel(); - _sink.Cancel().WasCalled(Times.Once); + _operation.Cancel().WasCalled(Times.Once); } [Test] - public void ReportShouldReportValueToProgressSink() + public void ReportShouldReportValueToOperation() { _reporter.Report(100); - _sink.Report(1).WasCalled(Times.Once); + _operation.Report(1).WasCalled(Times.Once); } [Test] @@ -44,14 +44,14 @@ public async Task ReportShouldThrowExceptionWhenValueIsNegative() await Assert.That(() => _reporter.Report(-100)) .ThrowsExactly(); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } [Test] public void ReportShouldClampToTheTotalWhenValueIsGreaterThanTotal() { _reporter.Report(999); - _sink.Report(1).WasCalled(Times.Once); + _operation.Report(1).WasCalled(Times.Once); } [Test] @@ -60,14 +60,14 @@ public void ReportShouldDoNothingWhenIsCancelled() _reporter.Cancel(); _reporter.Report(100); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } [Test] public void IncrementShouldIncrementByStep() { _reporter.Increment(25); - _sink.Report(0.25f).WasCalled(Times.Once); + _operation.Report(0.25f).WasCalled(Times.Once); } [Test] @@ -76,7 +76,7 @@ public void IncrementShouldCumulateAndIncrementTheStepsWhenCallingMultipleTimes( _reporter.Increment(25); _reporter.Increment(25); - _sink.Report(0.5f).WasCalled(Times.Once); + _operation.Report(0.5f).WasCalled(Times.Once); } [Test] @@ -85,7 +85,7 @@ public async Task IncrementShouldThrowExceptionWhenValueIsNegative() await Assert.That(() => _reporter.Increment(-25)) .ThrowsExactly(); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } [Test] @@ -94,14 +94,14 @@ public void IncrementShouldDoNothingWhenIsCancelled() _reporter.Cancel(); _reporter.Increment(25); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } [Test] public void AdvanceShouldIncrementByOneStep() { _reporter.Advance(); - _sink.Report(0.01f).WasCalled(Times.Once); + _operation.Report(0.01f).WasCalled(Times.Once); } [Test] @@ -110,7 +110,7 @@ public void AdvanceShouldCumulateAndAdvanceByOneStepWhenCallingMultipleTimes() _reporter.Advance(); _reporter.Advance(); - _sink.Report(0.02f).WasCalled(Times.Once); + _operation.Report(0.02f).WasCalled(Times.Once); } [Test] @@ -119,6 +119,6 @@ public void AdvanceShouldDoNothingWhenIsCancelled() _reporter.Cancel(); _reporter.Advance(); - _sink.Report(Any()).WasNeverCalled(); + _operation.Report(Any()).WasNeverCalled(); } } From 06863ee542ed6682b71bed36f9a50a24945e7183 Mon Sep 17 00:00:00 2001 From: beauchama Date: Wed, 10 Jun 2026 15:56:25 -0400 Subject: [PATCH 6/7] Added abstract ProgressBar --- analyzers/disabled.editorconfig | 5 + .../Progress/AsyncProgressReporter.cs | 6 +- src/KappaDuck.Quack/Progress/ProgressBar.cs | 281 ++++++++++++++++++ .../Progress/ProgressErrorEventArgs.cs | 16 + .../Progress/ProgressReporter.cs | 4 +- .../Progress/ProgressValueEventArgs.cs | 16 + 6 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 src/KappaDuck.Quack/Progress/ProgressBar.cs create mode 100644 src/KappaDuck.Quack/Progress/ProgressErrorEventArgs.cs create mode 100644 src/KappaDuck.Quack/Progress/ProgressValueEventArgs.cs diff --git a/analyzers/disabled.editorconfig b/analyzers/disabled.editorconfig index 9f7d880..85786d5 100644 --- a/analyzers/disabled.editorconfig +++ b/analyzers/disabled.editorconfig @@ -1,5 +1,10 @@ is_global = true +# CA1033: Interface methods should be callable by child types +# Some childs should not have the parent methods +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1033 +dotnet_diagnostic.CA1033.severity = none + # CA1031: Do not catch general exception types # Sometimes we don't know the exception. # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1031 diff --git a/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs b/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs index 68637c7..699797b 100644 --- a/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs +++ b/src/KappaDuck.Quack/Progress/AsyncProgressReporter.cs @@ -5,9 +5,9 @@ namespace KappaDuck.Quack.Progress; /// /// Asynchronous determinate progress reporter handed to -/// . +/// . /// -internal sealed class AsyncProgressReporter : IDisposable +public sealed class AsyncProgressReporter : IDisposable { private readonly IProgressOperation _operation; private readonly CancellationTokenSource _cts = new(); @@ -99,7 +99,7 @@ public void Increment(int steps) /// Reports the absolute current progress. /// /// - /// The total provided to is + /// The total provided to is /// used as the maximum limit if is greater than the total. /// /// The current progress. diff --git a/src/KappaDuck.Quack/Progress/ProgressBar.cs b/src/KappaDuck.Quack/Progress/ProgressBar.cs new file mode 100644 index 0000000..a40ff98 --- /dev/null +++ b/src/KappaDuck.Quack/Progress/ProgressBar.cs @@ -0,0 +1,281 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Exceptions; +using KappaDuck.Quack.Geometry; + +namespace KappaDuck.Quack.Progress; +#pragma warning disable SYSLIB5007, CA2252 + +/// +/// Base class for a progress bar backend, e.g. a window's taskbar icon, an on-screen control or a console bar. +/// +/// +/// +/// Owns the reporting lifecycle, value normalization, completion detection and events. A concrete backend +/// only translates a and a normalized value into its own representation by +/// implementing and . +/// +/// +/// The base is intentionally thread-agnostic: reporting and event raising run on whichever thread calls into +/// it. Any thread affinity (e.g. marshalling onto a UI/main thread) is a backend concern and belongs in the +/// overrides. See and friends for the event hooks. +/// +/// +public abstract class ProgressBar : IProgressOperation +{ + private float _lastValue = -1f; + private bool _isReporting; + private bool _isCompleted; + + /// + /// Occurs when the reporting is cancelled. + /// + public event EventHandler? Cancelled; + + /// + /// Occurs when the reporting is completed. + /// + public event EventHandler? Completed; + + /// + /// Occurs when an unhandled exception is encountered during the reporting. + /// + public event EventHandler? ErrorOccurred; + + /// + /// Occurs when the normalized progress value changes. + /// + public event EventHandler? ProgressChanged; + + /// + /// Raises . Override to control the thread it runs on. + /// + /// The normalized value. + protected virtual void OnProgressChanged(float value) => ProgressChanged?.Invoke(this, new ProgressValueEventArgs(value)); + + /// + /// Raises . Override to control the thread it runs on. + /// + protected virtual void OnCompleted() => Completed?.Invoke(this, EventArgs.Empty); + + /// + /// Raises . Override to control the thread it runs on. + /// + protected virtual void OnCancelled() => Cancelled?.Invoke(this, EventArgs.Empty); + + /// + /// Raises . Override to control the thread it runs on. + /// + /// The exception that was caught. + protected virtual void OnErrorOccurred(Exception exception) => ErrorOccurred?.Invoke(this, new ProgressErrorEventArgs(exception)); + + /// + /// Applies a to the backend. + /// + /// The state to apply. + protected abstract void SetState(ProgressState state); + + /// + /// Applies a normalized between 0 and 1 to the backend. + /// + /// + /// Already clamped by the base; the backend receives only meaningful changes. + /// + /// The normalized value between 0 and 1. + protected abstract void SetValue(float value); + + /// + /// Starts a synchronous determinate progress operation. + /// + /// + /// Any unhandled exception thrown within is caught, reported to + /// and switches the backend to . + /// + /// The action that performs the progress operation. + /// The value representing 100% of the progress. + /// value is negative or zero. + /// A progress report is already in progress. + public void Start(Action action, int total = 100) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(total); + ThrowHelper.ThrowIf(_isReporting, "Cannot begin a new progress report while another is in progress."); + + BeginReport(ProgressState.Normal); + + try + { + ProgressReporter reporter = new(this, total); + action(reporter); + } + catch (Exception exception) + { + Fail(exception); + } + } + + /// + /// Starts an asynchronous determinate progress operation. + /// + /// + /// The action that performs the progress operation. + /// The value representing 100% of the progress. + /// The task representing the asynchronous operation. + /// + public async ValueTask StartAsync(Func action, int total = 100) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(total); + ThrowHelper.ThrowIf(_isReporting, "Cannot begin a new progress report while another is in progress."); + + BeginReport(ProgressState.Normal); + + try + { + using AsyncProgressReporter reporter = new(this, total); + await action(reporter).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Cancel(); + } + catch (Exception exception) + { + Fail(exception); + } + } + + /// + /// Starts a synchronous indeterminate progress operation. + /// + /// + /// The action that performs the progress operation. + /// A progress report is already in progress. + public void StartIndeterminate(Action action) + { + ThrowHelper.ThrowIf(_isReporting, "Cannot begin a new progress report while another is in progress."); + + BeginReport(ProgressState.Indeterminate); + + try + { + IndeterminateProgressReporter progress = new(); + action(progress); + + Complete(); + } + catch (OperationCanceledException) + { + Cancel(); + } + catch (Exception exception) + { + SetValue(0.5f); + Fail(exception); + } + } + + /// + /// Starts an asynchronous indeterminate progress operation. + /// + /// + /// The action that performs the progress operation. + /// The task representing the asynchronous operation. + /// A progress report is already in progress. + public async ValueTask StartIndeterminateAsync(Func action) + { + ThrowHelper.ThrowIf(_isReporting, "Cannot begin a new progress report while another is in progress."); + + BeginReport(ProgressState.Indeterminate); + + try + { + using AsyncIndeterminateProgressReporter progress = new(); + await action(progress).ConfigureAwait(false); + + Complete(); + } + catch (OperationCanceledException) + { + Cancel(); + } + catch (Exception exception) + { + SetValue(0.5f); + Fail(exception); + } + } + + /// + /// Resets the progress bar to its default, empty state. + /// + /// + /// Useful to clear any existing state or value, especially after encountering an error. + /// + public void Reset() + { + _isReporting = false; + _isCompleted = false; + _lastValue = -1f; + + SetValue(0f); + SetState(ProgressState.None); + } + + void IProgressOperation.Report(float value) + { + if (_isCompleted) + return; + + float progress = Math.Clamp(value, 0f, 1f); + + if (MathF.ApproximatelyZero(progress - _lastValue)) + return; + + _lastValue = progress; + + SetValue(progress); + OnProgressChanged(progress); + + if (progress >= 1f) + Complete(); + } + + void IProgressOperation.Cancel() => Cancel(); + + private void BeginReport(ProgressState state) + { + _isCompleted = false; + _isReporting = true; + _lastValue = -1f; + + SetState(state); + } + + private void Cancel() + { + OnCancelled(); + Reset(); + } + + private void Complete() + { + if (_isCompleted) + return; + + _isCompleted = true; + _isReporting = false; + + OnCompleted(); + Reset(); + } + + private void Fail(Exception exception) + { + _isCompleted = false; + _isReporting = false; + + OnErrorOccurred(exception); + SetState(ProgressState.Error); + } +} +#pragma warning restore SYSLIB5007, CA2252 diff --git a/src/KappaDuck.Quack/Progress/ProgressErrorEventArgs.cs b/src/KappaDuck.Quack/Progress/ProgressErrorEventArgs.cs new file mode 100644 index 0000000..e2bf179 --- /dev/null +++ b/src/KappaDuck.Quack/Progress/ProgressErrorEventArgs.cs @@ -0,0 +1,16 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +namespace KappaDuck.Quack.Progress; + +/// +/// Provides data for the event. +/// +/// The exception that was caught during reporting. +public sealed class ProgressErrorEventArgs(Exception exception) : EventArgs +{ + /// + /// Gets the exception that was caught during reporting. + /// + public Exception Exception { get; } = exception; +} diff --git a/src/KappaDuck.Quack/Progress/ProgressReporter.cs b/src/KappaDuck.Quack/Progress/ProgressReporter.cs index 81ae248..300f5a7 100644 --- a/src/KappaDuck.Quack/Progress/ProgressReporter.cs +++ b/src/KappaDuck.Quack/Progress/ProgressReporter.cs @@ -4,7 +4,7 @@ namespace KappaDuck.Quack.Progress; /// -/// Synchronous determinate progress reporter handed to . +/// Synchronous determinate progress reporter handed to . /// public sealed class ProgressReporter { @@ -61,7 +61,7 @@ public void Increment(int steps) /// Reports the absolute current progress. /// /// - /// The total provided to is used as the maximum + /// The total provided to is used as the maximum /// limit if is greater than the total. /// /// The current progress. diff --git a/src/KappaDuck.Quack/Progress/ProgressValueEventArgs.cs b/src/KappaDuck.Quack/Progress/ProgressValueEventArgs.cs new file mode 100644 index 0000000..7094bac --- /dev/null +++ b/src/KappaDuck.Quack/Progress/ProgressValueEventArgs.cs @@ -0,0 +1,16 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +namespace KappaDuck.Quack.Progress; + +/// +/// Provides data for the event. +/// +/// The normalized progress value between 0 and 1. +public sealed class ProgressValueEventArgs(float value) : EventArgs +{ + /// + /// Gets the normalized progress value between 0 and 1. + /// + public float Value { get; } = value; +} From 05d96b98449251e2689e69b6130163b5bfd3f4a6 Mon Sep 17 00:00:00 2001 From: beauchama Date: Wed, 10 Jun 2026 17:05:15 -0400 Subject: [PATCH 7/7] Remove CancelAfter tests because of delay will cause headache --- .../AsyncIndeterminateProgressReporterTests.cs | 18 ------------------ .../Progress/AsyncProgressReporterTests.cs | 18 ------------------ 2 files changed, 36 deletions(-) diff --git a/tests/Unit.Tests/Progress/AsyncIndeterminateProgressReporterTests.cs b/tests/Unit.Tests/Progress/AsyncIndeterminateProgressReporterTests.cs index a6814b6..e5c674d 100644 --- a/tests/Unit.Tests/Progress/AsyncIndeterminateProgressReporterTests.cs +++ b/tests/Unit.Tests/Progress/AsyncIndeterminateProgressReporterTests.cs @@ -20,24 +20,6 @@ public async Task CancelShouldRequestToCancelTheToken() await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); } - [Test] - public async Task CancelAfterShouldRequestToCancelTheToken() - { - _reporter.CancelAfter(TimeSpan.FromMilliseconds(1)); - await Task.Delay(1); - - await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); - } - - [Test] - public async Task CancelAfterByMilliSecondsShouldRequestToCancelTheToken() - { - _reporter.CancelAfter(1); - await Task.Delay(1); - - await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); - } - [Test] public async Task CancelAsyncShouldRequestToCancelTheToken() { diff --git a/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs b/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs index 83c684a..eba780c 100644 --- a/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs +++ b/tests/Unit.Tests/Progress/AsyncProgressReporterTests.cs @@ -26,24 +26,6 @@ public async Task CancelShouldRequestToCancelTheToken() await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); } - [Test] - public async Task CancelAfterShouldRequestToCancelTheToken() - { - _reporter.CancelAfter(TimeSpan.FromMilliseconds(1)); - await Task.Delay(1); - - await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); - } - - [Test] - public async Task CancelAfterByMilliSecondsShouldRequestToCancelTheToken() - { - _reporter.CancelAfter(1); - await Task.Delay(1); - - await _reporter.CancellationToken.IsCancellationRequested.Should().BeTrue(); - } - [Test] public async Task CancelAsyncShouldRequestToCancelTheToken() {