From 8bbffcdc005af870fc080e94cc971001c3a5a48e Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Tue, 24 Dec 2024 00:22:34 +0700 Subject: [PATCH] Fix `Akka.Util.Result` edge case (#7433) Co-authored-by: Aaron Stannard --- src/core/Akka.Tests/Util/ResultSpec.cs | 192 +++++++++++++++++++++++++ src/core/Akka/Util/Result.cs | 25 +++- 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/core/Akka.Tests/Util/ResultSpec.cs diff --git a/src/core/Akka.Tests/Util/ResultSpec.cs b/src/core/Akka.Tests/Util/ResultSpec.cs new file mode 100644 index 00000000000..8a77d3ddc98 --- /dev/null +++ b/src/core/Akka.Tests/Util/ResultSpec.cs @@ -0,0 +1,192 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2024 Lightbend Inc. +// Copyright (C) 2013-2024 .NET Foundation +// +// ----------------------------------------------------------------------- + +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(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(new TestException("BOOM")); + + result.IsSuccess.Should().BeFalse(); + result.Exception.Should().NotBeNull(); + result.Exception.Should().BeOfType(); + } + + [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(new TestException("BOOM")); + + result.IsSuccess.Should().BeFalse(); + result.Exception.Should().NotBeNull(); + result.Exception.Should().BeOfType(); + } + + [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(() => throw new TestException("BOOM")); + + result.IsSuccess.Should().BeFalse(); + result.Exception.Should().NotBeNull(); + result.Exception.Should().BeOfType(); + } + + [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() + .Which.InnerException.Should().BeOfType(); + } + + [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(); + } + + [Fact(DisplayName = "Result.FromTask with incomplete task should throw")] + public void IncompleteTaskResult() + { + var tcs = new TaskCompletionSource(); + Invoking(() => Result.FromTask(tcs.Task)) + .Should().Throw().WithMessage("Task is not completed.*"); + } + + private static Task CompletedTask(int n) + { + var tcs = new TaskCompletionSource(); + Task.Run(async () => + { + await Task.Yield(); + tcs.TrySetResult(n); + }); + tcs.Task.Wait(); + return tcs.Task; + } + + private static Task CancelledTask(int n) + { + var tcs = new TaskCompletionSource(); + Task.Run(async () => + { + await Task.Yield(); + tcs.TrySetCanceled(); + }); + + try + { + tcs.Task.Wait(); + } + catch + { + // no-op + } + + return tcs.Task; + } + + private static Task FaultedTask(int n) + { + var tcs = new TaskCompletionSource(); + 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) + { + } + } +} \ No newline at end of file diff --git a/src/core/Akka/Util/Result.cs b/src/core/Akka/Util/Result.cs index 5d9a4c02fb6..9969a0080b0 100644 --- a/src/core/Akka/Util/Result.cs +++ b/src/core/Akka/Util/Result.cs @@ -137,7 +137,30 @@ public static Result Failure(Exception exception) /// TBD public static Result FromTask(Task task) { - return task.IsCanceled || task.IsFaulted ? new Result(task.Exception) : new Result(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(task.Exception); + + if (task.IsCanceled && task.Exception is null) + { + try + { + _ = task.GetAwaiter().GetResult(); + } + catch(Exception e) + { + return new Result(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(task.Result); } ///