diff --git a/TUnit.Assertions.Tests/Old/DoubleEqualsToAssertionTests.cs b/TUnit.Assertions.Tests/Old/DoubleEqualsToAssertionTests.cs index 0b82c5a6bd..3d60de3c99 100644 --- a/TUnit.Assertions.Tests/Old/DoubleEqualsToAssertionTests.cs +++ b/TUnit.Assertions.Tests/Old/DoubleEqualsToAssertionTests.cs @@ -44,7 +44,7 @@ public async Task Double_EqualsTo__With_Tolerance_Success() { var double1 = 1.1d; var double2 = 1.2d; - + await TUnitAssert.That(double1).IsEqualTo(double2).Within(0.1); } @@ -53,8 +53,34 @@ public async Task Double_EqualsTo__With_Tolerance_Failure() { var double1 = 1.1d; var double2 = 1.3d; - + await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(double1).IsEqualTo(double2).Within(0.1)); } + + [Test] + public async Task Double_NaN_EqualsTo_NaN_With_Tolerance_Success() + { + const double tolerance = 0.001; + + await TUnitAssert.That(double.NaN).IsEqualTo(double.NaN).Within(tolerance); + } + + [Test] + public async Task Double_NaN_EqualsTo_Number_With_Tolerance_Failure() + { + const double tolerance = 0.001; + + await TUnitAssert.ThrowsAsync(async () => + await TUnitAssert.That(double.NaN).IsEqualTo(1.0).Within(tolerance)); + } + + [Test] + public async Task Double_Number_EqualsTo_NaN_With_Tolerance_Failure() + { + const double tolerance = 0.001; + + await TUnitAssert.ThrowsAsync(async () => + await TUnitAssert.That(1.0).IsEqualTo(double.NaN).Within(tolerance)); + } #endif } diff --git a/TUnit.Assertions.Tests/Old/FloatEqualsToAssertionTests.cs b/TUnit.Assertions.Tests/Old/FloatEqualsToAssertionTests.cs new file mode 100644 index 0000000000..415afd1d73 --- /dev/null +++ b/TUnit.Assertions.Tests/Old/FloatEqualsToAssertionTests.cs @@ -0,0 +1,86 @@ +namespace TUnit.Assertions.Tests.Old; + +public class FloatEqualsToAssertionTests +{ + [Test] + public async Task Float_EqualsTo_Success() + { + var float1 = 1.1f; + var float2 = 1.1f; + + await TUnitAssert.That(float1).IsEqualTo(float2); + } + + [Test] + public async Task Float_EqualsTo_Failure() + { + var float1 = 1.1f; + var float2 = 1.2f; + + await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(float1).IsEqualTo(float2)); + } + + [Test] + public async Task Float_NaN_EqualsTo_NaN_Success() + { + await TUnitAssert.That(float.NaN).IsEqualTo(float.NaN); + } + + [Test] + public async Task Float_PositiveInfinity_EqualsTo_PositiveInfinity_Success() + { + await TUnitAssert.That(float.PositiveInfinity).IsEqualTo(float.PositiveInfinity); + } + + [Test] + public async Task Float_NegativeInfinity_EqualsTo_NegativeInfinity_Success() + { + await TUnitAssert.That(float.NegativeInfinity).IsEqualTo(float.NegativeInfinity); + } + +#if NET + [Test] + public async Task Float_EqualsTo__With_Tolerance_Success() + { + var float1 = 1.1f; + var float2 = 1.2f; + + await TUnitAssert.That(float1).IsEqualTo(float2).Within(0.2f); + } + + [Test] + public async Task Float_EqualsTo__With_Tolerance_Failure() + { + var float1 = 1.1f; + var float2 = 1.3f; + + await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(float1).IsEqualTo(float2).Within(0.1f)); + } + + [Test] + public async Task Float_NaN_EqualsTo_NaN_With_Tolerance_Success() + { + const float tolerance = 0.001f; + + await TUnitAssert.That(float.NaN).IsEqualTo(float.NaN).Within(tolerance); + } + + [Test] + public async Task Float_NaN_EqualsTo_Number_With_Tolerance_Failure() + { + const float tolerance = 0.001f; + + await TUnitAssert.ThrowsAsync(async () => + await TUnitAssert.That(float.NaN).IsEqualTo(1.0f).Within(tolerance)); + } + + [Test] + public async Task Float_Number_EqualsTo_NaN_With_Tolerance_Failure() + { + const float tolerance = 0.001f; + + await TUnitAssert.ThrowsAsync(async () => + await TUnitAssert.That(1.0f).IsEqualTo(float.NaN).Within(tolerance)); + } +#endif +} diff --git a/TUnit.Assertions/Conditions/EqualsAssertion.cs b/TUnit.Assertions/Conditions/EqualsAssertion.cs index 97d4b8a962..19ac4469e2 100644 --- a/TUnit.Assertions/Conditions/EqualsAssertion.cs +++ b/TUnit.Assertions/Conditions/EqualsAssertion.cs @@ -31,6 +31,7 @@ public class EqualsAssertion : Assertion #endif [typeof(int)] = new ToleranceComparer(CompareInt), [typeof(long)] = new ToleranceComparer(CompareLong), + [typeof(float)] = new ToleranceComparer(CompareFloat), [typeof(double)] = new ToleranceComparer(CompareDouble), [typeof(decimal)] = new ToleranceComparer(CompareDecimal) }; @@ -156,6 +157,38 @@ private static bool CompareLong(long actual, long expected, object tolerance, ou return false; } + private static bool CompareFloat(float actual, float expected, object tolerance, out string? errorMessage) + { + if (tolerance is not float floatTolerance) + { + errorMessage = null; + return false; + } + + // Handle NaN comparisons: NaN is only equal to NaN + if (float.IsNaN(actual) && float.IsNaN(expected)) + { + errorMessage = null; + return true; + } + + if (float.IsNaN(actual) || float.IsNaN(expected)) + { + errorMessage = $"found {actual}"; + return false; + } + + var difference = actual > expected ? actual - expected : expected - actual; + if (difference <= floatTolerance) + { + errorMessage = null; + return true; + } + + errorMessage = $"found {actual}, difference {difference} exceeds tolerance {floatTolerance}"; + return false; + } + private static bool CompareDouble(double actual, double expected, object tolerance, out string? errorMessage) { if (tolerance is not double doubleTolerance) @@ -164,6 +197,19 @@ private static bool CompareDouble(double actual, double expected, object toleran return false; } + // Handle NaN comparisons: NaN is only equal to NaN + if (double.IsNaN(actual) && double.IsNaN(expected)) + { + errorMessage = null; + return true; + } + + if (double.IsNaN(actual) || double.IsNaN(expected)) + { + errorMessage = $"found {actual}"; + return false; + } + var difference = actual > expected ? actual - expected : expected - actual; if (difference <= doubleTolerance) { diff --git a/TUnit.Assertions/Conditions/SpecializedEqualityAssertions.cs b/TUnit.Assertions/Conditions/SpecializedEqualityAssertions.cs index bcc9cc5fd4..469ac353f5 100644 --- a/TUnit.Assertions/Conditions/SpecializedEqualityAssertions.cs +++ b/TUnit.Assertions/Conditions/SpecializedEqualityAssertions.cs @@ -158,6 +158,17 @@ protected override Task CheckAsync(EvaluationMetadata m if (_tolerance.HasValue) { + // Handle NaN comparisons: NaN is only equal to NaN + if (double.IsNaN(value) && double.IsNaN(_expected)) + { + return Task.FromResult(AssertionResult.Passed); + } + + if (double.IsNaN(value) || double.IsNaN(_expected)) + { + return Task.FromResult(AssertionResult.Failed($"found {value}")); + } + var diff = Math.Abs(value - _expected); if (diff <= _tolerance.Value) { @@ -181,6 +192,75 @@ protected override string GetExpectation() => : $"to be {_expected}"; } +/// +/// Asserts that a float value is equal to another, with optional tolerance. +/// +public class FloatEqualsAssertion : Assertion +{ + private readonly float _expected; + private float? _tolerance; + + public FloatEqualsAssertion( + AssertionContext context, + float expected) + : base(context) + { + _expected = expected; + } + + public FloatEqualsAssertion Within(float tolerance) + { + _tolerance = tolerance; + Context.ExpressionBuilder.Append($".Within({tolerance})"); + return this; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}")); + } + + if (_tolerance.HasValue) + { + // Handle NaN comparisons: NaN is only equal to NaN + if (float.IsNaN(value) && float.IsNaN(_expected)) + { + return Task.FromResult(AssertionResult.Passed); + } + + if (float.IsNaN(value) || float.IsNaN(_expected)) + { + return Task.FromResult(AssertionResult.Failed($"found {value}")); + } + + var diff = Math.Abs(value - _expected); + if (diff <= _tolerance.Value) + { + return Task.FromResult(AssertionResult.Passed); + } + + return Task.FromResult(AssertionResult.Failed($"found {value}, which differs by {diff}")); + } + + if (float.Equals(value, _expected)) + { + return Task.FromResult(AssertionResult.Passed); + } + + return Task.FromResult(AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() => + _tolerance.HasValue + ? $"to be within {_tolerance} of {_expected}" + : $"to be {_expected}"; +} + /// /// Asserts that a long value is equal to another, with optional tolerance. /// diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs index 7de4c1af4b..69b03d78f6 100644 --- a/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -144,6 +144,21 @@ public static DoubleEqualsAssertion IsEqualTo( return new DoubleEqualsAssertion(source.Context, expected); } + /// + /// Asserts that the float is equal to the expected value. + /// Returns FloatEqualsAssertion which has .Within() method! + /// Priority 2: Highest priority for specialized type. + /// + [OverloadResolutionPriority(2)] + public static FloatEqualsAssertion IsEqualTo( + this IAssertionSource source, + float expected, + [CallerArgumentExpression(nameof(expected))] string? expression = null) + { + source.Context.ExpressionBuilder.Append($".IsEqualTo({expression})"); + return new FloatEqualsAssertion(source.Context, expected); + } + /// /// Asserts that the long is equal to the expected value. /// Returns LongEqualsAssertion which has .Within() method! diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 45eea5e5f8..ae1b7a9513 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -797,6 +797,13 @@ namespace .Conditions protected override .<.> CheckAsync(.<.FileInfo> metadata) { } protected override string GetExpectation() { } } + public class FloatEqualsAssertion : . + { + public FloatEqualsAssertion(. context, float expected) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + public . Within(float tolerance) { } + } [.("IsGreaterThan")] public class GreaterThanAssertion : . where TValue : @@ -1702,6 +1709,8 @@ namespace .Extensions [.(2)] public static . IsEqualTo(this . source, double expected, [.("expected")] string? expression = null) { } [.(2)] + public static . IsEqualTo(this . source, float expected, [.("expected")] string? expression = null) { } + [.(2)] public static . IsEqualTo(this . source, int expected, [.("expected")] string? expression = null) { } [.(2)] public static . IsEqualTo(this . source, long expected, [.("expected")] string? expression = null) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 20f88b34df..07f563c8c4 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -797,6 +797,13 @@ namespace .Conditions protected override .<.> CheckAsync(.<.FileInfo> metadata) { } protected override string GetExpectation() { } } + public class FloatEqualsAssertion : . + { + public FloatEqualsAssertion(. context, float expected) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + public . Within(float tolerance) { } + } [.("IsGreaterThan")] public class GreaterThanAssertion : . where TValue : @@ -1695,6 +1702,7 @@ namespace .Extensions public static . IsEqualTo(this .<> source, expected, [.("expected")] string? expression = null) { } public static .<> IsEqualTo(this .<> source, expected, [.("expected")] string? expression = null) { } public static . IsEqualTo(this . source, double expected, [.("expected")] string? expression = null) { } + public static . IsEqualTo(this . source, float expected, [.("expected")] string? expression = null) { } public static . IsEqualTo(this . source, int expected, [.("expected")] string? expression = null) { } public static . IsEqualTo(this . source, long expected, [.("expected")] string? expression = null) { } public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expression = null) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 7f639e8416..a29d79c036 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -797,6 +797,13 @@ namespace .Conditions protected override .<.> CheckAsync(.<.FileInfo> metadata) { } protected override string GetExpectation() { } } + public class FloatEqualsAssertion : . + { + public FloatEqualsAssertion(. context, float expected) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + public . Within(float tolerance) { } + } [.("IsGreaterThan")] public class GreaterThanAssertion : . where TValue : @@ -1702,6 +1709,8 @@ namespace .Extensions [.(2)] public static . IsEqualTo(this . source, double expected, [.("expected")] string? expression = null) { } [.(2)] + public static . IsEqualTo(this . source, float expected, [.("expected")] string? expression = null) { } + [.(2)] public static . IsEqualTo(this . source, int expected, [.("expected")] string? expression = null) { } [.(2)] public static . IsEqualTo(this . source, long expected, [.("expected")] string? expression = null) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index 4103591c61..e7ebbdd5cd 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -760,6 +760,13 @@ namespace .Conditions protected override .<.> CheckAsync(.<.FileInfo> metadata) { } protected override string GetExpectation() { } } + public class FloatEqualsAssertion : . + { + public FloatEqualsAssertion(. context, float expected) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + public . Within(float tolerance) { } + } [.("IsGreaterThan")] public class GreaterThanAssertion : . where TValue : @@ -1598,6 +1605,7 @@ namespace .Extensions public static . IsEqualTo(this .<> source, expected, [.("expected")] string? expression = null) { } public static .<> IsEqualTo(this .<> source, expected, [.("expected")] string? expression = null) { } public static . IsEqualTo(this . source, double expected, [.("expected")] string? expression = null) { } + public static . IsEqualTo(this . source, float expected, [.("expected")] string? expression = null) { } public static . IsEqualTo(this . source, int expected, [.("expected")] string? expression = null) { } public static . IsEqualTo(this . source, long expected, [.("expected")] string? expression = null) { } public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expression = null) { }