@@ -0,0 +1,185 @@
|
||||
namespace Raliway.Tests.Results;
|
||||
|
||||
public class Bugs
|
||||
{
|
||||
#region BUG-1: Result.Equals asymmetry
|
||||
|
||||
[Fact]
|
||||
public void Result_Success_ShouldNotEqual_Default()
|
||||
{
|
||||
// default(Result) has State=Bottom — it's uninitialized.
|
||||
// Equals should throw because one operand is Bottom.
|
||||
var success = Result.Success();
|
||||
Result bottom = default;
|
||||
|
||||
// Should throw ResultNotInitializedException, NOT return true
|
||||
Should.Throw<ResultNotInitializedException>(() => success.Equals(bottom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_Default_ShouldNotEqual_Success()
|
||||
{
|
||||
Result bottom = default;
|
||||
var success = Result.Success();
|
||||
|
||||
Should.Throw<ResultNotInitializedException>(() => bottom.Equals(success));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_Success_EqualsOperator_WithDefault()
|
||||
{
|
||||
var success = Result.Success();
|
||||
Result bottom = default;
|
||||
|
||||
Should.Throw<ResultNotInitializedException>(() => success == bottom);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_Error_Equals_WithBottom_ShouldThrow()
|
||||
{
|
||||
var error = Result.Failure("test");
|
||||
Result bottom = default;
|
||||
|
||||
Should.Throw<ResultNotInitializedException>(() => error.Equals(bottom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_Bottom_Equals_WithSuccess_ShouldThrow()
|
||||
{
|
||||
Result bottom = default;
|
||||
var success = Result.Success();
|
||||
|
||||
Should.Throw<ResultNotInitializedException>(() => bottom.Equals(success));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BUG-2: Result<T>.Equals asymmetry
|
||||
|
||||
[Fact]
|
||||
public void ResultOfT_Success_ShouldNotEqual_Default()
|
||||
{
|
||||
var success = Result.Success(42);
|
||||
Result<int> bottom = default;
|
||||
|
||||
Should.Throw<ResultNotInitializedException>(() => success.Equals(bottom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResultOfT_Error_ShouldNotEqual_Default()
|
||||
{
|
||||
var error = Result.Failure<int>("test");
|
||||
Result<int> bottom = default;
|
||||
|
||||
Should.Throw<ResultNotInitializedException>(() => error.Equals(bottom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResultOfT_Default_ShouldNotEqual_Success()
|
||||
{
|
||||
Result<int> bottom = default;
|
||||
var success = Result.Success(42);
|
||||
|
||||
Should.Throw<ResultNotInitializedException>(() => bottom.Equals(success));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BUG-3: Task.WhenAny Merge — faulted task
|
||||
|
||||
[Fact]
|
||||
public async Task Merge_Tasks_WhenOneFaults_ShouldReturnError_NotThrow()
|
||||
{
|
||||
var tasks = new Task<Result>[]
|
||||
{
|
||||
Task.FromResult(Result.Success()),
|
||||
Task.Run(() =>
|
||||
{
|
||||
throw new InvalidOperationException("boom");
|
||||
#pragma warning disable CS0162 // Unreachable code
|
||||
return Result.Success();
|
||||
#pragma warning restore CS0162
|
||||
}),
|
||||
};
|
||||
|
||||
// Should not throw AggregateException escaping the Result error channel
|
||||
var result = await tasks.Merge();
|
||||
|
||||
result.IsFailure.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Merge_Tasks_WhenOneIsCanceled_ShouldReturnError_NotThrow()
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
var canceledTask = Task.FromCanceled<Result>(cts.Token);
|
||||
|
||||
var tasks = new Task<Result>[]
|
||||
{
|
||||
Task.FromResult(Result.Success()),
|
||||
canceledTask,
|
||||
};
|
||||
|
||||
// Should not throw — canceled tasks have null Exception
|
||||
var result = await tasks.Merge();
|
||||
|
||||
result.IsFailure.ShouldBeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BUG-4: Task.WhenAny Merge<T> — canceled task + O(n²)
|
||||
|
||||
[Fact]
|
||||
public async Task MergeOfT_Tasks_WhenOneIsCanceled_ShouldReturnError_NotThrow()
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
var canceledTask = Task.FromCanceled<Result<int>>(cts.Token);
|
||||
|
||||
var tasks = new Task<Result<int>>[]
|
||||
{
|
||||
Task.FromResult(Result.Success(1)),
|
||||
canceledTask,
|
||||
};
|
||||
|
||||
// Should not crash on canceled task (task.Exception is null → null ref in implicit operator)
|
||||
var result = await tasks.Merge();
|
||||
|
||||
result.IsFailure.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergeOfT_Tasks_WhenOneFaults_ShouldReturnError_NotThrow()
|
||||
{
|
||||
var tasks = new Task<Result<int>>[]
|
||||
{
|
||||
Task.FromResult(Result.Success(1)),
|
||||
Task.Run(() =>
|
||||
{
|
||||
throw new InvalidOperationException("boom");
|
||||
#pragma warning disable CS0162 // Unreachable code
|
||||
return Result.Success(2);
|
||||
#pragma warning restore CS0162
|
||||
}),
|
||||
};
|
||||
|
||||
var result = await tasks.Merge();
|
||||
|
||||
result.IsFailure.ShouldBeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BUG-5: Error.New null type
|
||||
|
||||
[Fact]
|
||||
public void Error_New_WithNullType_ShouldThrowArgumentNullException()
|
||||
{
|
||||
// Null type should throw immediately at construction, not crash later in CompareTo/IsSimilarTo
|
||||
Should.Throw<ArgumentNullException>(() => Error.New(null!, "some message"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -37,6 +37,7 @@ public class GeneralUsage
|
||||
var result = Result.Success()
|
||||
.Append(() => Result.Failure<int>(error))
|
||||
.Append("test")
|
||||
.Tap(onSuccess: (i, s) => true.ShouldBeFalse("should not reach this path"))
|
||||
.Map((i, s) =>
|
||||
{
|
||||
true.ShouldBeFalse("should not reach this path");
|
||||
@@ -85,7 +86,7 @@ public class GeneralUsage
|
||||
return "";
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
result.ShouldBe("TEST_1;SOME");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user