Skip to content
Merged
102 changes: 102 additions & 0 deletions PowerKit.Tests/Extensions/TaskExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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_ReturnsFaultException_Test()
{
// Arrange
var task = Task.Run(() => throw new InvalidOperationException("test error"));

// Act
var exception = await task.ObserveException();

// Assert
task.IsFaulted.Should().BeTrue();
exception.Should().NotBeNull();
exception!.InnerException.Should().BeOfType<InvalidOperationException>();
}

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

// Act
var exception = await task.ObserveException();

// Assert
task.IsFaulted.Should().BeTrue();
exception.Should().NotBeNull();
exception!.InnerException.Should().BeOfType<InvalidOperationException>();
}

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

// Act
var exception = await task.ObserveException();

// Assert
task.IsCompletedSuccessfully.Should().BeTrue();
exception.Should().BeNull();
}

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

// Act
var exception = await task.ObserveException();

// Assert
task.IsCompletedSuccessfully.Should().BeTrue();
(await task).Should().Be(42);
exception.Should().BeNull();
}

[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();

task.IsFaulted.Should().BeTrue();
}
Comment thread
Copilot marked this conversation as resolved.
Outdated

[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();
}
Comment thread
Copilot marked this conversation as resolved.
Outdated
}
48 changes: 48 additions & 0 deletions PowerKit/Extensions/TaskExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#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.
/// Returns a <see cref="Task{TResult}" /> that resolves to the observed
/// <see cref="AggregateException" />, or <see langword="null" /> if the task did not fault.
/// Intended for use on detached (fire-and-forget) tasks.
/// </summary>
public Task<AggregateException?> ObserveException() =>
task.ContinueWith(
t => t.Exception,
default,
TaskContinuationOptions.None,
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.
/// Returns a <see cref="Task{TResult}" /> that resolves to the observed
/// <see cref="AggregateException" />, or <see langword="null" /> if the task did not fault.
/// Intended for use on detached (fire-and-forget) tasks.
/// </summary>
public Task<AggregateException?> ObserveException() =>
task.ContinueWith(
t => t.Exception,
default,
TaskContinuationOptions.None,
TaskScheduler.Default
Comment thread
Copilot marked this conversation as resolved.
Outdated
);
}
}
#endif