Skip to content

Commit

Permalink
Fix Akka.Util.Result edge case (#7433)
Browse files Browse the repository at this point in the history
Co-authored-by: Aaron Stannard <[email protected]>
  • Loading branch information
Arkatufus and Aaronontheweb authored Dec 23, 2024
1 parent 0a01453 commit 8bbffcd
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 1 deletion.
192 changes: 192 additions & 0 deletions src/core/Akka.Tests/Util/ResultSpec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// -----------------------------------------------------------------------
// <copyright file="ResultSpec.cs" company="Akka.NET Project">
// Copyright (C) 2009-2024 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2024 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using System;
using System.Threading.Tasks;
using Akka.Util;
using FluentAssertions;
using Xunit;
using static FluentAssertions.FluentActions;

namespace Akka.Tests.Util;

public class ResultSpec
{
[Fact(DisplayName = "Result constructor with value should return success")]
public void SuccessfulResult()
{
var result = new Result<int>(1);

result.IsSuccess.Should().BeTrue();
result.Value.Should().Be(1);
result.Exception.Should().BeNull();
}

[Fact(DisplayName = "Result constructor with exception should return failed")]
public void ExceptionResult()
{
var result = new Result<int>(new TestException("BOOM"));

result.IsSuccess.Should().BeFalse();
result.Exception.Should().NotBeNull();
result.Exception.Should().BeOfType<TestException>();
}

[Fact(DisplayName = "Result.Success with value should return success")]
public void SuccessfulStaticSuccess()
{
var result = Result.Success(1);

result.IsSuccess.Should().BeTrue();
result.Value.Should().Be(1);
result.Exception.Should().BeNull();
}

[Fact(DisplayName = "Result.Failure with exception should return failed")]
public void ExceptionStaticFailure()
{
var result = Result.Failure<int>(new TestException("BOOM"));

result.IsSuccess.Should().BeFalse();
result.Exception.Should().NotBeNull();
result.Exception.Should().BeOfType<TestException>();
}

[Fact(DisplayName = "Result.From with successful Func should return success")]
public void SuccessfulFuncResult()
{
var result = Result.From(() => 1);

result.IsSuccess.Should().BeTrue();
result.Value.Should().Be(1);
result.Exception.Should().BeNull();
}

[Fact(DisplayName = "Result.From with throwing Func should return failed")]
public void ThrowFuncResult()
{
var result = Result.From<int>(() => throw new TestException("BOOM"));

result.IsSuccess.Should().BeFalse();
result.Exception.Should().NotBeNull();
result.Exception.Should().BeOfType<TestException>();
}

[Fact(DisplayName = "Result.FromTask with successful task should return success")]
public void SuccessfulTaskResult()
{
var task = CompletedTask(1);
var result = Result.FromTask(task);

result.IsSuccess.Should().BeTrue();
result.Value.Should().Be(1);
result.Exception.Should().BeNull();
}

[Fact(DisplayName = "Result.FromTask with faulted task should return failed")]
public void FaultedTaskResult()
{
var task = FaultedTask(1);
var result = Result.FromTask(task);

result.IsSuccess.Should().BeFalse();
result.Exception.Should().NotBeNull();
result.Exception.Should().BeOfType<AggregateException>()
.Which.InnerException.Should().BeOfType<TestException>();
}

[Fact(DisplayName = "Result.FromTask with cancelled task should return failed")]
public void CancelledTaskResult()
{
var task = CancelledTask(1);
var result = Result.FromTask(task);

result.IsSuccess.Should().BeFalse();
result.Exception.Should().NotBeNull();
result.Exception.Should().BeOfType<TaskCanceledException>();
}

[Fact(DisplayName = "Result.FromTask with incomplete task should throw")]
public void IncompleteTaskResult()
{
var tcs = new TaskCompletionSource<int>();
Invoking(() => Result.FromTask(tcs.Task))
.Should().Throw<ArgumentException>().WithMessage("Task is not completed.*");
}

private static Task<int> CompletedTask(int n)
{
var tcs = new TaskCompletionSource<int>();
Task.Run(async () =>
{
await Task.Yield();
tcs.TrySetResult(n);
});
tcs.Task.Wait();
return tcs.Task;
}

private static Task<int> CancelledTask(int n)
{
var tcs = new TaskCompletionSource<int>();
Task.Run(async () =>
{
await Task.Yield();
tcs.TrySetCanceled();
});

try
{
tcs.Task.Wait();
}
catch
{
// no-op
}

return tcs.Task;
}

private static Task<int> FaultedTask(int n)
{
var tcs = new TaskCompletionSource<int>();
Task.Run(async () =>
{
await Task.Yield();
try
{
throw new TestException("BOOM");
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});

try
{
tcs.Task.Wait();
}
catch
{
// no-op
}

return tcs.Task;
}

private class TestException: Exception
{
public TestException(string message) : base(message)
{
}

public TestException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
25 changes: 24 additions & 1 deletion src/core/Akka/Util/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,30 @@ public static Result<T> Failure<T>(Exception exception)
/// <returns>TBD</returns>
public static Result<T> FromTask<T>(Task<T> task)
{
return task.IsCanceled || task.IsFaulted ? new Result<T>(task.Exception) : new Result<T>(task.Result);
if(!task.IsCompleted)
throw new ArgumentException("Task is not completed. Result.FromTask only accepts completed tasks.", nameof(task));

if(task.Exception is not null)
return new Result<T>(task.Exception);

if (task.IsCanceled && task.Exception is null)
{
try
{
_ = task.GetAwaiter().GetResult();
}
catch(Exception e)
{
return new Result<T>(e);
}

throw new InvalidOperationException("Should never reach this line!");
}

if(task.IsFaulted && task.Exception is null)
throw new InvalidOperationException("Should never happen! something is wrong with .NET Task code!");

return new Result<T>(task.Result);
}

/// <summary>
Expand Down

0 comments on commit 8bbffcd

Please sign in to comment.