186 lines
5.0 KiB
C#
186 lines
5.0 KiB
C#
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
|
|
}
|