diff --git a/.gitignore b/.gitignore index f178e11..a0cee26 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ bld/ [Ll]og/ [Ll]ogs/ .vs/ +.vscode/ # .NET Core project.lock.json diff --git a/README.md b/README.md index 220216b..a0fc3b7 100644 --- a/README.md +++ b/README.md @@ -3,75 +3,42 @@ [![NuGet version (Progress)](https://img.shields.io/nuget/v/Progress.svg?style=square)](https://www.nuget.org/packages/Progress/) -This library helps you to spin up reporters for giving an overview of a workload's progression. These reporters can either be for console apps that output the information or background jobs hosted by APIs handling the progress via hooks. +This library provides a set of features that help you spin up reporting tasks, either in the form of console apps that show workloads progression or via background jobs providing access to stats during the lifespan of the workload. -![Progress](img/logo512x512.png) +Please read the documentation for further info about how to interact with the library's API here: +[Library API](./src/Progress/Content/README.md) -## Report progress for console apps -Report progression with ease by using multilple components such as Bars, Spinners, Pulse and more. +## Reporters +The reporter is the key component that orchestrates, provides and display information about the workloads progression. The following reporters are currently supported: -```csharp -using var reporter = new ConsoleReporterBuilder() - .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) - .UsingComponentDescriptor(BarDescriptor.Default) - .Build(Worker.AllItems); +|Reporter| Description| +|---|---| +| **ConsoleReporter** | Reports progression of a single workload. Console based | +| **ConsoleAggregateReporter** | Reports progression of many workloads. Console based | +| **BackgroundReporter** | Reports progression of a single workload in background. Used in HostedService or other background jobs that don't require a std output | -var worker = new Worker() -{ - OnSuccess = () => reporter.ReportSuccess(), - OnFailure = () => reporter.ReportFailure(), -}; +## Supported features +The following are the supported features: -reporter.Start(); -await worker.DoMywork(); -``` +| Feature | Description | Reporters | +| ---| --- | --- | +| On progress stats | Provides stats info during the reporting process | ConsoleReporter, ConsoleAggregateReporter, BackgroundRepoerter | +| On completeion stats | Provides stats info once the reporting finishes | ConsoleReporter, ConsoleAggregateReporter, BackgroundRepoerter | +| Exports | Exports the final stats to: json, txt, csv or xml | ConsoleReporter, ConsoleAggregateReporter, BackgroundRepoerter | +| Display data points | Display important data points such as: starting time, ETA, elaspsed time, remaining time, success, failures and total hits | ConsoleReporter, ConsoleAggregateReporter | +| Display workload progression | Display progress with different types of components | ConsoleReporter, ConsoleAggregateReporter | -### Define the component to show progress -Components are subjected to the ConsoleReporter. +## Components +The components are meant for console-based reporters, aiming to display progression during the lifespan of the workload. When using the *ConsoleAggregateReporter* one can choose and use different components or use the same for all workloads. -```csharp -var descriptor = new BarDescriptor() - .UsingProgressSymbol('#') - .UsingWidth(50) - .DisplayingPercent(); -``` +### Progress bars +![Progress bars](img/consoleAggregateBarReporter.gif) -or use the default one -```csharp -var descriptor = BarDescriptor.Default; -``` +### Spinners +![Spinners](img/consoleAggregateSpinnerReporter.gif) -### Define the reporter to use the component -```csharp -var reporter = new ConsoleReporterBuilder() - .DisplayingStartingTime() - .DisplayingElapsedTime() - .DisplayingTimeOfArrival() - .DisplayingRemainingTime() - .DisplayingItemsSummary() - .DisplayingItemsOverview() - .NotifyingProgress(onProgress) - .NotifyingCompletion(onCompletion) - .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) - .UsingComponentDescriptor(descriptor) - .Build(Worker.AllItems); -``` +### Pulse +![Progress bars](img/consolePulseReporter.gif) -## Report progress for background jobs -In case your workload happens in the background and you only pretend to collect progression status during certain moments of the execution, or export the results at the end, the BackgroundReporter may help you to remove all the boilerplate. - -```csharp -_reporter = new BackgroundReporterBuilder() - .NotifyingProgress((stats) => - { - logger.LogDebug("Getting stats on progress {percent}", stats.CurrentPercent); - }) - .NotifyingCompletion((stats) => - { - logger.LogDebug("Getting stats on completion"); - }) - .Build(Worker.AllItems); - -_worker.OnSuccess = () => _reporter.ReportSuccess(); -_worker.OnFailure = () => _reporter.ReportFailure(); -``` \ No newline at end of file +### Hearthbeat +![Hearthbeat](img/consoleHearthbeatReporter.gif) diff --git a/img/consoleAggregateBarReporter.gif b/img/consoleAggregateBarReporter.gif new file mode 100644 index 0000000..bd3c6a3 Binary files /dev/null and b/img/consoleAggregateBarReporter.gif differ diff --git a/img/consoleAggregateSpinnerReporter.gif b/img/consoleAggregateSpinnerReporter.gif new file mode 100644 index 0000000..2ab0c8d Binary files /dev/null and b/img/consoleAggregateSpinnerReporter.gif differ diff --git a/img/consoleHearthbeatReporter.gif b/img/consoleHearthbeatReporter.gif new file mode 100644 index 0000000..2528cee Binary files /dev/null and b/img/consoleHearthbeatReporter.gif differ diff --git a/img/consolePulseReporter.gif b/img/consolePulseReporter.gif new file mode 100644 index 0000000..571c252 Binary files /dev/null and b/img/consolePulseReporter.gif differ diff --git a/src/Progress.UnitTest/Builders/ConsoleAggregateReporterBuilderTests.cs b/src/Progress.UnitTest/Builders/ConsoleAggregateReporterBuilderTests.cs new file mode 100644 index 0000000..f1991c3 --- /dev/null +++ b/src/Progress.UnitTest/Builders/ConsoleAggregateReporterBuilderTests.cs @@ -0,0 +1,203 @@ +using Progress.Builders; +using Progress.Descriptors; +using Progress.Reporters; +using Progress.Settings; + +namespace Progress.UnitTest.Builders; + +public class ConsoleAggregateReporterBuilderTests +{ + [Fact] + public void GivenNothingToComplete_WhenBuilding_ThenThrowsException() + { + // Arrange + var builder = new ConsoleAggregateReporterBuilder(); + + // Act + var action = () => builder.Build(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void GivenSomethingToComplete_WhenBuilding_ThenReturnsReporter() + { + // Arrange + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default); + + // Act + var actual = builder.Build(); + + // Assert + actual.Should().NotBeNull(); + } + + [Fact] + public void GivenReportingFrequency_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var expectedFrequency = TimeSpan.FromSeconds(20); + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .UsingReportingFrequency(expectedFrequency); + + // Act + var actual = builder.Build().Configuration; + + // Assert + actual.ReportFrequency.Should().Be(expectedFrequency); + } + + [Fact] + public void GivenElapsedTime_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .DisplayingElapsedTime(); + + // Act + var actual = builder.Build().Configuration.Options; + + // Assert + actual.DisplayElapsedTime.Should().BeTrue(); + } + + [Fact] + public void GivenStartingTime_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .DisplayingStartingTime(); + + // Act + var actual = builder.Build().Configuration.Options; + + // Assert + actual.DisplayStartingTime.Should().BeTrue(); + } + + [Fact] + public void GivenItemsOverview_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .DisplayingItemsOverview(); + + // Act + var actual = builder.Build().Configuration.Options; + + // Assert + actual.DisplayItemsOverview.Should().BeTrue(); + } + + [Fact] + public void GivenSummary_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .DisplayingItemsSummary(); + + // Act + var actual = builder.Build().Configuration.Options; + + // Assert + actual.DisplayItemsSummary.Should().BeTrue(); + } + + [Fact] + public void GivenProgressNotifications_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var callback = (Stats stats) => { }; + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .NotifyingProgress(callback); + + // Act + var actual = builder.Build(); + + // Arrange + actual.OnProgress.Should().NotBeNull(); + actual.OnProgress.Should().Be(callback); + actual.Configuration.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 ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .NotifyingProgress(callback, frequency); + + // Act + var actual = builder.Build(); + + // Arrange + actual.OnProgress.Should().NotBeNull(); + actual.OnProgress.Should().Be(callback); + actual.Configuration.StatsFrequency.Should().Be(frequency); + actual.Configuration.Options.NotifyProgressStats.Should().BeTrue(); + } + + [Fact] + public void GivenCompletionNotifications_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var callback = (Stats stats) => { }; + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .NotifyingCompletion(callback); + + // Act + var actual = builder.Build(); + + // Arrange + actual.OnCompletion.Should().NotBeNull(); + actual.OnCompletion.Should().Be(callback); + actual.Configuration.Options.NotifyCompletionStats.Should().BeTrue(); + } + + [Fact] + public void GivenExporting_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + const string fileName = "output.txt"; + const FileType type = FileType.Text; + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .ExportingTo(fileName, type); + + // Act + var actual = builder.Build(); + + // Assert + actual.Configuration.ExportSettings.Should().NotBeNull(); + actual.Configuration.ExportSettings.FileName.Should().Be(fileName); + actual.Configuration.ExportSettings.FileType.Should().Be(FileType.Text); + actual.Configuration.Options.ExportCompletionStats.Should().BeTrue(); + } + + [Fact] + public void GivenHideOnComplete_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var builder = new ConsoleAggregateReporterBuilder() + .UsingWorkload(Workload.Default(100), BarDescriptor.Default) + .HideWorkloadOnComplete(true); + + // Act + var actual = builder.Build().Configuration.Options; + + // Assert + actual.HideWorkflowOnComplete.Should().BeTrue(); + } +} diff --git a/src/Progress.UnitTest/Builders/ConsoleReporterBuilderTests.cs b/src/Progress.UnitTest/Builders/ConsoleReporterBuilderTests.cs index df0c456..44feb08 100644 --- a/src/Progress.UnitTest/Builders/ConsoleReporterBuilderTests.cs +++ b/src/Progress.UnitTest/Builders/ConsoleReporterBuilderTests.cs @@ -9,10 +9,11 @@ public class ConsoleReporterBuilderTests public void GivenNothingToComplete_WhenBuilding_ThenThrowsException() { // Arrange - var builder = new ConsoleReporterBuilder(); + var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(0); // Act - var action = () => builder.Build(0); + var action = () => builder.Build(); // Assert action.Should().Throw(); @@ -22,10 +23,11 @@ public void GivenNothingToComplete_WhenBuilding_ThenThrowsException() public void GivenSomethingToComplete_WhenBuilding_ThenReturnsReporter() { // Arrange - var builder = new ConsoleReporterBuilder(); + var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100); // Act - var actual = builder.Build(100); + var actual = builder.Build(); // Assert actual.Should().NotBeNull(); @@ -37,10 +39,11 @@ public void GivenReportingFrequency_WhenBuilding_ThenReporterIsSetUp() // Arrange var expectedFrequency = TimeSpan.FromSeconds(20); var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) .UsingReportingFrequency(expectedFrequency); // Act - var actual = builder.Build(100).Configuration; + var actual = builder.Build().Configuration; // Assert actual.ReportFrequency.Should().Be(expectedFrequency); @@ -51,10 +54,11 @@ public void GivenElapsedTime_WhenBuilding_ThenReporterIsSetUp() { // Arrange var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) .DisplayingElapsedTime(); // Act - var actual = builder.Build(100).Configuration.Options; + var actual = builder.Build().Configuration.Options; // Assert actual.DisplayElapsedTime.Should().BeTrue(); @@ -65,10 +69,11 @@ public void GivenStartingTime_WhenBuilding_ThenReporterIsSetUp() { // Arrange var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) .DisplayingStartingTime(); // Act - var actual = builder.Build(100).Configuration.Options; + var actual = builder.Build().Configuration.Options; // Assert actual.DisplayStartingTime.Should().BeTrue(); @@ -79,10 +84,11 @@ public void GivenItemsOverview_WhenBuilding_ThenReporterIsSetUp() { // Arrange var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) .DisplayingItemsOverview(); // Act - var actual = builder.Build(100).Configuration.Options; + var actual = builder.Build().Configuration.Options; // Assert actual.DisplayItemsOverview.Should().BeTrue(); @@ -93,10 +99,11 @@ public void GivenSummary_WhenBuilding_ThenReporterIsSetUp() { // Arrange var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) .DisplayingItemsSummary(); // Act - var actual = builder.Build(100).Configuration.Options; + var actual = builder.Build().Configuration.Options; // Assert actual.DisplayItemsSummary.Should().BeTrue(); @@ -108,10 +115,11 @@ public void GivenProgressNotifications_WhenBuilding_ThenReporterIsSetUp() // Arrange var callback = (Stats stats) => { }; var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) .NotifyingProgress(callback); // Act - var actual = builder.Build(100); + var actual = builder.Build(); // Arrange actual.OnProgress.Should().NotBeNull(); @@ -126,10 +134,11 @@ public void GivenProgressNotificationsWithFrequency_WhenBuilding_ThenReporterIsS var callback = (Stats stats) => { }; var frequency = TimeSpan.FromSeconds(20); var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) .NotifyingProgress(callback, frequency); // Act - var actual = builder.Build(100); + var actual = builder.Build(); // Arrange actual.OnProgress.Should().NotBeNull(); @@ -144,10 +153,11 @@ public void GivenCompletionNotifications_WhenBuilding_ThenReporterIsSetUp() // Arrange var callback = (Stats stats) => { }; var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) .NotifyingCompletion(callback); // Act - var actual = builder.Build(100); + var actual = builder.Build(); // Arrange actual.OnCompletion.Should().NotBeNull(); @@ -162,10 +172,11 @@ public void GivenExporting_WhenBuilding_ThenReporterIsSetUp() const string fileName = "output.txt"; const FileType type = FileType.Text; var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) .ExportingTo(fileName, type); // Act - var actual = builder.Build(100); + var actual = builder.Build(); // Assert actual.Configuration.ExportSettings.Should().NotBeNull(); @@ -173,4 +184,19 @@ public void GivenExporting_WhenBuilding_ThenReporterIsSetUp() actual.Configuration.ExportSettings.FileType.Should().Be(FileType.Text); actual.Configuration.Options.ExportCompletionStats.Should().BeTrue(); } + + [Fact] + public void GivenHideOnComplete_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + var builder = new ConsoleReporterBuilder() + .UsingExpectedItems(100) + .HideWorkloadOnComplete(true); + + // Act + var actual = builder.Build().Configuration.Options; + + // Assert + actual.HideWorkflowOnComplete.Should().BeTrue(); + } } diff --git a/src/Progress.UnitTest/PrinterTests.cs b/src/Progress.UnitTest/PrinterTests.cs index a80db71..aa993e7 100644 --- a/src/Progress.UnitTest/PrinterTests.cs +++ b/src/Progress.UnitTest/PrinterTests.cs @@ -1,4 +1,6 @@ -using Progress.Descriptors; +using Progress.Components; +using Progress.Descriptors; +using Progress.Reporters; namespace Progress.UnitTest; @@ -8,7 +10,9 @@ public class PrinterTests public void GivenDefaults_WhenPrinting_ThenGetsOutput() { // Arrange - Printer printer = new(new Settings.Console.ReportingOptions(), BarDescriptor.Default.Build()); + var workload = Workload.Default(10); + workload.Component = BarDescriptor.Default.Build(); + Printer printer = new(new Settings.Console.ReportingOptions(), [ workload ]); Stats stats = new(); // Act diff --git a/src/Progress.UnitTest/Reporters/ConsoleAggregateReporterTests.cs b/src/Progress.UnitTest/Reporters/ConsoleAggregateReporterTests.cs new file mode 100644 index 0000000..8d3449f --- /dev/null +++ b/src/Progress.UnitTest/Reporters/ConsoleAggregateReporterTests.cs @@ -0,0 +1,195 @@ +using Progress.Descriptors; +using Progress.Reporters; +using System.Text; + +namespace Progress.UnitTest.Reporters; + +public class ConsoleAggregateReporterTests +{ + public ConsoleAggregateReporterTests() + { + StringBuilder builder = new(); + TextWriter writer = new StringWriter(builder); + Console.SetOut(writer); + } + + [Fact] + public void GivenDefaults_WhenInitializing_ThenExpectedSettings() + { + // Arrange + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleAggregateReporter([workload]); + + // Assert + reporter.Configuration.Options.DisplayStartingTime.Should().BeTrue(); + reporter.Configuration.Options.DisplayRemainingTime.Should().BeTrue(); + reporter.Configuration.Options.DisplayElapsedTime.Should().BeTrue(); + reporter.Configuration.Options.DisplayEstimatedTimeOfArrival.Should().BeTrue(); + reporter.Configuration.Options.DisplayItemsOverview.Should().BeTrue(); + reporter.Configuration.Options.DisplayItemsSummary.Should().BeTrue(); + reporter.Configuration.Options.HideWorkflowOnComplete.Should().BeFalse(); + reporter.Configuration.Options.NotifyProgressStats.Should().BeFalse(); + reporter.Configuration.Options.NotifyCompletionStats.Should().BeFalse(); + reporter.Configuration.Options.ExportCompletionStats.Should().BeFalse(); + reporter.Configuration.ReportFrequency.Should().Be(TimeSpan.FromSeconds(1)); + reporter.Configuration.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); + reporter.Configuration.ExportSettings.Should().BeNull(); + reporter.OnProgress.Should().BeNull(); + reporter.OnCompletion.Should().BeNull(); + } + + [Fact] + public void GivenNothingToComplete_WhenInitializing_ThenThrowsException() + { + // Act + var workload = Workload.Default(0); + workload.Component = BarDescriptor.Default.Build(); + var action = () => new ConsoleAggregateReporter([workload]); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void GivenNoCollectionOfWorkflows_WhenInitializing_ThenThrowsException() + { + // Act + var action = () => new ConsoleAggregateReporter(null!); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void GivenNoWorkflows_WhenInitializing_ThenThrowsException() + { + // Act + var action = () => new ConsoleAggregateReporter([]); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void GivenOperationNotStarted_WhenStopping_ThenThrowsException() + { + // Arrange + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleAggregateReporter([workload]); + + // Act + var action = () => reporter.Stop(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void GivenOperationNotStarted_WhenResuming_ThenThrowsException() + { + // Arrange + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleAggregateReporter([workload]); + + // Act + var action = () => reporter.Resume(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void GivenOperationNotStopped_WhenResuming_ThenThrowsException() + { + // Arrange + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + using var reporter = new ConsoleAggregateReporter([workload]); + reporter.Start(); + + // Act + var action = () => reporter.Resume(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void GivenStartedOperation_WhenStopping_ThenSuccess() + { + // Arrange + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleAggregateReporter([workload]); + reporter.Start(); + + // Act + reporter.Stop(); + } + + [Fact] + public void GivenStoppedOperation_WhenResuming_ThenSuccess() + { + // Arrange + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleAggregateReporter([workload]); + reporter.Start(); + reporter.Stop(); + + // Act + reporter.Resume(); + } + + [Fact] + public async Task GivenProgressNotifications_WhenRunning_ThenCallbackIsCalled() + { + // Arrange + bool isCalled = false; + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleAggregateReporter([workload]) + { + OnProgress = (stats) => isCalled = true + }; + + reporter.Configuration.Options.NotifyProgressStats = true; + reporter.Configuration.StatsFrequency = TimeSpan.FromSeconds(1); + + + // 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 workload = new Workload("Install", "Install stuff", 1); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleAggregateReporter([workload]) + { + OnCompletion = (stats) => isCalled = true + }; + + reporter.Configuration.Options.NotifyCompletionStats = true; + reporter.Configuration.ReportFrequency = TimeSpan.FromMicroseconds(500); + + reporter.Start(); + + // Act + reporter.ReportSuccess(workload.Id); + await Task.Delay(TimeSpan.FromMilliseconds(1000)); + + // Assert + isCalled.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/src/Progress.UnitTest/Reporters/ConsoleReporterTests.cs b/src/Progress.UnitTest/Reporters/ConsoleReporterTests.cs index 6788f99..30ed734 100644 --- a/src/Progress.UnitTest/Reporters/ConsoleReporterTests.cs +++ b/src/Progress.UnitTest/Reporters/ConsoleReporterTests.cs @@ -17,7 +17,9 @@ public ConsoleReporterTests() public void GivenDefaults_WhenInitializing_ThenExpectedSettings() { // Arrange - var reporter = new ConsoleReporter(100, BarDescriptor.Default.Build()); + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleReporter(workload); // Assert reporter.Configuration.Options.DisplayStartingTime.Should().BeTrue(); @@ -26,6 +28,7 @@ public void GivenDefaults_WhenInitializing_ThenExpectedSettings() reporter.Configuration.Options.DisplayEstimatedTimeOfArrival.Should().BeTrue(); reporter.Configuration.Options.DisplayItemsOverview.Should().BeTrue(); reporter.Configuration.Options.DisplayItemsSummary.Should().BeTrue(); + reporter.Configuration.Options.HideWorkflowOnComplete.Should().BeFalse(); reporter.Configuration.Options.NotifyProgressStats.Should().BeFalse(); reporter.Configuration.Options.NotifyCompletionStats.Should().BeFalse(); reporter.Configuration.Options.ExportCompletionStats.Should().BeFalse(); @@ -40,7 +43,9 @@ public void GivenDefaults_WhenInitializing_ThenExpectedSettings() public void GivenNothingToComplete_WhenInitializing_ThenThrowsException() { // Act - var action = () => new ConsoleReporter(0, BarDescriptor.Default.Build()); + var workload = Workload.Default(0); + workload.Component = BarDescriptor.Default.Build(); + var action = () => new ConsoleReporter(workload); // Assert action.Should().Throw(); @@ -50,7 +55,9 @@ public void GivenNothingToComplete_WhenInitializing_ThenThrowsException() public void GivenNoComponent_WhenInitializing_ThenThrowsException() { // Act - var action = () => new ConsoleReporter(100, null!); + var workload = Workload.Default(100); + workload.Component = null!; + var action = () => new ConsoleReporter(workload); // Assert action.Should().Throw(); @@ -60,7 +67,9 @@ public void GivenNoComponent_WhenInitializing_ThenThrowsException() public void GivenOperationNotStarted_WhenStopping_ThenThrowsException() { // Arrange - var reporter = new ConsoleReporter(100, BarDescriptor.Default.Build()); + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleReporter(workload); // Act var action = () => reporter.Stop(); @@ -73,7 +82,9 @@ public void GivenOperationNotStarted_WhenStopping_ThenThrowsException() public void GivenOperationNotStarted_WhenResuming_ThenThrowsException() { // Arrange - var reporter = new ConsoleReporter(100, BarDescriptor.Default.Build()); + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleReporter(workload); // Act var action = () => reporter.Resume(); @@ -86,7 +97,9 @@ public void GivenOperationNotStarted_WhenResuming_ThenThrowsException() public void GivenOperationNotStopped_WhenResuming_ThenThrowsException() { // Arrange - using var reporter = new ConsoleReporter(100, BarDescriptor.Default.Build()); + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + using var reporter = new ConsoleReporter(workload); reporter.Start(); // Act @@ -100,7 +113,9 @@ public void GivenOperationNotStopped_WhenResuming_ThenThrowsException() public void GivenStartedOperation_WhenStopping_ThenSuccess() { // Arrange - var reporter = new ConsoleReporter(100, BarDescriptor.Default.Build()); + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleReporter(workload); reporter.Start(); // Act @@ -111,7 +126,9 @@ public void GivenStartedOperation_WhenStopping_ThenSuccess() public void GivenStoppedOperation_WhenResuming_ThenSuccess() { // Arrange - var reporter = new ConsoleReporter(100, BarDescriptor.Default.Build()); + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleReporter(workload); reporter.Start(); reporter.Stop(); @@ -124,7 +141,9 @@ public async Task GivenProgressNotifications_WhenRunning_ThenCallbackIsCalled() { // Arrange bool isCalled = false; - var reporter = new ConsoleReporter(100, BarDescriptor.Default.Build()) + var workload = Workload.Default(100); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleReporter(workload) { OnProgress = (stats) => isCalled = true }; @@ -146,7 +165,9 @@ public async Task GivenCompletionNotifications_WhenFinished_ThenCallbackIsCalled { // Arrange bool isCalled = false; - var reporter = new ConsoleReporter(1, BarDescriptor.Default.Build()) + var workload = Workload.Default(1); + workload.Component = BarDescriptor.Default.Build(); + var reporter = new ConsoleReporter(workload) { OnCompletion = (stats) => isCalled = true }; diff --git a/src/Progress/Builders/BackgroundReporterBuilder.cs b/src/Progress/Builders/BackgroundReporterBuilder.cs index a8d0b00..4b856ff 100644 --- a/src/Progress/Builders/BackgroundReporterBuilder.cs +++ b/src/Progress/Builders/BackgroundReporterBuilder.cs @@ -63,15 +63,15 @@ public BackgroundReporterBuilder ExportingTo(string fileName, FileType fileType) /// /// Builds the reporter getting an instance of . /// - /// + /// /// /// - public BackgroundReporter Build(ulong itemsCount) + public BackgroundReporter Build(ulong expectedItems) { - if (itemsCount == 0) - throw new ArgumentException("Nothing to do! Set the initial items count for completion."); + if (expectedItems == 0) + throw new ArgumentException("Nothing to do! Set the expected items count for completion."); - var reporter = new BackgroundReporter(itemsCount) + var reporter = new BackgroundReporter(expectedItems) { OnProgress = _onProgressNotified, OnCompletion = _onCompletionNotified diff --git a/src/Progress/Builders/ConsoleAggregateReporterBuilder.cs b/src/Progress/Builders/ConsoleAggregateReporterBuilder.cs new file mode 100644 index 0000000..0954881 --- /dev/null +++ b/src/Progress/Builders/ConsoleAggregateReporterBuilder.cs @@ -0,0 +1,50 @@ +using Progress.Descriptors; +using Progress.Reporters; + +namespace Progress.Builders; + +/// +/// Builder for helping to set up the console aggregate reporter with the desired behavior. +/// It will create an instance of using a by default. +/// +public class ConsoleAggregateReporterBuilder : ConsoleReporterBuilderBase +{ + private Dictionary _workloads = new(); + + + /// + /// Sets the workload with its name, description, expected items and the being used to render the progress of the operation. + /// + /// + /// + /// + public ConsoleAggregateReporterBuilder UsingWorkload(Workload workload, ComponentDescriptor descriptor) + { + if (workload.ItemsCount == 0) + throw new ArgumentException("Nothing to do! Set the expected items count for completion."); + + if (_workloads.ContainsKey(workload.Id)) + throw new InvalidOperationException($"A workload with id {workload.Id} is already set up. Please, use a different one."); + + workload.Component = descriptor.Build(); + _workloads.Add(workload.Id, workload); + return this; + } + + /// + /// Builds the reporter getting an instance of . + /// + /// + /// + public override ConsoleAggregateReporter Build() + { + if (!_workloads.Any()) + throw new ArgumentException("Add some workloads to display progress"); + + ConsoleAggregateReporter reporter = new(_workloads.Values); + SetCallbacks(reporter); + SetConfiguration(reporter); + + return reporter; + } +} diff --git a/src/Progress/Builders/ConsoleReporterBuilder.cs b/src/Progress/Builders/ConsoleReporterBuilder.cs index ce1b583..6b6faa5 100644 --- a/src/Progress/Builders/ConsoleReporterBuilder.cs +++ b/src/Progress/Builders/ConsoleReporterBuilder.cs @@ -1,6 +1,5 @@ using Progress.Descriptors; using Progress.Reporters; -using Progress.Settings; namespace Progress.Builders; @@ -8,90 +7,19 @@ namespace Progress.Builders; /// Builder for helping to set up the console reporter with the desired behavior. /// It will create an instance of using a by default. /// -public class ConsoleReporterBuilder +public class ConsoleReporterBuilder : ConsoleReporterBuilderBase { - private bool _displayRemainingTime; - private bool _displayEstTimeOfArrival; - private bool _displayElapsedTime; - private bool _displayStartingTime; - private bool _displayItemsOverview; - private bool _displayItemsSummary; - private TimeSpan _reportFrequency = TimeSpan.FromSeconds(1); - private TimeSpan _statsFrequency = TimeSpan.FromSeconds(5); private ComponentDescriptor _componentDescriptor = BarDescriptor.Default; - private ExportSettings _exportSettings = default!; - private Action _onProgressNotified = default!; - private Action _onCompletionNotified = default!; + private ulong _expectedItemsCount; /// - /// The reporter will display the elapsed time since the start. + /// Sets the expected items count to process /// + /// /// - public ConsoleReporterBuilder DisplayingElapsedTime() + public ConsoleReporterBuilder UsingExpectedItems(ulong expectedItemsCount) { - _displayElapsedTime = true; - return this; - } - - /// - /// The reporter will display the remaining time to finish. - /// - /// - public ConsoleReporterBuilder DisplayingRemainingTime() - { - _displayRemainingTime = true; - return this; - } - - /// - /// The reporter will display when the operation is expected to finish. - /// - /// - public ConsoleReporterBuilder DisplayingTimeOfArrival() - { - _displayEstTimeOfArrival = true; - return this; - } - - /// - /// The reporter will display when the operation started. - /// - /// - public ConsoleReporterBuilder DisplayingStartingTime() - { - _displayStartingTime = true; - return this; - } - - /// - /// The rerporter will display the total of items processed. - /// - /// - public ConsoleReporterBuilder DisplayingItemsOverview() - { - _displayItemsOverview = true; - return this; - } - - /// - /// The reporter will display the amount of success and failures. - /// - /// - public ConsoleReporterBuilder DisplayingItemsSummary() - { - _displayItemsSummary = true; - return this; - } - - /// - /// Sets the frequency the reporter will refresh the status of the operation. - /// The default reporting frequency is set to 1s. - /// - /// - /// - public ConsoleReporterBuilder UsingReportingFrequency(TimeSpan frequency) - { - _reportFrequency = frequency; + _expectedItemsCount = expectedItemsCount; return this; } @@ -105,85 +33,29 @@ public ConsoleReporterBuilder UsingComponentDescriptor(ComponentDescriptor descr _componentDescriptor = descriptor; return this; } - - /// - /// Sets the progress notification callback. - /// Default notification frequency is set to 5 seconds. - /// - /// - /// - public ConsoleReporterBuilder NotifyingProgress(Action callback) - { - return NotifyingProgress(callback, _statsFrequency); - } - - /// - /// Sets the progress notification callback with the invocation frequency. - /// - /// - /// - /// - public ConsoleReporterBuilder NotifyingProgress(Action callback, TimeSpan statsFrequency) - { - _onProgressNotified = callback; - _statsFrequency = statsFrequency; - return this; - } - - /// - /// Sets the completion notification callback. - /// - /// - /// - public ConsoleReporterBuilder NotifyingCompletion(Action callback) - { - _onCompletionNotified = callback; - return this; - } - - /// - /// Sets and tells the reporter how and where to export the final stats. - /// - /// - /// - /// - public ConsoleReporterBuilder ExportingTo(string fileName, FileType fileType) - { - _exportSettings = new ExportSettings(fileName, fileType); - return this; - } + /// /// Builds the reporter getting an instance of . /// - /// /// /// - public ConsoleReporter Build(ulong itemsCount) + public override ConsoleReporter Build() { - if (itemsCount == 0) - throw new ArgumentException("Nothing to do! Set the initial items count for completion."); + if (_expectedItemsCount == 0) + throw new ArgumentException("Nothing to do! Set the expected items count for completion."); + + if (_componentDescriptor == null) + throw new ArgumentException("Add a component descriptor to display progress"); var component = _componentDescriptor.Build(); - var reporter = new ConsoleReporter(itemsCount, component) - { - OnProgress = _onProgressNotified, - OnCompletion = _onCompletionNotified - }; + var workload = Workload.Default(_expectedItemsCount); + workload.Component = component; - reporter.Configuration.ReportFrequency = _reportFrequency; - reporter.Configuration.StatsFrequency = _statsFrequency; - reporter.Configuration.ExportSettings = _exportSettings; - reporter.Configuration.Options.DisplayRemainingTime = _displayRemainingTime; - reporter.Configuration.Options.DisplayEstimatedTimeOfArrival = _displayEstTimeOfArrival; - reporter.Configuration.Options.DisplayElapsedTime = _displayElapsedTime; - reporter.Configuration.Options.DisplayStartingTime = _displayStartingTime; - reporter.Configuration.Options.DisplayItemsOverview = _displayItemsOverview; - reporter.Configuration.Options.DisplayItemsSummary = _displayItemsSummary; - reporter.Configuration.Options.NotifyProgressStats = _onProgressNotified != null; - reporter.Configuration.Options.NotifyCompletionStats = _onCompletionNotified != null; - reporter.Configuration.Options.ExportCompletionStats = _exportSettings != null; + ConsoleReporter reporter = new(workload); + SetCallbacks(reporter); + SetConfiguration(reporter); return reporter; } diff --git a/src/Progress/Builders/ConsoleReporterBuilderBase.cs b/src/Progress/Builders/ConsoleReporterBuilderBase.cs new file mode 100644 index 0000000..6fbe9b9 --- /dev/null +++ b/src/Progress/Builders/ConsoleReporterBuilderBase.cs @@ -0,0 +1,233 @@ +using Progress.Reporters; +using Progress.Settings; +using Progress.Settings.Console; + +namespace Progress.Builders; + +/// +/// Base class for console based reporter builders +/// +public abstract class ConsoleReporterBuilderBase + where T: ConsoleReporterBase + where U: ConsoleReporterBuilderBase +{ + /// + /// Indicates whether it displays the remaining time + /// + protected bool _displayRemainingTime; + + /// + /// Indicates whether it displays the ETA + /// + protected bool _displayEstTimeOfArrival; + + /// + /// Indicates whether it displays the elapsed time + /// + protected bool _displayElapsedTime; + + /// + /// Indicates whether it displays the starting time + /// + protected bool _displayStartingTime; + + /// + /// Indicates whether it displays the items overview + /// + protected bool _displayItemsOverview; + + /// + /// Indicates whether it displays the items summary + /// + protected bool _displayItemsSummary; + + /// + /// Indicates whether it shows or hides the progression component once it has finished + /// + protected bool _hideOnComplete; + + /// + /// The reporting frequency timespan + /// + protected TimeSpan _reportFrequency = TimeSpan.FromSeconds(1); + + /// + /// The stats frequency timespan + /// + protected TimeSpan _statsFrequency = TimeSpan.FromSeconds(5); + + /// + /// The export settings + /// + protected ExportSettings _exportSettings = default!; + + /// + /// The stats hook called during the progression + /// + protected Action _onProgressNotified = default!; + + /// + /// The stats hook called on comppletion + /// + protected Action _onCompletionNotified = default!; + + /// + /// The reporter will display the elapsed time since the start. + /// + /// + public U DisplayingElapsedTime() + { + _displayElapsedTime = true; + return (U)this; + } + + /// + /// The reporter will display the remaining time to finish. + /// + /// + public U DisplayingRemainingTime() + { + _displayRemainingTime = true; + return (U)this; + } + + /// + /// The reporter will display when the operation is expected to finish. + /// + /// + public U DisplayingTimeOfArrival() + { + _displayEstTimeOfArrival = true; + return (U)this; + } + + /// + /// The reporter will display when the operation started. + /// + /// + public U DisplayingStartingTime() + { + _displayStartingTime = true; + return (U)this; + } + + /// + /// The rerporter will display the total of items processed. + /// + /// + public U DisplayingItemsOverview() + { + _displayItemsOverview = true; + return (U)this; + } + + /// + /// The reporter will display the amount of success and failures. + /// + /// + public U DisplayingItemsSummary() + { + _displayItemsSummary = true; + return (U)this; + } + + /// + /// The reporter will hide the workload progression when it gets the 100%. + /// + /// + /// + public U HideWorkloadOnComplete(bool hide) + { + _hideOnComplete = hide; + return (U)this; + } + + /// + /// Sets the frequency the reporter will refresh the status of the operation. + /// The default reporting frequency is set to 1s. + /// + /// + /// + public U UsingReportingFrequency(TimeSpan frequency) + { + _reportFrequency = frequency; + return (U)this; + } + + /// + /// Sets the progress notification callback. + /// Default notification frequency is set to 5 seconds. + /// + /// + /// + public U NotifyingProgress(Action callback) + { + return NotifyingProgress(callback, _statsFrequency); + } + + /// + /// Sets the progress notification callback with the invocation frequency. + /// + /// + /// + /// + public U NotifyingProgress(Action callback, TimeSpan statsFrequency) + { + _onProgressNotified = callback; + _statsFrequency = statsFrequency; + return (U)this; + } + + /// + /// Sets the completion notification callback. + /// + /// + /// + public U NotifyingCompletion(Action callback) + { + _onCompletionNotified = callback; + return (U)this; + } + + /// + /// Sets and tells the reporter how and where to export the final stats. + /// + /// + /// + /// + public U ExportingTo(string fileName, FileType fileType) + { + _exportSettings = new ExportSettings(fileName, fileType); + return (U)this; + } + + /// + /// Builds the console based reporter + /// + /// + public abstract T Build(); + + + internal void SetCallbacks(ConsoleReporterBase reporter) + { + reporter.OnProgress = _onProgressNotified; + reporter.OnCompletion = _onCompletionNotified; + } + + internal void SetConfiguration(ConsoleReporterBase reporter) + { + reporter.Configuration.ReportFrequency = _reportFrequency; + reporter.Configuration.StatsFrequency = _statsFrequency; + reporter.Configuration.ExportSettings = _exportSettings; + reporter.Configuration.Options.DisplayRemainingTime = _displayRemainingTime; + reporter.Configuration.Options.DisplayEstimatedTimeOfArrival = _displayEstTimeOfArrival; + reporter.Configuration.Options.DisplayElapsedTime = _displayElapsedTime; + reporter.Configuration.Options.DisplayStartingTime = _displayStartingTime; + reporter.Configuration.Options.DisplayItemsOverview = _displayItemsOverview; + reporter.Configuration.Options.DisplayItemsSummary = _displayItemsSummary; + reporter.Configuration.Options.HideWorkflowOnComplete = _hideOnComplete; + reporter.Configuration.Options.NotifyProgressStats = _onProgressNotified != null; + reporter.Configuration.Options.NotifyCompletionStats = _onCompletionNotified != null; + reporter.Configuration.Options.ExportCompletionStats = _exportSettings != null; + } +} diff --git a/src/Progress/Content/README.md b/src/Progress/Content/README.md index c9bec2d..f6095f8 100644 --- a/src/Progress/Content/README.md +++ b/src/Progress/Content/README.md @@ -1,121 +1,126 @@ -# Progress - -This library helps you to spin up reporters for giving an overview of the progression of a given operation. These reporters can either be for console apps that output the information or background jobs hosted by APIs handling the progress via hooks. - - -## Report progress for console apps -Report progression with ease by using multilple components such as Bars, Spinners, Pulse and more. +![Progress](logo512x512.png) -```csharp -using var reporter = new ConsoleReporterBuilder() - .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) - .UsingComponentDescriptor(BarDescriptor.Default) - .Build(Worker.AllItems); - -var worker = new Worker() -{ - OnSuccess = () => reporter.ReportSuccess(), - OnFailure = () => reporter.ReportFailure(), -}; +# Progress -reporter.Start(); -await worker.DoMywork(); -``` +This library provides a set of features that help you spin up reporting tasks, either in the form of console apps that show workloads progression or via background jobs providing access to stats during the lifespan of the workload. -### Define the component to show progress -Components are subjected to the ConsoleReporter. +## Key components +- Reporters are the core components that display information about the current workload status. + - **ConsoleReporter**: Used in console apps to report progression of a simple workload. + - **ConsoleAggregateReporter**: Used in console apps to report progression of many workloads. + - **BackgroundReporter**: Used in background services that don't require to ouput to std out. -```csharp -var descriptor = new BarDescriptor() - .UsingProgressSymbol('#') - .UsingWidth(50) - .DisplayingPercent(); -``` +- Components are objects that report progress as figures, such as bars, spinners, pulse ... Only supported for console-based reporters. All these components are internal and they can only be initialized via their Descriptors. + - Bar + - Spinner + - HearthBeat + - Pulse -or use the default one -```csharp -var descriptor = BarDescriptor.Default; -``` +- Descriptors are builders that help to set up components with proper appearance and behavior. + - BarDescriptor + - SpinnerDescriptor + - HearthBeatDescriptor + - PulseDescriptor -### Define the reporter to use the component -```csharp -var reporter = new ConsoleReporterBuilder() - .DisplayingStartingTime() - .DisplayingElapsedTime() - .DisplayingTimeOfArrival() - .DisplayingRemainingTime() - .DisplayingItemsSummary() - .DisplayingItemsOverview() - .NotifyingProgress(onProgress) - .NotifyingCompletion(onCompletion) - .ExportingTo("output.json", FileType.Json) - .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) - .UsingComponentDescriptor(BarDescriptor.Default) - .Build(Worker.AllItems); -``` +- ReporterBuilders help to set up reporters with workloads, components and behavior. + - ConsoleReporterBuilder + - ConsoleAggregateReporterBuilder + - BackgroundReporterBuilder -## Report progress for background jobs -In case your workload happens in the background and you only pretend to collect progression status during certain moments of the execution, or export the results at the end, the BackgroundReporter may help you to remove all the boilerplate. +## Console apps with ConsoleReporter ```csharp -_reporter = new BackgroundReporterBuilder() - .NotifyingProgress((stats) => - { - logger.LogDebug("Getting stats on progress {percent}", stats.CurrentPercent); - }) - .NotifyingCompletion((stats) => - { - logger.LogDebug("Getting stats on completion"); - }) - .Build(Worker.AllItems); - -_worker.OnSuccess = () => _reporter.ReportSuccess(); -_worker.OnFailure = () => _reporter.ReportFailure(); + using var reporter = new ConsoleReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .DisplayingItemsSummary() + .DisplayingItemsOverview() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingComponentDescriptor(BarDescriptor.Default) + .UsingExpectedItems(SimpleWorker.ExpectedItems) + .Build(); + + var worker = new SimpleWorker() + { + OnSuccess = reporter.ReportSuccess, + OnFailure = reporter.ReportFailure, + }; + + reporter.Start(); + await worker.DoMyworkAsync(); ``` -### Start the reporter -```csharp -reporter.Start(); -``` +## Console apps with ConsoleAggregateReporter -### Stop the reporter ```csharp -reporter.Stop(); + var worker = new InstallerWorker(); + + using var reporter = new ConsoleAggregateReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .DisplayingItemsSummary() + .DisplayingItemsOverview() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingWorkload(worker.CalcRequirements, BarDescriptor.Default) + .UsingWorkload(worker.DownloadArtifacts, BarDescriptor.Default) + .UsingWorkload(worker.InstallArtifacts, BarDescriptor.Default) + .Build(); + + worker.OnSuccess = reporter.ReportSuccess; + worker.OnFailure = reporter.ReportFailure; + + reporter.Start(); + await worker.CalcRequirements.CalcAsync(); + Task[] restTasks = [worker.DownloadArtifacts.DownloadAsync(), worker.InstallArtifacts.InstallAsync()]; + await Task.WhenAll(restTasks); ``` -### Resume the reporter -```csharp -reporter.Resume(); -``` -### Report progress -```csharp -reporter.ReportSuccess() -reporter.ReportFailure() -``` +## Background services -### Collect stats while running ```csharp -var onProgress = (Stats stats) => +internal class HostedService : IHostedService { - // TODO: Do something useful -}; + private readonly SimpleWorker _worker = new(); + private readonly BackgroundReporter _reporter; -var onCompletion = (Stats stats) => -{ - // TODO: Do something useful -}; - -var reporter = new ConsoleReporterBuilder() - .NotifyingProgress(onProgress) - .NotifyingCompletion(onCompletion) - .Build(Worker.AllItems); -``` - - -### Exports final stats (json, csv, xml, txt) -```csharp -var reporter = new ConsoleReporterBuilder() - .ExportingTo("output.json", FileType.Json) - .Build(Worker.AllItems); + public HostedService(ILogger logger) + { + _reporter = new BackgroundReporterBuilder() + .NotifyingProgress((stats) => + { + logger.LogDebug("Getting stats on progress {percent}", stats.CurrentPercent); + }) + .NotifyingCompletion((stats) => + { + logger.LogDebug("Getting stats on completion"); + }) + .Build(SimpleWorker.ExpectedItems); + + _worker.OnSuccess = () => _reporter.ReportSuccess(); + _worker.OnFailure = () => _reporter.ReportFailure(); + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _reporter.Start(); + await _worker.DoMyworkAsync(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _reporter.Stop(); + return Task.CompletedTask; + } +} ``` \ No newline at end of file diff --git a/img/logo512x512.png b/src/Progress/Content/logo512x512.png similarity index 100% rename from img/logo512x512.png rename to src/Progress/Content/logo512x512.png diff --git a/src/Progress/Printer.cs b/src/Progress/Printer.cs index b247f45..3656844 100644 --- a/src/Progress/Printer.cs +++ b/src/Progress/Printer.cs @@ -1,16 +1,16 @@ -using Progress.Components; +using Progress.Reporters; using Progress.Settings.Console; using System.Text; namespace Progress; -internal class Printer(ReportingOptions options, Component component) +internal class Printer(ReportingOptions options, ICollection workloads) { private const int Left_Padding = 30; private const int Right_Padding = 20; private readonly ReportingOptions _options = options; - private readonly Component _component = component; + private readonly IEnumerable _workloads = workloads; public string Print(Stats stats) { @@ -63,7 +63,17 @@ public string Print(Stats stats) } sBuilder.AppendLine(); - sBuilder.AppendLine(_component.ToString()); + + foreach(var workload in _workloads) + { + if (_options.HideWorkflowOnComplete && workload.IsFinished) + continue; + + sBuilder.AppendLine(workload.Description); + sBuilder.AppendLine(workload.Component.ToString()); + sBuilder.AppendLine(); + } + return sBuilder.ToString(); } } diff --git a/src/Progress/Progress.csproj b/src/Progress/Progress.csproj index c61d973..de98ba7 100644 --- a/src/Progress/Progress.csproj +++ b/src/Progress/Progress.csproj @@ -36,6 +36,10 @@ True \Content + + True + \Content + True \Content diff --git a/src/Progress/Reporters/ConsoleAggregateReporter.cs b/src/Progress/Reporters/ConsoleAggregateReporter.cs new file mode 100644 index 0000000..7876f76 --- /dev/null +++ b/src/Progress/Reporters/ConsoleAggregateReporter.cs @@ -0,0 +1,44 @@ +namespace Progress.Reporters; + +/// +/// Reports many background task's progress to console based on the settings provided. +/// +public class ConsoleAggregateReporter : ConsoleReporterBase +{ + internal ConsoleAggregateReporter(ICollection workloads) + : base(workloads) + { + } + + /// + /// Reportes success. By calling this method, the success counter increases. + /// + public void ReportSuccess(string workloadName) => Workloads.Success(workloadName); + + /// + /// Reports failures. By calling this method, the failure counter increases. + /// + public void ReportFailure(string workloadName) => Workloads.Failure(workloadName); + + /// + /// Collect stats relative to all workloads + /// + /// + protected override Stats CollectStats() + { + double currentPercent = Workloads.CurrentPercent; + + return new() + { + StartedOn = Timer.StartedOn, + ElapsedTime = Timer.ElapsedTime, + RemainingTime = Timer.GetRemainingTime(currentPercent), + EstTimeOfArrival = Timer.GetEstimatedTimeOfArrival(currentPercent), + ExpectedItems = Workloads.AllItemsCount, + SuccessCount = Workloads.AllSuccess, + FailureCount = Workloads.AllFailures, + CurrentCount = Workloads.CurrentCount, + CurrentPercent = Workloads.CurrentPercent, + }; + } +} diff --git a/src/Progress/Reporters/ConsoleReporter.cs b/src/Progress/Reporters/ConsoleReporter.cs index 83d45df..b353be7 100644 --- a/src/Progress/Reporters/ConsoleReporter.cs +++ b/src/Progress/Reporters/ConsoleReporter.cs @@ -1,247 +1,46 @@ -using Progress.Components; -using Progress.Settings.Console; -using System.Text; - -namespace Progress.Reporters; +namespace Progress.Reporters; /// /// Reports background task's progress to console based on the settings provided. /// -public class ConsoleReporter : IDisposable +public class ConsoleReporter : ConsoleReporterBase { - private readonly ulong _itemsCount; - private readonly Configuration _configuration; - - private bool _isDisposed; - private ulong _successCount; - private ulong _failureCount; - private Component _component; - private Timer _timer; - private Printer _printer; - 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; + private Workload Workload => Workloads.ElementAt(0); - internal Configuration Configuration => _configuration; - internal Action? OnProgress { get; set; } = null!; - internal Action? OnCompletion { get; set; } = null!; - - private ulong CurrentCount => _successCount + _failureCount; - - - internal ConsoleReporter(ulong itemsCount, Component component) + internal ConsoleReporter(Workload workload) + : base([workload]) { - if (itemsCount == 0) - throw new ArgumentException($"Nothing to do!. {nameof(itemsCount)} must be greater than 0"); - - if (component == null) - throw new ArgumentNullException($"{nameof(component)} cannot be null"); - - _itemsCount = itemsCount; - _component = component; - _timer = Timer.Start(); - _configuration = new Configuration(); - _printer = new Printer(Configuration.Options, component); } /// /// Reportes success. By calling this method, the success counter increases. /// - public void ReportSuccess() => Interlocked.Increment(ref _successCount); + public void ReportSuccess() => Workload.ReportSuccess(); /// /// Reports failures. By calling this method, the failure counter increases. /// - public void ReportFailure() => Interlocked.Increment(ref _failureCount); - - /// - /// Starts a thread that refeshes the console output displaying the progress of the operation. - /// - public void Start() - { - if (_isDisposed) - throw new ObjectDisposedException("Instance already disposed"); - - _successCount = 0; - _failureCount = 0; - _lastStatsNotification = DateTimeOffset.UtcNow; - _timer = Timer.Start(); - _cancellationTokenSource = new CancellationTokenSource(); - _cancellationToken = _cancellationTokenSource.Token; - _reportingThread = DoWork(); - _reportingThread.Start(); - - if (Configuration.Options.NotifyProgressStats) - { - _statsThread = DoStats(); - _statsThread.Start(); - } - } - - /// - /// Stops the thread that refeshes the console output. - /// - public void Stop() - { - if (_reportingThread == null) - throw new InvalidOperationException($"First start the reporting by calling {nameof(Start)}"); - - if (_cancellationToken.CanBeCanceled) - _cancellationTokenSource.Cancel(); - - if (_reportingThread != null && _reportingThread.IsAlive) - _reportingThread.Join(); - - if (_statsThread != null && _statsThread.IsAlive) - _statsThread.Join(); - } - - /// - /// Resumes the reporting operation. - /// - public void Resume() - { - if (_reportingThread == null) - throw new InvalidOperationException($"First start the reporting by calling {nameof(Start)}"); - - if (_reportingThread.IsAlive) - throw new InvalidOperationException($"The reporting is still in progress."); - - _cancellationTokenSource = new CancellationTokenSource(); - _cancellationToken = _cancellationTokenSource.Token; - _reportingThread = DoWork(); - _reportingThread.Start(); - - if (Configuration.Options.NotifyProgressStats) - { - _statsThread = DoStats(); - _statsThread.Start(); - } - } + public void ReportFailure() => Workload.ReportFailure(); /// - /// Gets the reporter's status. + /// Collect stats of the workload /// /// - public override string ToString() + protected override Stats CollectStats() { - var stats = CollectStats(); - return _printer.Print(stats); - } - - /// - /// Call and call for finalize - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Cancels the reporting task and waits for the reporting thread in case it is alive. - /// - /// - protected virtual void Dispose(bool disposing) - { - if (!_isDisposed) - { - _isDisposed = true; - - if (disposing) - { - if (_cancellationToken.CanBeCanceled) - _cancellationTokenSource.Cancel(); - - if (_reportingThread != null && _reportingThread.IsAlive) - _reportingThread.Join(); - } - } - } - - private void Display() - { - if (!Console.IsOutputRedirected) - Console.SetCursorPosition(0, 0); - - _component = _component.Next(_itemsCount, CurrentCount); - byte[] output = Encoding.Default.GetBytes(ToString()); - using var stream = Console.OpenStandardOutput(); - stream.Write(output, 0, output.Length); - stream.Flush(); - } + double percent = Workload.Component.CurrentPercent.Value; - private void ReportStats() - { - if (OnProgress == null || DateTimeOffset.UtcNow - _lastStatsNotification < Configuration.StatsFrequency) - return; - - var stats = CollectStats(); - OnProgress.Invoke(stats); - _lastStatsNotification = DateTimeOffset.UtcNow; - } - - private Thread DoWork() - { - var tStart = new ThreadStart(() => - { - do - { - Display(); - Thread.Sleep(Configuration.ReportFrequency); - } - while (!IsFinished && !_cancellationToken.IsCancellationRequested); - - Display(); - - if (IsFinished && (Configuration.Options.NotifyCompletionStats || Configuration.Options.ExportCompletionStats)) - { - var stats = CollectStats(); - OnCompletion?.Invoke(stats); - - if (Configuration.ExportSettings != null) - new Exporter(Configuration.ExportSettings).Export(stats); - } - }); - - return new(tStart); - } - - private Thread DoStats() - { - var tStart = new ThreadStart(() => - { - do - { - ReportStats(); - Thread.Sleep(Configuration.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, + StartedOn = Timer.StartedOn, + ElapsedTime = Timer.ElapsedTime, + RemainingTime = Timer.GetRemainingTime(percent), + EstTimeOfArrival = Timer.GetEstimatedTimeOfArrival(percent), + ExpectedItems = Workload.ItemsCount, + CurrentCount = Workload.CurrentCount, + SuccessCount = Workload.SuccessCount, + FailureCount = Workload.FailureCount, + CurrentPercent = percent, }; } } diff --git a/src/Progress/Reporters/ConsoleReporterBase.cs b/src/Progress/Reporters/ConsoleReporterBase.cs new file mode 100644 index 0000000..f3e11e3 --- /dev/null +++ b/src/Progress/Reporters/ConsoleReporterBase.cs @@ -0,0 +1,252 @@ +using Progress.Settings.Console; +using System.Text; + +namespace Progress.Reporters; + +/// +/// Base class for console based reporters. +/// +public abstract class ConsoleReporterBase : IDisposable +{ + private readonly Configuration _configuration; + private readonly Workloads _workloads; + + private bool _isDisposed; + private Timer _timer; + private Printer _printer; + private Thread _reportingThread = default!; + private Thread _statsThread = default!; + private CancellationTokenSource _cancellationTokenSource = default!; + private CancellationToken _cancellationToken; + private DateTimeOffset _lastStatsNotification; + private Dictionary _lastWorkflowOverview; + + /// + /// Inidicates whehter the task is completed or not. + /// + public bool IsFinished => _workloads.AreFinished; + + internal Configuration Configuration => _configuration; + internal Action? OnProgress { get; set; } = null!; + internal Action? OnCompletion { get; set; } = null!; + internal Timer Timer => _timer; + internal Printer Printer => _printer; + internal Workloads Workloads => _workloads; + + /// + /// Initializes an insatance of any console based reporter. + /// + /// + /// + /// + internal ConsoleReporterBase(ICollection workloads) + { + if (workloads == null) + throw new ArgumentNullException(nameof(workloads)); + + if (workloads.Count == 0) + throw new ArgumentException("Nothing to do!. Provide at least one workload"); + + foreach (var workload in workloads) + { + if (workload.ItemsCount == 0) + throw new ArgumentException($"Nothing to do!. {nameof(workload.ItemsCount)} must be greater than 0"); + + if (workload.Component == null) + throw new ArgumentNullException($"{nameof(workload.Component)} cannot be null"); + } + + _timer = Timer.Start(); + _configuration = new Configuration(); + _workloads = new Workloads(workloads); + _printer = new Printer(_configuration.Options, _workloads.ToArray()); + _lastWorkflowOverview = _workloads.GetOverview(); + } + + /// + /// Starts a thread that refeshes the console output displaying the progress of the operation. + /// + public void Start() + { + if (_isDisposed) + throw new ObjectDisposedException("Instance already disposed"); + + foreach (var workload in _workloads) + workload.Reset(); + + _lastStatsNotification = DateTimeOffset.UtcNow; + _timer = Timer.Start(); + _cancellationTokenSource = new CancellationTokenSource(); + _cancellationToken = _cancellationTokenSource.Token; + _reportingThread = DoWork(); + _reportingThread.Start(); + + if (_configuration.Options.NotifyProgressStats) + { + _statsThread = DoStats(); + _statsThread.Start(); + } + } + + /// + /// Stops the thread that refeshes the console output. + /// + public void Stop() + { + if (_reportingThread == null) + throw new InvalidOperationException($"First start the reporting by calling {nameof(Start)}"); + + if (_cancellationToken.CanBeCanceled) + _cancellationTokenSource.Cancel(); + + if (_reportingThread != null && _reportingThread.IsAlive) + _reportingThread.Join(); + + if (_statsThread != null && _statsThread.IsAlive) + _statsThread.Join(); + } + + /// + /// Resumes the reporting operation. + /// + public void Resume() + { + if (_reportingThread == null) + throw new InvalidOperationException($"First start the reporting by calling {nameof(Start)}"); + + if (_reportingThread.IsAlive) + throw new InvalidOperationException($"The reporting is still in progress."); + + _cancellationTokenSource = new CancellationTokenSource(); + _cancellationToken = _cancellationTokenSource.Token; + _reportingThread = DoWork(); + _reportingThread.Start(); + + if (_configuration.Options.NotifyProgressStats) + { + _statsThread = DoStats(); + _statsThread.Start(); + } + } + + /// + /// Gets the reporter's status. + /// + /// + public override string ToString() + { + var stats = CollectStats(); + return _printer.Print(stats); + } + + /// + /// Call and call for finalize + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Cancels the reporting task and waits for the reporting thread in case it is alive. + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + _isDisposed = true; + + if (disposing) + { + if (_cancellationToken.CanBeCanceled) + _cancellationTokenSource.Cancel(); + + if (_reportingThread != null && _reportingThread.IsAlive) + _reportingThread.Join(); + + OnProgress = null!; + OnCompletion = null!; + } + } + } + + /// + /// Collect stats relative to all workloads + /// + /// + protected abstract Stats CollectStats(); + + private Thread DoWork() + { + var tStart = new ThreadStart(() => + { + do + { + Display(); + Thread.Sleep(_configuration.ReportFrequency); + } + while (!IsFinished && !_cancellationToken.IsCancellationRequested); + + Display(); + + if (IsFinished && (_configuration.Options.NotifyCompletionStats || _configuration.Options.ExportCompletionStats)) + { + var stats = CollectStats(); + OnCompletion?.Invoke(stats); + + if (_configuration.ExportSettings != null) + new Exporter(_configuration.ExportSettings).Export(stats); + } + }); + + return new(tStart); + } + + private Thread DoStats() + { + var tStart = new ThreadStart(() => + { + do + { + ReportStats(); + Thread.Sleep(_configuration.StatsFrequency); + } + while (!IsFinished && !_cancellationToken.IsCancellationRequested); + }); + + return new(tStart); + } + + private void ReportStats() + { + if (OnProgress == null || DateTimeOffset.UtcNow - _lastStatsNotification < Configuration.StatsFrequency) + return; + + var stats = CollectStats(); + OnProgress.Invoke(stats); + _lastStatsNotification = DateTimeOffset.UtcNow; + } + + private void Display() + { + _workloads.Next(); + var currentWorflowOverview = _workloads.GetOverview(); + + if (!Console.IsOutputRedirected) + { + if (_configuration.Options.HideWorkflowOnComplete && _lastWorkflowOverview.Except(currentWorflowOverview).Any()) + Console.Clear(); + + Console.SetCursorPosition(0, 0); + } + + _lastWorkflowOverview = currentWorflowOverview; + byte[] output = Encoding.Default.GetBytes(ToString()); + using var stream = Console.OpenStandardOutput(); + stream.Write(output, 0, output.Length); + stream.Flush(); + + } +} diff --git a/src/Progress/Reporters/Workload.cs b/src/Progress/Reporters/Workload.cs new file mode 100644 index 0000000..4ff0ec8 --- /dev/null +++ b/src/Progress/Reporters/Workload.cs @@ -0,0 +1,93 @@ +using Progress.Components; +using System.Collections; + +namespace Progress.Reporters +{ + /// + /// Initializes a new instance of describing the task to perform. + /// + /// + /// + /// + public class Workload(string id, string description, ulong expectedItems) + { + /// + /// Helper to initialize a new instance of with its identifier as 'Default' + /// + /// + /// + public static Workload Default(ulong itemsCount) => new("Default", string.Empty, itemsCount); + + private ulong _successCount; + private ulong _failureCount; + + /// + /// Gets the workload's identifier + /// + public string Id { get; } = id; + + /// + /// Gets the workflow description + /// + public string Description { get; } = description; + + /// + /// Gets the exepected items count + /// + public ulong ItemsCount { get; } = expectedItems; + + internal Component Component { get; set; } = default!; + internal ulong SuccessCount => _successCount; + internal ulong FailureCount => _failureCount; + internal ulong CurrentCount => SuccessCount + FailureCount; + internal bool IsFinished => CurrentCount == ItemsCount; + + internal void ReportSuccess() => Interlocked.Increment(ref _successCount); + internal void ReportFailure() => Interlocked.Increment(ref _failureCount); + internal void Next() => Component.Next(ItemsCount, CurrentCount); + + internal void Reset() + { + _successCount = 0; + _failureCount = 0; + } + } + + internal class Workloads(ICollection workloads) : IEnumerable + { + private readonly Dictionary _workloads = workloads.ToDictionary(w => w.Id, w => w); + + public IEnumerator GetEnumerator() => _workloads.Values.GetEnumerator(); + public double CurrentPercent => _workloads.Values.Select(w => w.Component.CurrentPercent.Value).Sum() / _workloads.Count(); + public ulong AllItemsCount { get; } = (ulong)Enumerable.Sum(workloads, (w) => (long)w.ItemsCount); + public ulong CurrentCount => (ulong)Enumerable.Sum(_workloads.Values, (w) => (long)w.CurrentCount); + public ulong AllSuccess => (ulong)Enumerable.Sum(_workloads.Values, (w) => (long)w.SuccessCount); + public ulong AllFailures => (ulong)Enumerable.Sum(_workloads.Values, (w) => (long)w.FailureCount); + public bool AreFinished => _workloads.Values.All(w => w.IsFinished); + + public void Next() + { + foreach (var workload in _workloads.Values) + workload.Next(); + } + + public void Success(string workloadName) + { + if (_workloads.ContainsKey(workloadName)) + _workloads[workloadName].ReportSuccess(); + } + + public void Failure(string workloadName) + { + if (_workloads.ContainsKey(workloadName)) + _workloads[workloadName].ReportFailure(); + } + + public Dictionary GetOverview() => _workloads.ToDictionary(w => w.Key, w => w.Value.IsFinished); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Progress/Settings/Console/ReportingOptions.cs b/src/Progress/Settings/Console/ReportingOptions.cs index dc9f457..5656147 100644 --- a/src/Progress/Settings/Console/ReportingOptions.cs +++ b/src/Progress/Settings/Console/ReportingOptions.cs @@ -8,6 +8,7 @@ internal class ReportingOptions internal bool DisplayStartingTime { get; set; } = true; internal bool DisplayItemsOverview { get; set; } = true; internal bool DisplayItemsSummary { get; set; } = true; + internal bool HideWorkflowOnComplete { get; set; } = false; internal bool NotifyProgressStats { get; set; } = false; internal bool NotifyCompletionStats { get; set; } = false; internal bool ExportCompletionStats { get; set; } = false; diff --git a/src/Progress/Settings/ExportSettings.cs b/src/Progress/Settings/ExportSettings.cs index c6778e9..108fcaf 100644 --- a/src/Progress/Settings/ExportSettings.cs +++ b/src/Progress/Settings/ExportSettings.cs @@ -23,4 +23,9 @@ public enum FileType Xml } -internal record ExportSettings(string FileName, FileType FileType); \ No newline at end of file +/// +/// Define the export settings +/// +/// +/// +public record ExportSettings(string FileName, FileType FileType); \ No newline at end of file diff --git a/src/Samples/Progress.Samples.Background.Api/BackgroundJob.cs b/src/Samples/Progress.Samples.Background.Api/BackgroundJob.cs index 81ff345..1bb6739 100644 --- a/src/Samples/Progress.Samples.Background.Api/BackgroundJob.cs +++ b/src/Samples/Progress.Samples.Background.Api/BackgroundJob.cs @@ -23,16 +23,16 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) .NotifyingProgress(onProgress, TimeSpan.FromSeconds(10)) .NotifyingCompletion(onCompletion) .ExportingTo("output.json", Settings.FileType.Json) - .Build(Worker.AllItems); + .Build(SimpleWorker.ExpectedItems); - var worker = new Worker() + var worker = new SimpleWorker() { OnSuccess = () => reporter.ReportSuccess(), OnFailure = () => reporter.ReportFailure(), }; reporter.Start(); - await worker.DoMywork(); + await worker.DoMyworkAsync(); } } diff --git a/src/Samples/Progress.Samples.Background.Api/HostedService.cs b/src/Samples/Progress.Samples.Background.Api/HostedService.cs index b1a0a06..314d1c9 100644 --- a/src/Samples/Progress.Samples.Background.Api/HostedService.cs +++ b/src/Samples/Progress.Samples.Background.Api/HostedService.cs @@ -5,7 +5,7 @@ namespace Progress.Samples.Background.Api; internal class HostedService : IHostedService { - private readonly Worker _worker = new(); + private readonly SimpleWorker _worker = new(); private readonly BackgroundReporter _reporter; public HostedService(ILogger logger) @@ -19,7 +19,7 @@ public HostedService(ILogger logger) { logger.LogDebug("Getting stats on completion"); }) - .Build(Worker.AllItems); + .Build(SimpleWorker.ExpectedItems); _worker.OnSuccess = () => _reporter.ReportSuccess(); _worker.OnFailure = () => _reporter.ReportFailure(); @@ -28,7 +28,7 @@ public HostedService(ILogger logger) public async Task StartAsync(CancellationToken cancellationToken) { _reporter.Start(); - await _worker.DoMywork(); + await _worker.DoMyworkAsync(); } public Task StopAsync(CancellationToken cancellationToken) diff --git a/src/Samples/Progress.Samples.Bar.App/Program.cs b/src/Samples/Progress.Samples.Bar.App/Program.cs index 3149bbf..71e9a0c 100644 --- a/src/Samples/Progress.Samples.Bar.App/Program.cs +++ b/src/Samples/Progress.Samples.Bar.App/Program.cs @@ -2,37 +2,86 @@ using Progress.Builders; using Progress.Descriptors; using Progress.Samples; +using Progress.Samples.Utils; using Progress.Settings; -var onProgress = (Stats stats) => -{ - // TODO: Do something useful -}; -var onCompletion = (Stats stats) => -{ - // TODO: Do something useful -}; - -using var reporter = new ConsoleReporterBuilder() - .DisplayingStartingTime() - .DisplayingElapsedTime() - .DisplayingTimeOfArrival() - .DisplayingRemainingTime() - .DisplayingItemsSummary() - .DisplayingItemsOverview() - .NotifyingProgress(onProgress) - .NotifyingCompletion(onCompletion) - .ExportingTo("output.json", FileType.Json) - .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) - .UsingComponentDescriptor(BarDescriptor.Default) - .Build(Worker.AllItems); - -var worker = new Worker() +class Program { - OnSuccess = () => reporter.ReportSuccess(), - OnFailure = () => reporter.ReportFailure(), -}; + private static Action OnProgress = (Stats stats) => + { + // TODO: Do something useful + }; + + private static Action OnCompletion = (Stats stats) => + { + // TODO: Do something useful + }; + + static async Task Main(string[] args) + { + var task = ConsoleUtils.UseAggregateReporter(args) switch + { + true => RunInstallerSample(), + false => RunSimpleSample(), + }; + + await task; + } + + private async static Task RunSimpleSample() + { + using var reporter = new ConsoleReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .DisplayingItemsSummary() + .DisplayingItemsOverview() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingComponentDescriptor(BarDescriptor.Default) + .UsingExpectedItems(SimpleWorker.ExpectedItems) + .Build(); + + var worker = new SimpleWorker() + { + OnSuccess = reporter.ReportSuccess, + OnFailure = reporter.ReportFailure, + }; + + reporter.Start(); + await worker.DoMyworkAsync(); + } + + private async static Task RunInstallerSample() + { + var worker = new InstallerWorker(); + + using var reporter = new ConsoleAggregateReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .DisplayingItemsSummary() + .DisplayingItemsOverview() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingWorkload(worker.CalcRequirements, BarDescriptor.Default) + .UsingWorkload(worker.DownloadArtifacts, BarDescriptor.Default) + .UsingWorkload(worker.InstallArtifacts, BarDescriptor.Default) + .Build(); + + worker.OnSuccess = reporter.ReportSuccess; + worker.OnFailure = reporter.ReportFailure; -reporter.Start(); -await worker.DoMywork(); \ No newline at end of file + reporter.Start(); + await worker.CalcRequirements.CalcAsync(); + Task[] restTasks = [worker.DownloadArtifacts.DownloadAsync(), worker.InstallArtifacts.InstallAsync()]; + await Task.WhenAll(restTasks); + } +} \ No newline at end of file diff --git a/src/Samples/Progress.Samples.Bar.App/Properties/launchSettings.json b/src/Samples/Progress.Samples.Bar.App/Properties/launchSettings.json new file mode 100644 index 0000000..9fe7175 --- /dev/null +++ b/src/Samples/Progress.Samples.Bar.App/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Progress.Samples.Bar.App": { + "commandName": "Project", + "commandLineArgs": "--type aggregate" + } + } +} \ No newline at end of file diff --git a/src/Samples/Progress.Samples.HearthBeat.App/Program.cs b/src/Samples/Progress.Samples.HearthBeat.App/Program.cs index ec55aae..de3728b 100644 --- a/src/Samples/Progress.Samples.HearthBeat.App/Program.cs +++ b/src/Samples/Progress.Samples.HearthBeat.App/Program.cs @@ -2,37 +2,84 @@ using Progress.Builders; using Progress.Descriptors; using Progress.Samples; +using Progress.Samples.Utils; using Progress.Settings; -var onProgress = (Stats stats) => -{ - // TODO: Do something useful -}; -var onCompletion = (Stats stats) => -{ - // TODO: Do something useful -}; - -using var reporter = new ConsoleReporterBuilder() - .DisplayingStartingTime() - .DisplayingElapsedTime() - .DisplayingTimeOfArrival() - .DisplayingRemainingTime() - .DisplayingItemsSummary() - .DisplayingItemsOverview() - .NotifyingProgress(onProgress) - .NotifyingCompletion(onCompletion) - .ExportingTo("output.csv", FileType.Csv) - .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) - .UsingComponentDescriptor(HearthBeatDescriptor.Default) - .Build(Worker.AllItems); - -var worker = new Worker() +class Program { - OnSuccess = () => reporter.ReportSuccess(), - OnFailure = () => reporter.ReportFailure(), -}; + private static Action OnProgress = (Stats stats) => + { + // TODO: Do something useful + }; + + private static Action OnCompletion = (Stats stats) => + { + // TODO: Do something useful + }; + + static async Task Main(string[] args) + { + var task = ConsoleUtils.UseAggregateReporter(args) switch + { + true => RunInstallerSample(), + false => RunSimpleSample(), + }; + + await task; + } + + private async static Task RunSimpleSample() + { + using var reporter = new ConsoleReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .DisplayingItemsSummary() + .DisplayingItemsOverview() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingComponentDescriptor(HearthBeatDescriptor.Default) + .UsingExpectedItems(SimpleWorker.ExpectedItems) + .Build(); + + var worker = new SimpleWorker() + { + OnSuccess = reporter.ReportSuccess, + OnFailure = reporter.ReportFailure, + }; + + reporter.Start(); + await worker.DoMyworkAsync(); + } + + private async static Task RunInstallerSample() + { + var worker = new InstallerWorker(); + + using var reporter = new ConsoleAggregateReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingWorkload(worker.CalcRequirements, HearthBeatDescriptor.Default) + .UsingWorkload(worker.DownloadArtifacts, HearthBeatDescriptor.Default) + .UsingWorkload(worker.InstallArtifacts, HearthBeatDescriptor.Default) + .Build(); + + worker.OnSuccess = reporter.ReportSuccess; + worker.OnFailure = reporter.ReportFailure; -reporter.Start(); -await worker.DoMywork(); \ No newline at end of file + reporter.Start(); + await worker.CalcRequirements.CalcAsync(); + Task[] restTasks = [worker.DownloadArtifacts.DownloadAsync(), worker.InstallArtifacts.InstallAsync()]; + await Task.WhenAll(restTasks); + } +} \ No newline at end of file diff --git a/src/Samples/Progress.Samples.HearthBeat.App/Properties/launchSettings.json b/src/Samples/Progress.Samples.HearthBeat.App/Properties/launchSettings.json new file mode 100644 index 0000000..6d46de4 --- /dev/null +++ b/src/Samples/Progress.Samples.HearthBeat.App/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Progress.Samples.HearthBeat.App": { + "commandName": "Project", + "commandLineArgs": "--type aggregate" + } + } +} \ No newline at end of file diff --git a/src/Samples/Progress.Samples.Pulse.App/Program.cs b/src/Samples/Progress.Samples.Pulse.App/Program.cs index acdf5cc..973edcf 100644 --- a/src/Samples/Progress.Samples.Pulse.App/Program.cs +++ b/src/Samples/Progress.Samples.Pulse.App/Program.cs @@ -2,38 +2,83 @@ using Progress.Builders; using Progress.Descriptors; using Progress.Samples; +using Progress.Samples.Utils; using Progress.Settings; -var onProgress = (Stats stats) => +class Program { - // TODO: Do something useful -}; + private static Action OnProgress = (Stats stats) => + { + // TODO: Do something useful + }; -var onCompletion = (Stats stats) => -{ - // TODO: Do something useful -}; - - -using var reporter = new ConsoleReporterBuilder() - .DisplayingStartingTime() - .DisplayingElapsedTime() - .DisplayingTimeOfArrival() - .DisplayingRemainingTime() - .DisplayingItemsSummary() - .DisplayingItemsOverview() - .NotifyingProgress(onProgress) - .NotifyingCompletion(onCompletion) - .ExportingTo("output.txt", FileType.Text) - .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) - .UsingComponentDescriptor(PulseDescriptor.Default) - .Build(Worker.AllItems); - -var worker = new Worker() -{ - OnSuccess = () => reporter.ReportSuccess(), - OnFailure = () => reporter.ReportFailure(), -}; + private static Action OnCompletion = (Stats stats) => + { + // TODO: Do something useful + }; + + static async Task Main(string[] args) + { + var task = ConsoleUtils.UseAggregateReporter(args) switch + { + true => RunInstallerSample(), + false => RunSimpleSample(), + }; + + await task; + } + + private async static Task RunSimpleSample() + { + using var reporter = new ConsoleReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .DisplayingItemsSummary() + .DisplayingItemsOverview() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingComponentDescriptor(PulseDescriptor.Default) + .UsingExpectedItems(SimpleWorker.ExpectedItems) + .Build(); + + var worker = new SimpleWorker() + { + OnSuccess = reporter.ReportSuccess, + OnFailure = reporter.ReportFailure, + }; + + reporter.Start(); + await worker.DoMyworkAsync(); + } + + private async static Task RunInstallerSample() + { + var worker = new InstallerWorker(); + + using var reporter = new ConsoleAggregateReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingWorkload(worker.CalcRequirements, PulseDescriptor.Default) + .UsingWorkload(worker.DownloadArtifacts, PulseDescriptor.Default) + .UsingWorkload(worker.InstallArtifacts, PulseDescriptor.Default) + .Build(); + + worker.OnSuccess = reporter.ReportSuccess; + worker.OnFailure = reporter.ReportFailure; -reporter.Start(); -await worker.DoMywork(); \ No newline at end of file + reporter.Start(); + await worker.CalcRequirements.CalcAsync(); + Task[] restTasks = [worker.DownloadArtifacts.DownloadAsync(), worker.InstallArtifacts.InstallAsync()]; + await Task.WhenAll(restTasks); + } +} \ No newline at end of file diff --git a/src/Samples/Progress.Samples.Pulse.App/Properties/launchSettings.json b/src/Samples/Progress.Samples.Pulse.App/Properties/launchSettings.json new file mode 100644 index 0000000..de12169 --- /dev/null +++ b/src/Samples/Progress.Samples.Pulse.App/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Progress.Samples.Pulse.App": { + "commandName": "Project", + "commandLineArgs": "--type aggregate" + } + } +} \ No newline at end of file diff --git a/src/Samples/Progress.Samples.Spinner.App/Program.cs b/src/Samples/Progress.Samples.Spinner.App/Program.cs index 00cb6b1..96bfef9 100644 --- a/src/Samples/Progress.Samples.Spinner.App/Program.cs +++ b/src/Samples/Progress.Samples.Spinner.App/Program.cs @@ -2,38 +2,83 @@ using Progress.Builders; using Progress.Descriptors; using Progress.Samples; +using Progress.Samples.Utils; using Progress.Settings; -var onProgress = (Stats stats) => +class Program { - // TODO: Do something useful -}; + private static Action OnProgress = (Stats stats) => + { + // TODO: Do something useful + }; -var onCompletion = (Stats stats) => -{ - // TODO: Do something useful -}; - - -using var reporter = new ConsoleReporterBuilder() - .DisplayingStartingTime() - .DisplayingElapsedTime() - .DisplayingTimeOfArrival() - .DisplayingRemainingTime() - .DisplayingItemsSummary() - .DisplayingItemsOverview() - .NotifyingProgress(onProgress) - .NotifyingCompletion(onCompletion) - .ExportingTo("output.xml", FileType.Xml) - .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) - .UsingComponentDescriptor(SpinnerDescriptor.Default) - .Build(Worker.AllItems); - -var worker = new Worker() -{ - OnSuccess = () => reporter.ReportSuccess(), - OnFailure = () => reporter.ReportFailure(), -}; + private static Action OnCompletion = (Stats stats) => + { + // TODO: Do something useful + }; + + static async Task Main(string[] args) + { + var task = ConsoleUtils.UseAggregateReporter(args) switch + { + true => RunInstallerSample(), + false => RunSimpleSample(), + }; + + await task; + } + + private async static Task RunSimpleSample() + { + using var reporter = new ConsoleReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .DisplayingItemsSummary() + .DisplayingItemsOverview() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingComponentDescriptor(SpinnerDescriptor.Default) + .UsingExpectedItems(SimpleWorker.ExpectedItems) + .Build(); + + var worker = new SimpleWorker() + { + OnSuccess = reporter.ReportSuccess, + OnFailure = reporter.ReportFailure, + }; + + reporter.Start(); + await worker.DoMyworkAsync(); + } + + private async static Task RunInstallerSample() + { + var worker = new InstallerWorker(); + + using var reporter = new ConsoleAggregateReporterBuilder() + .DisplayingStartingTime() + .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() + .NotifyingProgress(OnProgress) + .NotifyingCompletion(OnCompletion) + .ExportingTo("output.json", FileType.Json) + .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) + .UsingWorkload(worker.CalcRequirements, SpinnerDescriptor.Default) + .UsingWorkload(worker.DownloadArtifacts, SpinnerDescriptor.Default) + .UsingWorkload(worker.InstallArtifacts, SpinnerDescriptor.Default) + .Build(); + + worker.OnSuccess = reporter.ReportSuccess; + worker.OnFailure = reporter.ReportFailure; -reporter.Start(); -await worker.DoMywork(); \ No newline at end of file + reporter.Start(); + await worker.CalcRequirements.CalcAsync(); + Task[] restTasks = [worker.DownloadArtifacts.DownloadAsync(), worker.InstallArtifacts.InstallAsync()]; + await Task.WhenAll(restTasks); + } +} \ No newline at end of file diff --git a/src/Samples/Progress.Samples.Spinner.App/Properties/launchSettings.json b/src/Samples/Progress.Samples.Spinner.App/Properties/launchSettings.json new file mode 100644 index 0000000..611624e --- /dev/null +++ b/src/Samples/Progress.Samples.Spinner.App/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Progress.Samples.Spinner.App": { + "commandName": "Project", + "commandLineArgs": "--type aggregate" + } + } +} \ No newline at end of file diff --git a/src/Samples/Progress.Samples/InstallerWorker.cs b/src/Samples/Progress.Samples/InstallerWorker.cs new file mode 100644 index 0000000..a3d43aa --- /dev/null +++ b/src/Samples/Progress.Samples/InstallerWorker.cs @@ -0,0 +1,88 @@ +using Progress.Reporters; + +namespace Progress.Samples; + +public class InstallerWorker +{ + public Action OnSuccess { get; set; } = default!; + public Action OnFailure { get; set; } = default!; + + public CalcRequirementsWorkload CalcRequirements { get; } + public DownloadArtifactsWorkload DownloadArtifacts { get; } + public InstallArtifactsWorkload InstallArtifacts { get; } + + public InstallerWorker() + { + CalcRequirements = new(this); + DownloadArtifacts = new(this); + InstallArtifacts = new(this); + } + + public class CalcRequirementsWorkload(InstallerWorker worker) : Workload("Calculate", "Calculating physical requirements", 3) + { + public async Task CalcAsync() + { + for (ulong i = 0; i < ItemsCount; i++) + { + worker.OnSuccess(Id); + await Task.Delay(TimeSpan.FromSeconds(2)); + } + } + } + + public class DownloadArtifactsWorkload(InstallerWorker worker) : Workload("Download", "Downloading artifacts", 15) + { + public async Task DownloadAsync() + { + Random rnd = Random.Shared; + + for (ulong i = 0; i < ItemsCount; i++) + { + worker.OnSuccess(Id); + double nextSecond = rnd.NextDouble(); + await Task.Delay(TimeSpan.FromMilliseconds(nextSecond * 1000)); + } + } + } + + public class InstallArtifactsWorkload(InstallerWorker worker) : Workload("Install", "Installing artifacts", 15000) + { + public async Task InstallAsync() + { + const long BatchSize = 500; + + ulong rem = ItemsCount % BatchSize; + int batchCount = (int)(ItemsCount / BatchSize); + if (rem > 0) + batchCount++; + + string[][] batches = Enumerable.Range(0, batchCount) + .Select(i => + { + if (rem > 0) + { + return i < batchCount - 1 + ? new string[BatchSize] + : new string[ItemsCount % BatchSize]; + } + + return new string[BatchSize]; + }) + .ToArray(); + + await Parallel.ForEachAsync(batches, async (items, ct) => + { + for (int i = 0; i < items.Length; i++) + { + if (i % 3 == 0) + await Task.Delay(TimeSpan.FromMilliseconds(10)); + + if (i % 7 == 0) + worker.OnSuccess?.Invoke(Id); + else + worker.OnFailure?.Invoke(Id); + } + }); + } + } +} diff --git a/src/Samples/Progress.Samples/Progress.Samples.csproj b/src/Samples/Progress.Samples/Progress.Samples.csproj index 758a2d0..0f1e85f 100644 --- a/src/Samples/Progress.Samples/Progress.Samples.csproj +++ b/src/Samples/Progress.Samples/Progress.Samples.csproj @@ -1,2 +1,5 @@  + + + diff --git a/src/Samples/Progress.Samples/Worker.cs b/src/Samples/Progress.Samples/SimpleWorker.cs similarity index 75% rename from src/Samples/Progress.Samples/Worker.cs rename to src/Samples/Progress.Samples/SimpleWorker.cs index 995f1cd..cb9773f 100644 --- a/src/Samples/Progress.Samples/Worker.cs +++ b/src/Samples/Progress.Samples/SimpleWorker.cs @@ -1,17 +1,17 @@ namespace Progress.Samples; -public class Worker +public class SimpleWorker { - public const long AllItems = 85161; + public const long ExpectedItems = 85161; private const long BatchSize = 500; public Action OnSuccess { get; set; } = default!; public Action OnFailure { get; set; } = default!; - public async Task DoMywork() + public async Task DoMyworkAsync() { - long rem = AllItems % BatchSize; - int batchCount = (int)(AllItems / BatchSize); + long rem = ExpectedItems % BatchSize; + int batchCount = (int)(ExpectedItems / BatchSize); if (rem > 0) batchCount++; @@ -22,7 +22,7 @@ public async Task DoMywork() { return i < batchCount - 1 ? new string[BatchSize] - : new string[AllItems % BatchSize]; + : new string[ExpectedItems % BatchSize]; } return new string[BatchSize]; diff --git a/src/Samples/Progress.Samples/Utils/ConsoleUtils.cs b/src/Samples/Progress.Samples/Utils/ConsoleUtils.cs new file mode 100644 index 0000000..88b3553 --- /dev/null +++ b/src/Samples/Progress.Samples/Utils/ConsoleUtils.cs @@ -0,0 +1,12 @@ +namespace Progress.Samples.Utils; + +public static class ConsoleUtils +{ + public static bool UseAggregateReporter(string[] args) + { + if (args.Length < 2) + return false; + + return args[0] == "--type" && args[1] == "aggregate"; + } +}