Skip to content

Commit 36fedf1

Browse files
committed
feat: enhance progress and status handling with cancellation support and configuration actions
1 parent 2341b76 commit 36fedf1

8 files changed

Lines changed: 102 additions & 59 deletions

File tree

src/Extensions/ILoggerExtensions.cs

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading;
23
using System.Threading.Tasks;
34
using Spectre.Console;
45
using Spectre.Console.Rendering;
@@ -10,8 +11,6 @@ namespace WB.Logging.LogSinks.Console.Spectre;
1011
/// </summary>
1112
public static class ILoggerExtensions
1213
{
13-
private static readonly ProgressConfiguration defaultProgressConfiguration = new();
14-
1514
// ┌─────────────────────────────────────────────────────────────────────────────┐
1615
// │ Public Methods │
1716
// └─────────────────────────────────────────────────────────────────────────────┘
@@ -82,19 +81,21 @@ public static ILogger HorizontalRule(this ILogger @this, string? title = null)
8281
/// <param name="this">The <see cref="ILogger"/> instance to start the progress on.</param>
8382
/// <param name="progressConfiguration">The configuration for the progress. This includes settings such as
8483
/// <param name="action">The function that performs the progress.</param>
84+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
8585
/// <returns>A task that represents the asynchronous operation.</returns>
8686
/// <seealso cref="ProgressConfiguration"/>
87-
public static async Task StartProgressAsync(this ILogger @this, ProgressConfiguration progressConfiguration, Func<ProgressContext, Task> action)
87+
public static async Task StartProgressAsync(this ILogger @this, Action<Progress> progress, Func<ProgressContext, CancellationToken, Task> action, CancellationToken cancellationToken = default)
8888
{
8989
ArgumentNullException.ThrowIfNull(@this);
9090

9191
ProgressPayload progressPayload = new()
9292
{
93-
Configuration = progressConfiguration,
94-
Action = action,
93+
ProgressConfigurationAction = progress,
94+
ProgressAction = action,
95+
CancellationToken = cancellationToken,
9596
};
9697

97-
await @this.FlushAsync().ConfigureAwait(false);
98+
await @this.FlushAsync(cancellationToken).ConfigureAwait(false);
9899

99100
@this.Log(null, progressPayload);
100101

@@ -108,17 +109,18 @@ public static async Task StartProgressAsync(this ILogger @this, ProgressConfigur
108109
/// <param name="this">The <see cref="ILogger"/> instance to start the progress on.</param>
109110
/// <param name="action">The function that performs the progress.</param>
110111
/// <returns>A task that represents the asynchronous operation.</returns>
111-
public static async Task StartProgressAsync(this ILogger @this, Func<ProgressContext, Task> action)
112-
=> await StartProgressAsync(@this, defaultProgressConfiguration, action).ConfigureAwait(false);
112+
public static async Task StartProgressAsync(this ILogger @this, Func<ProgressContext, CancellationToken, Task> action, CancellationToken cancellationToken = default)
113+
=> await StartProgressAsync(@this, _ => { }, action, cancellationToken).ConfigureAwait(false);
113114

114-
public static async Task StartStatusAsync(this ILogger @this, StatusConfiguration statusConfiguration, Func<StatusContext, Task> action)
115+
public static async Task StartStatusAsync(this ILogger @this, string message, Action<Status> status, Func<StatusContext, Task> action)
115116
{
116117
ArgumentNullException.ThrowIfNull(@this);
117118

118119
StatusPayload statusPayload = new()
119120
{
120-
Configuration = statusConfiguration,
121-
Action = action,
121+
StatusConfigurationAction = status,
122+
StatusAction = action,
123+
StatusMessage = message,
122124
};
123125

124126
await @this.FlushAsync().ConfigureAwait(false);
@@ -129,11 +131,5 @@ public static async Task StartStatusAsync(this ILogger @this, StatusConfiguratio
129131
}
130132

131133
public static Task StartStatusAsync(this ILogger @this, string statusMessage, Func<StatusContext, Task> action)
132-
{
133-
ArgumentNullException.ThrowIfNull(@this);
134-
135-
StatusConfiguration statusConfiguration = new(statusMessage);
136-
137-
return StartStatusAsync(@this, statusConfiguration, action);
138-
}
134+
=> StartStatusAsync(@this, statusMessage, _ => { }, action);
139135
}

src/LogMessageWriters/ProgressConsoleMessageWriter.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ internal sealed class ProgressConsoleMessageWriter : IAsyncLogMessageWriter<Prog
2929
/// <inheritdoc/>
3030
public async ValueTask WriteAsync(DateTimeOffset timestamp, LogLevel? logLevel, IEnumerable<string> senders, ProgressPayload payload)
3131
{
32-
ProgressConfiguration progressConfiguration = payload.Configuration;
33-
3432
#pragma warning disable CA1031 // Do not catch general exception types
3533
try
3634
{
37-
await Writer.Progress()
38-
.AutoClear(progressConfiguration.AutoClear)
39-
.AutoRefresh(progressConfiguration.AutoRefresh)
40-
.HideCompleted(progressConfiguration.HideCompleted)
41-
.StartAsync(payload.Action).ConfigureAwait(false);
35+
using IDisposable _ = payload.CancellationToken.Register(payload.SetCanceled);
36+
37+
Progress progress = Writer.Progress();
38+
39+
payload.ProgressConfigurationAction(progress);
40+
41+
await progress.StartAsync(context => payload.ProgressAction(context, payload.CancellationToken)).ConfigureAwait(false);
4242

4343
payload.SetCompleted();
4444
}

src/LogMessageWriters/StatusConsoleMessageWriter.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,14 @@ internal sealed class StatusConsoleMessageWriter : IAsyncLogMessageWriter<Status
2929
/// <inheritdoc/>
3030
public async ValueTask WriteAsync(DateTimeOffset timestamp, LogLevel? logLevel, IEnumerable<string> senders, StatusPayload payload)
3131
{
32-
StatusConfiguration statusConfiguration = payload.Configuration;
33-
3432
#pragma warning disable CA1031 // Do not catch general exception types
3533
try
3634
{
37-
await Writer.Status()
38-
.AutoRefresh(statusConfiguration.AutoRefresh)
39-
.Spinner(statusConfiguration.Spinner)
40-
.StartAsync(statusConfiguration.StatusMessage, payload.Action).ConfigureAwait(false);
35+
Status status = Writer.Status();
36+
37+
payload.StatusConfigurationAction(status);
38+
39+
await status.StartAsync(payload.StatusMessage, payload.StatusAction).ConfigureAwait(false);
4140

4241
payload.SetCompleted();
4342
}

src/Payloads/ProgressPayload.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading;
23
using System.Threading.Tasks;
34
using Spectre.Console;
45

@@ -19,14 +20,11 @@ internal sealed class ProgressPayload
1920
// │ Public Properties │
2021
// └─────────────────────────────────────────────────────────────────────────────┘
2122

22-
/// <summary>
23-
/// Gets or sets the configuration for the progress. This includes settings such as
24-
/// whether the progress should be automatically cleared, refreshed, or hide completed tasks.
25-
/// </summary>
26-
/// <seealso cref="ProgressConfiguration"/>
27-
public required ProgressConfiguration Configuration { get; init; }
23+
public required Func<ProgressContext, CancellationToken, Task> ProgressAction { get; init; }
24+
25+
public Action<Progress> ProgressConfigurationAction { get; init; } = _ => { };
2826

29-
public required Func<ProgressContext, Task> Action { get; init; }
27+
public CancellationToken CancellationToken { get; init; } = CancellationToken.None;
3028

3129
/// <summary>
3230
/// Gets a <see cref="Task"/> that represents the completion of the progress. The
@@ -55,4 +53,11 @@ public void SetCompleted()
5553
/// <param name="exception">The exception that caused the progress to fail.</param>
5654
public void SetException(Exception exception)
5755
=> taskCompletionSource.TrySetException(exception);
56+
57+
/// <summary>
58+
/// Sets the progress as canceled, which will complete the <see cref="Completed"/>
59+
/// task as canceled.
60+
/// </summary>
61+
public void SetCanceled()
62+
=> taskCompletionSource.TrySetCanceled();
5863
}

src/Payloads/StatusPayload.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ internal sealed class StatusPayload
1919
// │ Public Properties │
2020
// └─────────────────────────────────────────────────────────────────────────────┘
2121

22-
public required StatusConfiguration Configuration { get; init; }
22+
public required string StatusMessage { get; init; }
2323

2424
/// <summary>
2525
/// Gets or sets the action that performs the status update.
2626
/// </summary>
27-
public required Func<StatusContext, Task> Action { get; init; }
27+
public required Func<StatusContext, Task> StatusAction { get; init; }
28+
29+
public Action<Status> StatusConfigurationAction { get; init; } = _ => { };
2830

2931
/// <summary>
3032
/// Gets a <see cref="Task"/> that represents the completion of the status. The

src/ProgressConfiguration.cs

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/StatusConfiguration.cs

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/ExtensionsTests/src/ILoggerExtensionsTests/StartProgressAsyncMethodTests.cs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System;
2+
using System.Threading;
13
using System.Threading.Tasks;
4+
using AwesomeAssertions;
25
using Spectre.Console.Testing;
36
using VerifyTUnit;
47
using WB.Logging;
@@ -23,19 +26,73 @@ public async Task ShouldStartAProgressTaskAndLogItToTheConsole()
2326
});
2427

2528
// Act
26-
await logger.StartProgressAsync("Processing...", async progress =>
29+
await logger.StartProgressAsync(async (progress, cancellationToken) =>
2730
{
2831
var task = progress.AddTask("Processing files", maxValue: 100);
2932

3033
while (!progress.IsFinished)
3134
{
3235
task.Increment(1);
3336

34-
await Task.Delay(100).ConfigureAwait(false);
37+
await Task.Delay(10).ConfigureAwait(false);
3538
}
3639
}).ConfigureAwait(false);
3740
await logger.FlushAsync().ConfigureAwait(false);
3841

39-
await Verifier.Verify(testConsole.Output).ConfigureAwait(false);
42+
testConsole.Output.Should().NotBeEmpty(because: "a progress should have been logged to the console");
43+
}
44+
45+
[Test]
46+
public async Task ShouldThrowTheExceptionFromProgressAction()
47+
{
48+
// Arrange
49+
TestConsole testConsole = new()
50+
{
51+
EmitAnsiSequences = true,
52+
};
53+
ILogger logger = new Logger("Test");
54+
logger.AttachSpectreConsole(logSink =>
55+
{
56+
logSink.Writer = testConsole;
57+
});
58+
59+
// Act
60+
Func<Task> action = () => logger.StartProgressAsync(async (progress, cancellationToken) =>
61+
{
62+
throw new Exception("Something went wrong during progress.");
63+
});
64+
65+
// Assert
66+
await action.Should().ThrowAsync<Exception>(because: "the exception from the progress action should be propagated");
67+
}
68+
69+
[Test]
70+
public async Task ShouldBeCancellable()
71+
{
72+
// Arrange
73+
TestConsole testConsole = new()
74+
{
75+
EmitAnsiSequences = true,
76+
};
77+
ILogger logger = new Logger("Test");
78+
logger.AttachSpectreConsole(logSink =>
79+
{
80+
logSink.Writer = testConsole;
81+
});
82+
83+
using CancellationTokenSource cancellationTokenSource = new();
84+
85+
// Act
86+
Func<Task> action = () => logger.StartProgressAsync(async (progress, cancellationToken) =>
87+
{
88+
while (true)
89+
{
90+
cancellationToken.ThrowIfCancellationRequested();
91+
}
92+
}, cancellationTokenSource.Token);
93+
cancellationTokenSource.Cancel();
94+
95+
// Assert
96+
await action.Should().ThrowAsync<OperationCanceledException>(because: "the progress should be canceled when the cancellation token is canceled");
4097
}
4198
}

0 commit comments

Comments
 (0)