Skip to content

Commit 8bbffcd

Browse files
Fix Akka.Util.Result edge case (#7433)
Co-authored-by: Aaron Stannard <aaron@petabridge.com>
1 parent 0a01453 commit 8bbffcd

File tree

2 files changed

+216
-1
lines changed

2 files changed

+216
-1
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// -----------------------------------------------------------------------
2+
// <copyright file="ResultSpec.cs" company="Akka.NET Project">
3+
// Copyright (C) 2009-2024 Lightbend Inc. <http://www.lightbend.com>
4+
// Copyright (C) 2013-2024 .NET Foundation <https://github.com/akkadotnet/akka.net>
5+
// </copyright>
6+
// -----------------------------------------------------------------------
7+
8+
using System;
9+
using System.Threading.Tasks;
10+
using Akka.Util;
11+
using FluentAssertions;
12+
using Xunit;
13+
using static FluentAssertions.FluentActions;
14+
15+
namespace Akka.Tests.Util;
16+
17+
public class ResultSpec
18+
{
19+
[Fact(DisplayName = "Result constructor with value should return success")]
20+
public void SuccessfulResult()
21+
{
22+
var result = new Result<int>(1);
23+
24+
result.IsSuccess.Should().BeTrue();
25+
result.Value.Should().Be(1);
26+
result.Exception.Should().BeNull();
27+
}
28+
29+
[Fact(DisplayName = "Result constructor with exception should return failed")]
30+
public void ExceptionResult()
31+
{
32+
var result = new Result<int>(new TestException("BOOM"));
33+
34+
result.IsSuccess.Should().BeFalse();
35+
result.Exception.Should().NotBeNull();
36+
result.Exception.Should().BeOfType<TestException>();
37+
}
38+
39+
[Fact(DisplayName = "Result.Success with value should return success")]
40+
public void SuccessfulStaticSuccess()
41+
{
42+
var result = Result.Success(1);
43+
44+
result.IsSuccess.Should().BeTrue();
45+
result.Value.Should().Be(1);
46+
result.Exception.Should().BeNull();
47+
}
48+
49+
[Fact(DisplayName = "Result.Failure with exception should return failed")]
50+
public void ExceptionStaticFailure()
51+
{
52+
var result = Result.Failure<int>(new TestException("BOOM"));
53+
54+
result.IsSuccess.Should().BeFalse();
55+
result.Exception.Should().NotBeNull();
56+
result.Exception.Should().BeOfType<TestException>();
57+
}
58+
59+
[Fact(DisplayName = "Result.From with successful Func should return success")]
60+
public void SuccessfulFuncResult()
61+
{
62+
var result = Result.From(() => 1);
63+
64+
result.IsSuccess.Should().BeTrue();
65+
result.Value.Should().Be(1);
66+
result.Exception.Should().BeNull();
67+
}
68+
69+
[Fact(DisplayName = "Result.From with throwing Func should return failed")]
70+
public void ThrowFuncResult()
71+
{
72+
var result = Result.From<int>(() => throw new TestException("BOOM"));
73+
74+
result.IsSuccess.Should().BeFalse();
75+
result.Exception.Should().NotBeNull();
76+
result.Exception.Should().BeOfType<TestException>();
77+
}
78+
79+
[Fact(DisplayName = "Result.FromTask with successful task should return success")]
80+
public void SuccessfulTaskResult()
81+
{
82+
var task = CompletedTask(1);
83+
var result = Result.FromTask(task);
84+
85+
result.IsSuccess.Should().BeTrue();
86+
result.Value.Should().Be(1);
87+
result.Exception.Should().BeNull();
88+
}
89+
90+
[Fact(DisplayName = "Result.FromTask with faulted task should return failed")]
91+
public void FaultedTaskResult()
92+
{
93+
var task = FaultedTask(1);
94+
var result = Result.FromTask(task);
95+
96+
result.IsSuccess.Should().BeFalse();
97+
result.Exception.Should().NotBeNull();
98+
result.Exception.Should().BeOfType<AggregateException>()
99+
.Which.InnerException.Should().BeOfType<TestException>();
100+
}
101+
102+
[Fact(DisplayName = "Result.FromTask with cancelled task should return failed")]
103+
public void CancelledTaskResult()
104+
{
105+
var task = CancelledTask(1);
106+
var result = Result.FromTask(task);
107+
108+
result.IsSuccess.Should().BeFalse();
109+
result.Exception.Should().NotBeNull();
110+
result.Exception.Should().BeOfType<TaskCanceledException>();
111+
}
112+
113+
[Fact(DisplayName = "Result.FromTask with incomplete task should throw")]
114+
public void IncompleteTaskResult()
115+
{
116+
var tcs = new TaskCompletionSource<int>();
117+
Invoking(() => Result.FromTask(tcs.Task))
118+
.Should().Throw<ArgumentException>().WithMessage("Task is not completed.*");
119+
}
120+
121+
private static Task<int> CompletedTask(int n)
122+
{
123+
var tcs = new TaskCompletionSource<int>();
124+
Task.Run(async () =>
125+
{
126+
await Task.Yield();
127+
tcs.TrySetResult(n);
128+
});
129+
tcs.Task.Wait();
130+
return tcs.Task;
131+
}
132+
133+
private static Task<int> CancelledTask(int n)
134+
{
135+
var tcs = new TaskCompletionSource<int>();
136+
Task.Run(async () =>
137+
{
138+
await Task.Yield();
139+
tcs.TrySetCanceled();
140+
});
141+
142+
try
143+
{
144+
tcs.Task.Wait();
145+
}
146+
catch
147+
{
148+
// no-op
149+
}
150+
151+
return tcs.Task;
152+
}
153+
154+
private static Task<int> FaultedTask(int n)
155+
{
156+
var tcs = new TaskCompletionSource<int>();
157+
Task.Run(async () =>
158+
{
159+
await Task.Yield();
160+
try
161+
{
162+
throw new TestException("BOOM");
163+
}
164+
catch (Exception ex)
165+
{
166+
tcs.TrySetException(ex);
167+
}
168+
});
169+
170+
try
171+
{
172+
tcs.Task.Wait();
173+
}
174+
catch
175+
{
176+
// no-op
177+
}
178+
179+
return tcs.Task;
180+
}
181+
182+
private class TestException: Exception
183+
{
184+
public TestException(string message) : base(message)
185+
{
186+
}
187+
188+
public TestException(string message, Exception innerException) : base(message, innerException)
189+
{
190+
}
191+
}
192+
}

src/core/Akka/Util/Result.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,30 @@ public static Result<T> Failure<T>(Exception exception)
137137
/// <returns>TBD</returns>
138138
public static Result<T> FromTask<T>(Task<T> task)
139139
{
140-
return task.IsCanceled || task.IsFaulted ? new Result<T>(task.Exception) : new Result<T>(task.Result);
140+
if(!task.IsCompleted)
141+
throw new ArgumentException("Task is not completed. Result.FromTask only accepts completed tasks.", nameof(task));
142+
143+
if(task.Exception is not null)
144+
return new Result<T>(task.Exception);
145+
146+
if (task.IsCanceled && task.Exception is null)
147+
{
148+
try
149+
{
150+
_ = task.GetAwaiter().GetResult();
151+
}
152+
catch(Exception e)
153+
{
154+
return new Result<T>(e);
155+
}
156+
157+
throw new InvalidOperationException("Should never reach this line!");
158+
}
159+
160+
if(task.IsFaulted && task.Exception is null)
161+
throw new InvalidOperationException("Should never happen! something is wrong with .NET Task code!");
162+
163+
return new Result<T>(task.Result);
141164
}
142165

143166
/// <summary>

0 commit comments

Comments
 (0)