Skip to content
Merged
71 changes: 71 additions & 0 deletions PowerKit.Tests/Extensions/TaskExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Threading.Tasks;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;

namespace PowerKit.Tests.Extensions;

public class TaskExtensionsTests
{
[Fact]
public async Task ObserveException_Task_DoesNotThrowUnobservedException_Test()
{
// Arrange
var task = Task.Run(() => throw new InvalidOperationException("test error"));
task.ObserveException();

// Act & assert: waiting for the task to complete should not raise an unobserved exception
await Task.Delay(100);

// Force GC to collect the task and trigger finalizer-based unobserved exception detection
GC.Collect();
GC.WaitForPendingFinalizers();

// The exception was observed, so no UnobservedTaskException should have been raised
task.IsFaulted.Should().BeTrue();
}

[Fact]
public async Task ObserveException_TaskOfT_DoesNotThrowUnobservedException_Test()
{
// Arrange
var task = Task.Run(new Func<int>(() => throw new InvalidOperationException("test error")));
task.ObserveException();

// Act & assert
await Task.Delay(100);

GC.Collect();
GC.WaitForPendingFinalizers();

task.IsFaulted.Should().BeTrue();
}

[Fact]
public async Task ObserveException_Task_SuccessfulTask_IsUnaffected_Test()
{
// Arrange
var task = Task.CompletedTask;
task.ObserveException();

// Act & assert
await Task.Delay(50);

task.IsCompletedSuccessfully.Should().BeTrue();
}

[Fact]
public async Task ObserveException_TaskOfT_SuccessfulTask_IsUnaffected_Test()
{
// Arrange
var task = Task.FromResult(42);
task.ObserveException();

// Act & assert
await Task.Delay(50);

task.IsCompletedSuccessfully.Should().BeTrue();
(await task).Should().Be(42);
}
}
44 changes: 44 additions & 0 deletions PowerKit/Extensions/TaskExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#if !NETFRAMEWORK || NET45_OR_GREATER
using System;
using System.Threading.Tasks;

namespace PowerKit.Extensions;

/// <summary>
/// Extensions for <see cref="Task" />.
/// </summary>
public static class TaskExtensions
{
extension(Task task)
{
/// <summary>
/// Registers a continuation that observes and suppresses the task's exception,
/// preventing it from surfacing as an unobserved task exception.
/// Intended for use on detached (fire-and-forget) tasks.
/// </summary>
public void ObserveException() =>
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
task.ContinueWith(
t => _ = t.Exception,
default,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.Default
Comment thread
Copilot marked this conversation as resolved.
);
}

extension<T>(Task<T> task)
{
/// <summary>
/// Registers a continuation that observes and suppresses the task's exception,
/// preventing it from surfacing as an unobserved task exception.
/// Intended for use on detached (fire-and-forget) tasks.
/// </summary>
public void ObserveException() =>
task.ContinueWith(
t => _ = t.Exception,
default,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.Default
);
}
}
#endif