From ed83b30a0e8c75465ec4365fdf5531609cc60fdc Mon Sep 17 00:00:00 2001 From: gcastellov Date: Mon, 1 Sep 2025 13:45:43 +0200 Subject: [PATCH 1/3] stats feature --- src/Progress.Samples.Bar.App/Program.cs | 6 ++ .../Program.cs | 6 ++ src/Progress.Samples.Pulse.App/Program.cs | 6 ++ src/Progress.Samples.Spinner.App/Program.cs | 6 ++ src/Progress/Reporter.cs | 63 +++++++++++++++++++ src/Progress/ReporterBuilder.cs | 31 +++++++++ src/Progress/Stats.cs | 52 +++++++++++++++ 7 files changed, 170 insertions(+) create mode 100644 src/Progress/Stats.cs diff --git a/src/Progress.Samples.Bar.App/Program.cs b/src/Progress.Samples.Bar.App/Program.cs index e575b27..58509da 100644 --- a/src/Progress.Samples.Bar.App/Program.cs +++ b/src/Progress.Samples.Bar.App/Program.cs @@ -2,6 +2,11 @@ using Progress.Descriptors; using Progress.Samples; +var onStatsNotified = (Stats stats) => +{ + // TODO: Implement collect +}; + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -9,6 +14,7 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() + .NotifyingStats(onStatsNotified) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(BarDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.Samples.HearthBeat.App/Program.cs b/src/Progress.Samples.HearthBeat.App/Program.cs index 21f50e8..c473cac 100644 --- a/src/Progress.Samples.HearthBeat.App/Program.cs +++ b/src/Progress.Samples.HearthBeat.App/Program.cs @@ -2,6 +2,11 @@ using Progress.Descriptors; using Progress.Samples; +var onStatsNotified = (Stats stats) => +{ + // TODO: Implement collect +}; + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -9,6 +14,7 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() + .NotifyingStats(onStatsNotified) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(HearthBeatDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.Samples.Pulse.App/Program.cs b/src/Progress.Samples.Pulse.App/Program.cs index 7757123..39bf374 100644 --- a/src/Progress.Samples.Pulse.App/Program.cs +++ b/src/Progress.Samples.Pulse.App/Program.cs @@ -2,6 +2,11 @@ using Progress.Descriptors; using Progress.Samples; +var onStatsNotified = (Stats stats) => +{ + // TODO: Implement collect +}; + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -9,6 +14,7 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() + .NotifyingStats(onStatsNotified) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(PulseDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.Samples.Spinner.App/Program.cs b/src/Progress.Samples.Spinner.App/Program.cs index f689c62..3b8e0bb 100644 --- a/src/Progress.Samples.Spinner.App/Program.cs +++ b/src/Progress.Samples.Spinner.App/Program.cs @@ -2,6 +2,11 @@ using Progress.Descriptors; using Progress.Samples; +var onStatsNotified = (Stats stats) => +{ + // TODO: Implement collect +}; + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -9,6 +14,7 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() + .NotifyingStats(onStatsNotified) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(SpinnerDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress/Reporter.cs b/src/Progress/Reporter.cs index cb629a9..b31d9b3 100644 --- a/src/Progress/Reporter.cs +++ b/src/Progress/Reporter.cs @@ -19,21 +19,31 @@ public class Reporter : IDisposable private Component _component; private Timer _timer; private Thread _reportingThread = default!; + private Thread _statsThread = default!; private CancellationTokenSource _cancellationTokenSource = default!; private CancellationToken _cancellationToken; + private DateTimeOffset _lastStatsNotification; /// /// Inidicates whehter the task is completed or not. /// public bool IsFinished => CurrentCount == _itemsCount; + /// + /// Gets or sets the stats notification hook. + /// + public Action OnStatsNotified { get; set; } = default!; + internal bool DisplayEstimatedTimeOfArrival { get; set; } = true; internal bool DisplayRemainingTime { get; set; } = true; internal bool DisplayElapsedTime { get; set; } = true; internal bool DisplayStartingTime { get; set; } = true; internal bool DisplayItemsOverview { get; set; } = true; internal bool DisplayItemsSummary { get; set; } = true; + internal bool NotifyStats { get; set; } = true; + internal TimeSpan ReportFrequency { get; set; } = TimeSpan.FromSeconds(1); + internal TimeSpan StatsFrequency { get; set; } = TimeSpan.FromSeconds(5); private string AllItems => (_successCount + _failureCount).ToString().PadLeft(10); private string SuccessfulItems => _successCount.ToString().PadLeft(10); @@ -73,11 +83,18 @@ public void Start() _successCount = 0; _failureCount = 0; + _lastStatsNotification = DateTimeOffset.UtcNow; _timer = Timer.Start(); _cancellationTokenSource = new CancellationTokenSource(); _cancellationToken = _cancellationTokenSource.Token; _reportingThread = DoWork(); _reportingThread.Start(); + + if (NotifyStats) + { + _statsThread = DoStats(); + _statsThread.Start(); + } } /// @@ -93,6 +110,9 @@ public void Stop() if (_reportingThread != null && _reportingThread.IsAlive) _reportingThread.Join(); + + if (_statsThread != null && _statsThread.IsAlive) + _statsThread.Join(); } /// @@ -110,6 +130,12 @@ public void Resume() _cancellationToken = _cancellationTokenSource.Token; _reportingThread = DoWork(); _reportingThread.Start(); + + if (NotifyStats) + { + _statsThread = DoStats(); + _statsThread.Start(); + } } /// @@ -213,6 +239,28 @@ private void Display() stream.Flush(); } + private void ReportStats() + { + if (OnStatsNotified == null || DateTimeOffset.UtcNow - _lastStatsNotification < StatsFrequency) + return; + + var stats = new Stats + { + StartedOn = _timer.StartedOn, + ElapsedTime = _timer.ElapsedTime, + RemainingTime = _timer.GetRemainingTime(_component.CurrentPercent.Value), + EstTimeOfArrival = _timer.GetEstimatedTimeOfArrival(_component.CurrentPercent.Value), + ExpectedItems = _itemsCount, + CurrentCount = CurrentCount, + SuccessCount = _successCount, + FailureCount = _failureCount, + CurrentPercent = _component.CurrentPercent.Value, + }; + + OnStatsNotified.Invoke(stats); + _lastStatsNotification = DateTimeOffset.UtcNow; + } + private Thread DoWork() { var tStart = new ThreadStart(() => @@ -229,4 +277,19 @@ private Thread DoWork() return new(tStart); } + + private Thread DoStats() + { + var tStart = new ThreadStart(() => + { + do + { + ReportStats(); + Thread.Sleep(StatsFrequency); + } + while (!IsFinished && !_cancellationToken.IsCancellationRequested); + }); + + return new(tStart); + } } diff --git a/src/Progress/ReporterBuilder.cs b/src/Progress/ReporterBuilder.cs index 376cc45..d7dfc72 100644 --- a/src/Progress/ReporterBuilder.cs +++ b/src/Progress/ReporterBuilder.cs @@ -14,8 +14,11 @@ public class ReporterBuilder private bool _displayStartingTime; private bool _displayItemsOverview; private bool _displayItemsSummary; + private bool _notifyingStats; private TimeSpan _reportFrequency = TimeSpan.FromSeconds(1); + private TimeSpan _statsFrequency = TimeSpan.FromSeconds(5); private ComponentDescriptor _componentDescriptor = BarDescriptor.Default; + private Action _onStatsNotified = default!; /// /// The reporter will display the elapsed time since the start. @@ -100,6 +103,31 @@ public ReporterBuilder UsingComponentDescriptor(ComponentDescriptor descriptor) return this; } + /// + /// Sets the stats notification callback. + /// Default notification frequency is set to 5 seconds. + /// + /// + /// + public ReporterBuilder NotifyingStats(Action callback) + { + return NotifyingStats(callback, _statsFrequency); + } + + /// + /// Sets the stats notification callback with the invocation frequency . + /// + /// + /// + /// + public ReporterBuilder NotifyingStats(Action callback, TimeSpan statsFrequency) + { + _onStatsNotified = callback; + _statsFrequency = statsFrequency; + _notifyingStats = true; + return this; + } + /// /// Builds the reporte getting an instance of . /// @@ -121,7 +149,10 @@ public Reporter Build(ulong itemsCount) DisplayStartingTime = _displayStartingTime, DisplayItemsOverview = _displayItemsOverview, DisplayItemsSummary = _displayItemsSummary, + NotifyStats = _notifyingStats, ReportFrequency = _reportFrequency, + StatsFrequency = _statsFrequency, + OnStatsNotified = _onStatsNotified, }; } } diff --git a/src/Progress/Stats.cs b/src/Progress/Stats.cs new file mode 100644 index 0000000..9bcb7a2 --- /dev/null +++ b/src/Progress/Stats.cs @@ -0,0 +1,52 @@ +namespace Progress; + +/// +/// Represents statisitics about the operation +/// +public class Stats +{ + /// + /// Gets the starting time + /// + public DateTimeOffset StartedOn { get; internal init; } + + /// + /// Gets the ETA + /// + public DateTimeOffset EstTimeOfArrival { get; internal init; } + + /// + /// Gets the elapsed time + /// + public TimeSpan ElapsedTime { get; internal init; } + + /// + /// Gets the remaining time + /// + public TimeSpan RemainingTime { get; internal init; } + + /// + /// Gets the count of success + /// + public ulong SuccessCount { get; internal init; } + + /// + /// Gets the count of failures + /// + public ulong FailureCount { get; internal init; } + + /// + /// Gets the current count of failures and success + /// + public ulong CurrentCount { get; internal init; } + + /// + /// Gets the expected items to process + /// + public ulong ExpectedItems { get; internal init; } + + /// + /// Gets the current percent + /// + public double CurrentPercent { get; internal init; } +} From 8fbb29a898ce50b969b006af1ca14d6490cc4100 Mon Sep 17 00:00:00 2001 From: gcastellov Date: Mon, 1 Sep 2025 16:21:38 +0200 Subject: [PATCH 2/3] reporter builder tests for stats notifications --- src/Progress.UnitTest/ReporterBuilderTests.cs | 35 +++++++++++++++++++ src/Progress.UnitTest/ReporterTests.cs | 22 ++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/Progress.UnitTest/ReporterBuilderTests.cs b/src/Progress.UnitTest/ReporterBuilderTests.cs index 6c02bbd..b92d03c 100644 --- a/src/Progress.UnitTest/ReporterBuilderTests.cs +++ b/src/Progress.UnitTest/ReporterBuilderTests.cs @@ -98,4 +98,39 @@ public void GivenSummary_WhenBuilding_ThenReporterIsSetUp() // Assert actual.DisplayItemsSummary.Should().BeTrue(); } + + [Fact] + public void GivenStatsNotifications_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var callback = (Stats stats) => { }; + var builder = new ReporterBuilder() + .NotifyingStats(callback); + + // Act + var actual = builder.Build(100); + + // Arrange + actual.OnStatsNotified.Should().NotBeNull(); + actual.OnStatsNotified.Should().Be(callback); + actual.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); + } + + [Fact] + public void GivenStatsNotificationsWithFrequency_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var callback = (Stats stats) => { }; + var frequency = TimeSpan.FromSeconds(20); + var builder = new ReporterBuilder() + .NotifyingStats(callback, frequency); + + // Act + var actual = builder.Build(100); + + // Arrange + actual.OnStatsNotified.Should().NotBeNull(); + actual.OnStatsNotified.Should().Be(callback); + actual.StatsFrequency.Should().Be(frequency); + } } diff --git a/src/Progress.UnitTest/ReporterTests.cs b/src/Progress.UnitTest/ReporterTests.cs index fbdcd97..9444cba 100644 --- a/src/Progress.UnitTest/ReporterTests.cs +++ b/src/Progress.UnitTest/ReporterTests.cs @@ -7,11 +7,29 @@ public class ReporterTests { public ReporterTests() { - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); TextWriter writer = new StringWriter(builder); Console.SetOut(writer); } + [Fact] + public void GivenDefaults_WhenInitializing_ThenExpectedSettings() + { + // Arrange + var reporter = new Reporter(100, BarDescriptor.Default.Build()); + + // Assert + reporter.DisplayStartingTime.Should().BeTrue(); + reporter.DisplayRemainingTime.Should().BeTrue(); + reporter.DisplayElapsedTime.Should().BeTrue(); + reporter.DisplayEstimatedTimeOfArrival.Should().BeTrue(); + reporter.DisplayItemsOverview.Should().BeTrue(); + reporter.DisplayItemsSummary.Should().BeTrue(); + reporter.NotifyStats.Should().BeTrue(); + reporter.ReportFrequency.Should().Be(TimeSpan.FromSeconds(1)); + reporter.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); + } + [Fact] public void GivenNothingToComplete_WhenInitializing_ThenThrowsException() { @@ -94,4 +112,4 @@ public void GivenStoppedOperation_WhenResuming_ThenSuccess() // Act reporter.Resume(); } -} +} \ No newline at end of file From 944322b1a047f08f0268925a4d52eb0d8f3dad3a Mon Sep 17 00:00:00 2001 From: gcastellov Date: Tue, 2 Sep 2025 15:14:14 +0200 Subject: [PATCH 3/3] onCompletion notification feature --- README.md | 32 +++++++++-- src/Progress.Samples.Bar.App/Program.cs | 12 +++- .../Program.cs | 12 +++- src/Progress.Samples.Pulse.App/Program.cs | 13 ++++- src/Progress.Samples.Spinner.App/Program.cs | 13 ++++- src/Progress.UnitTest/ReporterBuilderTests.cs | 33 ++++++++--- src/Progress.UnitTest/ReporterTests.cs | 48 +++++++++++++++- src/Progress/Content/README.md | 26 +++++++-- src/Progress/Progress.csproj | 2 +- src/Progress/Reporter.cs | 57 ++++++++++++------- src/Progress/ReporterBuilder.cs | 34 +++++++---- 11 files changed, 217 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index a42e63c..3ad7a4f 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,6 @@ Report progression with ease by using multilple components such as Bars, Spinner ```csharp using var reporter = new ReporterBuilder() - .DisplayingStartingTime() - .DisplayingElapsedTime() - .DisplayingTimeOfArrival() - .DisplayingRemainingTime() - .DisplayingItemsSummary() - .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(BarDescriptor.Default) .Build(Worker.AllItems); @@ -44,6 +38,14 @@ var descriptor = BarDescriptor.Default; ### Define the reporter to use the component ```csharp var reporter = new ReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .DisplayingItemsSummary() + .DisplayingItemsOverview() + .NotifyingProgress(onProgress) + .NotifyingCompletion(onCompletion) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(descriptor) .Build(Worker.AllItems); @@ -59,3 +61,21 @@ reporter.Start(); reporter.ReportSuccess() reporter.ReportFailure() ``` + +### Collect stats while running +```csharp +var onProgress = (Stats stats) => +{ + // TODO: Do something useful +}; + +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful +}; + +var reporter = new ReporterBuilder() + .NotifyingProgress(onProgress) + .NotifyingCompletion(onCompletion) + .Build(Worker.AllItems); +``` \ No newline at end of file diff --git a/src/Progress.Samples.Bar.App/Program.cs b/src/Progress.Samples.Bar.App/Program.cs index 58509da..e8bd44c 100644 --- a/src/Progress.Samples.Bar.App/Program.cs +++ b/src/Progress.Samples.Bar.App/Program.cs @@ -2,9 +2,14 @@ using Progress.Descriptors; using Progress.Samples; -var onStatsNotified = (Stats stats) => +var onProgress = (Stats stats) => { - // TODO: Implement collect + // TODO: Do something useful +}; + +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful }; using var reporter = new ReporterBuilder() @@ -14,7 +19,8 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() - .NotifyingStats(onStatsNotified) + .NotifyingProgress(onProgress) + .NotifyingCompletion(onCompletion) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(BarDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.Samples.HearthBeat.App/Program.cs b/src/Progress.Samples.HearthBeat.App/Program.cs index c473cac..5a0e56d 100644 --- a/src/Progress.Samples.HearthBeat.App/Program.cs +++ b/src/Progress.Samples.HearthBeat.App/Program.cs @@ -2,9 +2,14 @@ using Progress.Descriptors; using Progress.Samples; -var onStatsNotified = (Stats stats) => +var onProgress = (Stats stats) => { - // TODO: Implement collect + // TODO: Do something useful +}; + +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful }; using var reporter = new ReporterBuilder() @@ -14,7 +19,8 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() - .NotifyingStats(onStatsNotified) + .NotifyingProgress(onProgress) + .NotifyingCompletion(onCompletion) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(HearthBeatDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.Samples.Pulse.App/Program.cs b/src/Progress.Samples.Pulse.App/Program.cs index 39bf374..3bb2dab 100644 --- a/src/Progress.Samples.Pulse.App/Program.cs +++ b/src/Progress.Samples.Pulse.App/Program.cs @@ -2,11 +2,17 @@ using Progress.Descriptors; using Progress.Samples; -var onStatsNotified = (Stats stats) => +var onProgress = (Stats stats) => { - // TODO: Implement collect + // TODO: Do something useful }; +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful +}; + + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -14,7 +20,8 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() - .NotifyingStats(onStatsNotified) + .NotifyingProgress(onProgress) + .NotifyingCompletion(onCompletion) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(PulseDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.Samples.Spinner.App/Program.cs b/src/Progress.Samples.Spinner.App/Program.cs index 3b8e0bb..d593589 100644 --- a/src/Progress.Samples.Spinner.App/Program.cs +++ b/src/Progress.Samples.Spinner.App/Program.cs @@ -2,11 +2,17 @@ using Progress.Descriptors; using Progress.Samples; -var onStatsNotified = (Stats stats) => +var onProgress = (Stats stats) => { - // TODO: Implement collect + // TODO: Do something useful }; +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful +}; + + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -14,7 +20,8 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() - .NotifyingStats(onStatsNotified) + .NotifyingProgress(onProgress) + .NotifyingCompletion(onCompletion) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(SpinnerDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.UnitTest/ReporterBuilderTests.cs b/src/Progress.UnitTest/ReporterBuilderTests.cs index b92d03c..8c7340c 100644 --- a/src/Progress.UnitTest/ReporterBuilderTests.cs +++ b/src/Progress.UnitTest/ReporterBuilderTests.cs @@ -100,37 +100,54 @@ public void GivenSummary_WhenBuilding_ThenReporterIsSetUp() } [Fact] - public void GivenStatsNotifications_WhenBuilding_ThenReporterIsSetUp() + public void GivenProgressNotifications_WhenBuilding_ThenReporterIsSetUp() { // Arrange var callback = (Stats stats) => { }; var builder = new ReporterBuilder() - .NotifyingStats(callback); + .NotifyingProgress(callback); // Act var actual = builder.Build(100); // Arrange - actual.OnStatsNotified.Should().NotBeNull(); - actual.OnStatsNotified.Should().Be(callback); + actual.OnProgress.Should().NotBeNull(); + actual.OnProgress.Should().Be(callback); actual.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); } [Fact] - public void GivenStatsNotificationsWithFrequency_WhenBuilding_ThenReporterIsSetUp() + public void GivenProgressNotificationsWithFrequency_WhenBuilding_ThenReporterIsSetUp() { // Arrange var callback = (Stats stats) => { }; var frequency = TimeSpan.FromSeconds(20); var builder = new ReporterBuilder() - .NotifyingStats(callback, frequency); + .NotifyingProgress(callback, frequency); // Act var actual = builder.Build(100); // Arrange - actual.OnStatsNotified.Should().NotBeNull(); - actual.OnStatsNotified.Should().Be(callback); + actual.OnProgress.Should().NotBeNull(); + actual.OnProgress.Should().Be(callback); actual.StatsFrequency.Should().Be(frequency); } + + [Fact] + public void GivenCompletionNotifications_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var callback = (Stats stats) => { }; + var builder = new ReporterBuilder() + .NotifyingCompletion(callback); + + // Act + var actual = builder.Build(100); + + // Arrange + actual.OnCompletion.Should().NotBeNull(); + actual.OnCompletion.Should().Be(callback); + } + } diff --git a/src/Progress.UnitTest/ReporterTests.cs b/src/Progress.UnitTest/ReporterTests.cs index 9444cba..6b0ddd1 100644 --- a/src/Progress.UnitTest/ReporterTests.cs +++ b/src/Progress.UnitTest/ReporterTests.cs @@ -25,9 +25,12 @@ public void GivenDefaults_WhenInitializing_ThenExpectedSettings() reporter.DisplayEstimatedTimeOfArrival.Should().BeTrue(); reporter.DisplayItemsOverview.Should().BeTrue(); reporter.DisplayItemsSummary.Should().BeTrue(); - reporter.NotifyStats.Should().BeTrue(); + reporter.NotifyProgressStats.Should().BeTrue(); + reporter.NotifyCompletionStats.Should().BeTrue(); reporter.ReportFrequency.Should().Be(TimeSpan.FromSeconds(1)); reporter.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); + reporter.OnProgress.Should().BeNull(); + reporter.OnCompletion.Should().BeNull(); } [Fact] @@ -112,4 +115,47 @@ public void GivenStoppedOperation_WhenResuming_ThenSuccess() // Act reporter.Resume(); } + + [Fact] + public async Task GivenProgressNotifications_WhenRunning_ThenCallbackIsCalled() + { + // Arrange + bool isCalled = false; + var reporter = new Reporter(100, BarDescriptor.Default.Build()) + { + NotifyProgressStats = true, + StatsFrequency = TimeSpan.FromSeconds(1), + OnProgress = (stats) => isCalled = true + }; + + + // Act + reporter.Start(); + await Task.Delay(TimeSpan.FromMilliseconds(1500)); + + // Assert + isCalled.Should().BeTrue(); + } + + [Fact] + public async Task GivenCompletionNotifications_WhenFinished_ThenCallbackIsCalled() + { + // Arrange + bool isCalled = false; + var reporter = new Reporter(1, BarDescriptor.Default.Build()) + { + NotifyCompletionStats = true, + ReportFrequency = TimeSpan.FromMicroseconds(500), + OnCompletion = (stats) => isCalled = true + }; + + reporter.Start(); + + // Act + reporter.ReportSuccess(); + await Task.Delay(TimeSpan.FromMilliseconds(1000)); + + // Assert + isCalled.Should().BeTrue(); + } } \ No newline at end of file diff --git a/src/Progress/Content/README.md b/src/Progress/Content/README.md index b0e74ad..4f5f387 100644 --- a/src/Progress/Content/README.md +++ b/src/Progress/Content/README.md @@ -7,12 +7,6 @@ Report progression with ease by using multilple components such as Bars, Spinner ```csharp using var reporter = new ReporterBuilder() - .DisplayingStartingTime() - .DisplayingElapsedTime() - .DisplayingTimeOfArrival() - .DisplayingRemainingTime() - .DisplayingItemsSummary() - .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(BarDescriptor.Default) .Build(Worker.AllItems); @@ -49,6 +43,8 @@ var reporter = new ReporterBuilder() .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() + .NotifyingProgress(onProgress) + .NotifyingCompletion(onCompletion) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(BarDescriptor.Default) .Build(Worker.AllItems); @@ -74,3 +70,21 @@ reporter.Resume(); reporter.ReportSuccess() reporter.ReportFailure() ``` + +### Collect stats while running +```csharp +var onProgress = (Stats stats) => +{ + // TODO: Do something useful +}; + +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful +}; + +var reporter = new ReporterBuilder() + .NotifyingProgress(onProgress) + .NotifyingCompletion(onCompletion) + .Build(Worker.AllItems); +``` diff --git a/src/Progress/Progress.csproj b/src/Progress/Progress.csproj index 7a4bfbc..4827469 100644 --- a/src/Progress/Progress.csproj +++ b/src/Progress/Progress.csproj @@ -9,7 +9,7 @@ Progress - 1.2.0 + 1.3.0 MIT Gerard Castello Report progress with ease diff --git a/src/Progress/Reporter.cs b/src/Progress/Reporter.cs index b31d9b3..5b446f7 100644 --- a/src/Progress/Reporter.cs +++ b/src/Progress/Reporter.cs @@ -30,9 +30,14 @@ public class Reporter : IDisposable public bool IsFinished => CurrentCount == _itemsCount; /// - /// Gets or sets the stats notification hook. + /// Gets or sets the progress notification hook which invocation happens during the operation. /// - public Action OnStatsNotified { get; set; } = default!; + public Action? OnProgress { get; set; } = null!; + + /// + /// Gets or sets the completion hook which invocation happens at the end of the operation. + /// + public Action? OnCompletion { get; set; } = null!; internal bool DisplayEstimatedTimeOfArrival { get; set; } = true; internal bool DisplayRemainingTime { get; set; } = true; @@ -40,7 +45,8 @@ public class Reporter : IDisposable internal bool DisplayStartingTime { get; set; } = true; internal bool DisplayItemsOverview { get; set; } = true; internal bool DisplayItemsSummary { get; set; } = true; - internal bool NotifyStats { get; set; } = true; + internal bool NotifyProgressStats { get; set; } = true; + internal bool NotifyCompletionStats { get; set; } = true; internal TimeSpan ReportFrequency { get; set; } = TimeSpan.FromSeconds(1); internal TimeSpan StatsFrequency { get; set; } = TimeSpan.FromSeconds(5); @@ -50,6 +56,7 @@ public class Reporter : IDisposable private string UnsuccessfulItems => _failureCount.ToString().PadLeft(10); private ulong CurrentCount => _successCount + _failureCount; + internal Reporter(ulong itemsCount, Component component) { if (itemsCount == 0) @@ -90,7 +97,7 @@ public void Start() _reportingThread = DoWork(); _reportingThread.Start(); - if (NotifyStats) + if (NotifyProgressStats) { _statsThread = DoStats(); _statsThread.Start(); @@ -131,7 +138,7 @@ public void Resume() _reportingThread = DoWork(); _reportingThread.Start(); - if (NotifyStats) + if (NotifyProgressStats) { _statsThread = DoStats(); _statsThread.Start(); @@ -241,23 +248,11 @@ private void Display() private void ReportStats() { - if (OnStatsNotified == null || DateTimeOffset.UtcNow - _lastStatsNotification < StatsFrequency) + if (OnProgress == null || DateTimeOffset.UtcNow - _lastStatsNotification < StatsFrequency) return; - var stats = new Stats - { - StartedOn = _timer.StartedOn, - ElapsedTime = _timer.ElapsedTime, - RemainingTime = _timer.GetRemainingTime(_component.CurrentPercent.Value), - EstTimeOfArrival = _timer.GetEstimatedTimeOfArrival(_component.CurrentPercent.Value), - ExpectedItems = _itemsCount, - CurrentCount = CurrentCount, - SuccessCount = _successCount, - FailureCount = _failureCount, - CurrentPercent = _component.CurrentPercent.Value, - }; - - OnStatsNotified.Invoke(stats); + var stats = CollectStats(); + OnProgress.Invoke(stats); _lastStatsNotification = DateTimeOffset.UtcNow; } @@ -273,6 +268,12 @@ private Thread DoWork() while (!IsFinished && !_cancellationToken.IsCancellationRequested); Display(); + + if (IsFinished && NotifyCompletionStats) + { + var stats = CollectStats(); + OnCompletion?.Invoke(stats); + } }); return new(tStart); @@ -292,4 +293,20 @@ private Thread DoStats() return new(tStart); } + + private Stats CollectStats() + { + return new() + { + StartedOn = _timer.StartedOn, + ElapsedTime = _timer.ElapsedTime, + RemainingTime = _timer.GetRemainingTime(_component.CurrentPercent.Value), + EstTimeOfArrival = _timer.GetEstimatedTimeOfArrival(_component.CurrentPercent.Value), + ExpectedItems = _itemsCount, + CurrentCount = CurrentCount, + SuccessCount = _successCount, + FailureCount = _failureCount, + CurrentPercent = _component.CurrentPercent.Value, + }; + } } diff --git a/src/Progress/ReporterBuilder.cs b/src/Progress/ReporterBuilder.cs index d7dfc72..83547bb 100644 --- a/src/Progress/ReporterBuilder.cs +++ b/src/Progress/ReporterBuilder.cs @@ -14,11 +14,11 @@ public class ReporterBuilder private bool _displayStartingTime; private bool _displayItemsOverview; private bool _displayItemsSummary; - private bool _notifyingStats; private TimeSpan _reportFrequency = TimeSpan.FromSeconds(1); private TimeSpan _statsFrequency = TimeSpan.FromSeconds(5); private ComponentDescriptor _componentDescriptor = BarDescriptor.Default; - private Action _onStatsNotified = default!; + private Action _onProgressNotified = default!; + private Action _onCompletionNotified = default!; /// /// The reporter will display the elapsed time since the start. @@ -104,27 +104,37 @@ public ReporterBuilder UsingComponentDescriptor(ComponentDescriptor descriptor) } /// - /// Sets the stats notification callback. + /// Sets the progress notification callback. /// Default notification frequency is set to 5 seconds. /// /// /// - public ReporterBuilder NotifyingStats(Action callback) + public ReporterBuilder NotifyingProgress(Action callback) { - return NotifyingStats(callback, _statsFrequency); + return NotifyingProgress(callback, _statsFrequency); } /// - /// Sets the stats notification callback with the invocation frequency . + /// Sets the progress notification callback with the invocation frequency. /// /// /// /// - public ReporterBuilder NotifyingStats(Action callback, TimeSpan statsFrequency) + public ReporterBuilder NotifyingProgress(Action callback, TimeSpan statsFrequency) { - _onStatsNotified = callback; + _onProgressNotified = callback; _statsFrequency = statsFrequency; - _notifyingStats = true; + return this; + } + + /// + /// Sets the completion notification callback. + /// + /// + /// + public ReporterBuilder NotifyingCompletion(Action callback) + { + _onCompletionNotified = callback; return this; } @@ -149,10 +159,12 @@ public Reporter Build(ulong itemsCount) DisplayStartingTime = _displayStartingTime, DisplayItemsOverview = _displayItemsOverview, DisplayItemsSummary = _displayItemsSummary, - NotifyStats = _notifyingStats, + NotifyProgressStats = _onProgressNotified != null, + NotifyCompletionStats = _onCompletionNotified != null, ReportFrequency = _reportFrequency, StatsFrequency = _statsFrequency, - OnStatsNotified = _onStatsNotified, + OnProgress = _onProgressNotified, + OnCompletion = _onCompletionNotified }; } }