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 e575b27..e8bd44c 100644 --- a/src/Progress.Samples.Bar.App/Program.cs +++ b/src/Progress.Samples.Bar.App/Program.cs @@ -2,6 +2,16 @@ using Progress.Descriptors; using Progress.Samples; +var onProgress = (Stats stats) => +{ + // TODO: Do something useful +}; + +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful +}; + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -9,6 +19,8 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() + .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 21f50e8..5a0e56d 100644 --- a/src/Progress.Samples.HearthBeat.App/Program.cs +++ b/src/Progress.Samples.HearthBeat.App/Program.cs @@ -2,6 +2,16 @@ using Progress.Descriptors; using Progress.Samples; +var onProgress = (Stats stats) => +{ + // TODO: Do something useful +}; + +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful +}; + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -9,6 +19,8 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() + .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 7757123..3bb2dab 100644 --- a/src/Progress.Samples.Pulse.App/Program.cs +++ b/src/Progress.Samples.Pulse.App/Program.cs @@ -2,6 +2,17 @@ using Progress.Descriptors; using Progress.Samples; +var onProgress = (Stats stats) => +{ + // TODO: Do something useful +}; + +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful +}; + + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -9,6 +20,8 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() + .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 f689c62..d593589 100644 --- a/src/Progress.Samples.Spinner.App/Program.cs +++ b/src/Progress.Samples.Spinner.App/Program.cs @@ -2,6 +2,17 @@ using Progress.Descriptors; using Progress.Samples; +var onProgress = (Stats stats) => +{ + // TODO: Do something useful +}; + +var onCompletion = (Stats stats) => +{ + // TODO: Do something useful +}; + + using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() @@ -9,6 +20,8 @@ .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() + .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 6c02bbd..8c7340c 100644 --- a/src/Progress.UnitTest/ReporterBuilderTests.cs +++ b/src/Progress.UnitTest/ReporterBuilderTests.cs @@ -98,4 +98,56 @@ public void GivenSummary_WhenBuilding_ThenReporterIsSetUp() // Assert actual.DisplayItemsSummary.Should().BeTrue(); } + + [Fact] + public void GivenProgressNotifications_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var callback = (Stats stats) => { }; + var builder = new ReporterBuilder() + .NotifyingProgress(callback); + + // Act + var actual = builder.Build(100); + + // Arrange + actual.OnProgress.Should().NotBeNull(); + actual.OnProgress.Should().Be(callback); + actual.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); + } + + [Fact] + public void GivenProgressNotificationsWithFrequency_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var callback = (Stats stats) => { }; + var frequency = TimeSpan.FromSeconds(20); + var builder = new ReporterBuilder() + .NotifyingProgress(callback, frequency); + + // Act + var actual = builder.Build(100); + + // Arrange + 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 fbdcd97..6b0ddd1 100644 --- a/src/Progress.UnitTest/ReporterTests.cs +++ b/src/Progress.UnitTest/ReporterTests.cs @@ -7,11 +7,32 @@ 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.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] public void GivenNothingToComplete_WhenInitializing_ThenThrowsException() { @@ -94,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 cb629a9..5b446f7 100644 --- a/src/Progress/Reporter.cs +++ b/src/Progress/Reporter.cs @@ -19,27 +19,44 @@ 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 progress notification hook which invocation happens during the operation. + /// + 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; 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 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); private string AllItems => (_successCount + _failureCount).ToString().PadLeft(10); private string SuccessfulItems => _successCount.ToString().PadLeft(10); private string UnsuccessfulItems => _failureCount.ToString().PadLeft(10); private ulong CurrentCount => _successCount + _failureCount; + internal Reporter(ulong itemsCount, Component component) { if (itemsCount == 0) @@ -73,11 +90,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 (NotifyProgressStats) + { + _statsThread = DoStats(); + _statsThread.Start(); + } } /// @@ -93,6 +117,9 @@ public void Stop() if (_reportingThread != null && _reportingThread.IsAlive) _reportingThread.Join(); + + if (_statsThread != null && _statsThread.IsAlive) + _statsThread.Join(); } /// @@ -110,6 +137,12 @@ public void Resume() _cancellationToken = _cancellationTokenSource.Token; _reportingThread = DoWork(); _reportingThread.Start(); + + if (NotifyProgressStats) + { + _statsThread = DoStats(); + _statsThread.Start(); + } } /// @@ -213,6 +246,16 @@ private void Display() stream.Flush(); } + private void ReportStats() + { + if (OnProgress == null || DateTimeOffset.UtcNow - _lastStatsNotification < StatsFrequency) + return; + + var stats = CollectStats(); + OnProgress.Invoke(stats); + _lastStatsNotification = DateTimeOffset.UtcNow; + } + private Thread DoWork() { var tStart = new ThreadStart(() => @@ -225,8 +268,45 @@ private Thread DoWork() while (!IsFinished && !_cancellationToken.IsCancellationRequested); Display(); + + if (IsFinished && NotifyCompletionStats) + { + var stats = CollectStats(); + OnCompletion?.Invoke(stats); + } + }); + + return new(tStart); + } + + private Thread DoStats() + { + var tStart = new ThreadStart(() => + { + do + { + ReportStats(); + Thread.Sleep(StatsFrequency); + } + while (!IsFinished && !_cancellationToken.IsCancellationRequested); }); 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 376cc45..83547bb 100644 --- a/src/Progress/ReporterBuilder.cs +++ b/src/Progress/ReporterBuilder.cs @@ -15,7 +15,10 @@ public class ReporterBuilder private bool _displayItemsOverview; private bool _displayItemsSummary; private TimeSpan _reportFrequency = TimeSpan.FromSeconds(1); + private TimeSpan _statsFrequency = TimeSpan.FromSeconds(5); private ComponentDescriptor _componentDescriptor = BarDescriptor.Default; + private Action _onProgressNotified = default!; + private Action _onCompletionNotified = default!; /// /// The reporter will display the elapsed time since the start. @@ -100,6 +103,41 @@ public ReporterBuilder UsingComponentDescriptor(ComponentDescriptor descriptor) return this; } + /// + /// Sets the progress notification callback. + /// Default notification frequency is set to 5 seconds. + /// + /// + /// + public ReporterBuilder NotifyingProgress(Action callback) + { + return NotifyingProgress(callback, _statsFrequency); + } + + /// + /// Sets the progress notification callback with the invocation frequency. + /// + /// + /// + /// + public ReporterBuilder NotifyingProgress(Action callback, TimeSpan statsFrequency) + { + _onProgressNotified = callback; + _statsFrequency = statsFrequency; + return this; + } + + /// + /// Sets the completion notification callback. + /// + /// + /// + public ReporterBuilder NotifyingCompletion(Action callback) + { + _onCompletionNotified = callback; + return this; + } + /// /// Builds the reporte getting an instance of . /// @@ -121,7 +159,12 @@ public Reporter Build(ulong itemsCount) DisplayStartingTime = _displayStartingTime, DisplayItemsOverview = _displayItemsOverview, DisplayItemsSummary = _displayItemsSummary, + NotifyProgressStats = _onProgressNotified != null, + NotifyCompletionStats = _onCompletionNotified != null, ReportFrequency = _reportFrequency, + StatsFrequency = _statsFrequency, + OnProgress = _onProgressNotified, + OnCompletion = _onCompletionNotified }; } } 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; } +}