Conversation
51eef1d to
185505c
Compare
There was a problem hiding this comment.
Code Review
This PR adds relative tolerance support for numeric equality assertions, which addresses a genuine gap in the assertion library. The feature itself is well-motivated and the test coverage is solid. However, I have several architectural and correctness concerns.
Major Issues
1. API Surface Proliferation — Three Overlapping Ways to Do the Same Thing
The PR introduces significant redundancy:
IsEqualTo(expected).Within(tolerance)— existing, absolute tolerance- NEW:
IsEqualTo(expected).WithinRelativeTolerance(5)— relative tolerance on equality chain - NEW:
IsCloseTo(expected, tolerance)— standalone absolute tolerance assertion - NEW:
IsWithinPercentOf(expected, percent)— standalone relative tolerance assertion
IsCloseTo and IsEqualTo().Within() are conceptually identical, as are IsWithinPercentOf and IsEqualTo().WithinRelativeTolerance(). The original issue (#4870) asked for relative tolerance — the WithinRelativeTolerance extension on the existing chain is the right solution. The standalone IsCloseTo and IsWithinPercentOf appear unnecessary and significantly bloat the public API surface (10 new public classes + extension methods).
Recommendation: Remove the standalone IsCloseTo and IsWithinPercentOf assertions entirely. They duplicate functionality already available via the IsEqualTo().Within() pattern that users already know.
2. Massive Code Duplication
NumericIsCloseToAssertion.cs and NumericIsWithinPercentOfAssertion.cs each contain 5 nearly-identical concrete classes with repeated NaN/infinity handling, arithmetic, and error message logic. The existing ToleranceBasedEqualsAssertion<TValue, TTolerance> pattern was designed to avoid exactly this — yet it's not used here. This is a significant maintenance liability.
If standalone assertions are kept, they should at minimum share a generic base class or use a helper method for the repeated special-value handling.
3. Bug: Division by Zero in Error Message
In NumericIsWithinPercentOfAssertion.cs, the failure message for FloatIsWithinPercentOfAssertion does not guard against _expected == 0f:
// FloatIsWithinPercentOfAssertion — no zero-guard:
Task.FromResult(AssertionResult.Failed(
$"found {value}, which differs by {diff} ({(diff / Math.Abs(_expected)) * 100:F2}% of expected)"));When _expected == 0f and the assertion fails, this produces NaN% or Infinity% in the message. Compare with DoubleIsWithinPercentOfAssertion which correctly guards:
var actualPercent = _expected != 0 ? (diff / Math.Abs(_expected)) * 100 : double.PositiveInfinity;FloatIsWithinPercentOfAssertion and DecimalIsWithinPercentOfAssertion need the same guard for their failure messages.
4. Missing for / — Silent Runtime Failure
The PR adds IsWithinRelativeTolerance overrides for double, float, and decimal, but not for int or long. The base class default throws NotSupportedException:
protected virtual bool IsWithinRelativeTolerance(TValue actual, TValue expected, double percentTolerance)
=> throw new NotSupportedException($"{GetType().Name} does not support relative tolerance.");This means a user who writes:
await Assert.That(myInt).IsEqualTo(100).WithinRelativeTolerance(5);...will get a NotSupportedException at runtime, with no compile-time indication. This is a poor developer experience, especially since WithinRelativeTolerance is a public method on the base class and appears callable. Either implement it for int/long (straightforward — convert to double, apply the formula) or prevent it from being callable via the type system.
5. Precision Loss in
// LongIsCloseToAssertion
var diff = Math.Abs((double) value - (double) _expected);
return diff <= (double) _toleranceConverting long values to double for subtraction loses precision for large values (e.g., long.MaxValue and long.MaxValue - 1 both round to the same double). The existing LongEqualsAssertion.IsWithinTolerance avoids this by working in long arithmetic:
protected override bool IsWithinTolerance(long actual, long expected, long tolerance)
{
var diff = Math.Abs(actual - expected);
return diff <= tolerance;
}LongIsCloseToAssertion should do the same (though Math.Abs on long.MinValue needs a guard for overflow).
Minor Issues
6. Inconsistent Validation
WithinRelativeTolerance validates the percentage is non-negative, but the new standalone assertions (IsCloseTo, IsWithinPercentOf) accept negative tolerances silently. If these are kept, they should validate too.
7. Zero-Expected Error Formatting Inconsistency
var actualPercent = _expected != 0 ? (diff / Math.Abs(_expected)) * 100m : -1m;
var percentDisplay = actualPercent >= 0 ? $"{actualPercent:F2}%" : "Infinity%";Using -1m as a sentinel value for "infinity" is fragile and unusual. Prefer double.PositiveInfinity (as the double version does) or just special-case the message string.
What's Good
- The
WithinRelativeTolerancefeature itself is well-designed. The formula|actual - expected| <= |expected| * (percent / 100)is correct, and the zero/NaN/infinity edge case handling inDoubleEqualsAssertionandFloatEqualsAssertionis thorough. - The
ToleranceModeenum with aswitchin the base class is a clean way to handle the two tolerance modes. - Test coverage is excellent — boundary inclusion, negative values, zero, NaN, and infinity are all tested.
- Public API snapshots are updated correctly.
- The validation in
WithinRelativeTolerancethrowingArgumentOutOfRangeExceptionfor NaN/Infinity/negative inputs is the right approach.
Summary
The core feature (WithinRelativeTolerance on numeric equality assertions) is the right approach and well-implemented for floating-point types. The main concerns are: (1) the standalone IsCloseTo/IsWithinPercentOf assertions appear redundant given the existing API patterns; (2) there's a real bug in FloatIsWithinPercentOfAssertion's error message when expected is zero; and (3) the silent NotSupportedException for int/long relative tolerance is a usability trap.
There was a problem hiding this comment.
Code Review
This PR adds relative tolerance support for numeric equality assertions, which addresses a genuine gap in the assertion library. The feature itself is well-motivated and test coverage is solid. However, I have several architectural and correctness concerns.
Major Issues
1. API Surface Proliferation — Three Overlapping Ways to Do the Same Thing
The PR introduces significant redundancy:
IsEqualTo(expected).Within(tolerance)— existing, absolute tolerance- NEW:
IsEqualTo(expected).WithinRelativeTolerance(5)— relative tolerance on equality chain - NEW:
IsCloseTo(expected, tolerance)— standalone absolute tolerance assertion - NEW:
IsWithinPercentOf(expected, percent)— standalone relative tolerance assertion
IsCloseTo and IsEqualTo().Within() are conceptually identical, as are IsWithinPercentOf and IsEqualTo().WithinRelativeTolerance(). The original issue (#4870) asked for relative tolerance — the WithinRelativeTolerance extension on the existing chain is the right solution. The standalone IsCloseTo and IsWithinPercentOf appear unnecessary and significantly bloat the public API surface (10 new public classes + extension methods).
Recommendation: Consider removing the standalone IsCloseTo and IsWithinPercentOf assertions. They duplicate functionality already available via the IsEqualTo().Within() pattern.
2. Massive Code Duplication
NumericIsCloseToAssertion.cs and NumericIsWithinPercentOfAssertion.cs each contain 5 nearly-identical concrete classes with repeated NaN/infinity handling and error message logic. The existing ToleranceBasedEqualsAssertion<TValue, TTolerance> pattern was designed to avoid exactly this. If standalone assertions are kept, they should at minimum share a generic base class.
3. Bug: Division by Zero in FloatIsWithinPercentOfAssertion Error Message
In NumericIsWithinPercentOfAssertion.cs, the failure message for FloatIsWithinPercentOfAssertion does not guard against _expected == 0f:
// No zero-guard — produces NaN% or Infinity% in the message:
$"found {value}, which differs by {diff} ({(diff / Math.Abs(_expected)) * 100:F2}% of expected)"Compare with DoubleIsWithinPercentOfAssertion which correctly guards:
var actualPercent = _expected != 0 ? (diff / Math.Abs(_expected)) * 100 : double.PositiveInfinity;FloatIsWithinPercentOfAssertion needs the same guard.
4. Missing IsWithinRelativeTolerance for int/long — Silent Runtime Failure
The PR adds IsWithinRelativeTolerance overrides for double, float, and decimal, but not for int or long. The base class default throws NotSupportedException. This means:
await Assert.That(myInt).IsEqualTo(100).WithinRelativeTolerance(5);...compiles fine but throws NotSupportedException at runtime. Since WithinRelativeTolerance is public on the base class and appears callable, users won't know until their test runs. Implement it for int/long (convert to double, apply the formula) or prevent it from being callable via the type system.
5. Precision Loss in LongIsCloseToAssertion
var diff = Math.Abs((double) value - (double) _expected);
return diff <= (double) _toleranceConverting long to double loses precision for large values — long.MaxValue and long.MaxValue - 1 both round to the same double. The existing LongEqualsAssertion.IsWithinTolerance correctly avoids this by staying in long arithmetic. LongIsCloseToAssertion should do the same.
Minor Issues
6. Inconsistent Validation
WithinRelativeTolerance validates the percentage is non-negative, but the standalone IsCloseTo and IsWithinPercentOf constructors accept negative tolerances silently. If these assertions are kept, they should validate at construction time too.
7. DecimalIsWithinPercentOfAssertion Sentinel Value
var actualPercent = _expected != 0 ? ... : -1m;
var percentDisplay = actualPercent >= 0 ? $"{actualPercent:F2}%" : "Infinity%";Using -1m as a sentinel for "infinity" is fragile. Prefer a direct if/else on _expected == 0m for clarity.
What's Good
- The
WithinRelativeTolerancefeature itself is well-designed. The formula|actual - expected| <= |expected| * (percent / 100)is correct, and zero/NaN/infinity edge case handling inDoubleEqualsAssertionandFloatEqualsAssertionis thorough. - The
ToleranceModeenum with aswitchin the base class is a clean pattern — avoids bool flag proliferation. - Test coverage is excellent — boundary inclusion, negative values, zero, NaN, and infinity are all covered.
- Public API snapshots are updated correctly.
- Validating
WithinRelativeToleranceinputs at construction time (ArgumentOutOfRangeExceptionfor NaN/Infinity/negative) is the right approach.
Summary
The core WithinRelativeTolerance feature on numeric equality assertions is the right approach and well-implemented for floating-point types. The main concerns before merging are: (1) the standalone IsCloseTo/IsWithinPercentOf assertions appear redundant and add significant code duplication; (2) there is a real bug in FloatIsWithinPercentOfAssertion's error message when expected is zero; and (3) the silent NotSupportedException for int/long relative tolerance is a usability trap.
185505c to
596e841
Compare
|
@thomhurst : Please give modular a kick |
596e841 to
b706d48
Compare
There was a problem hiding this comment.
Code Review — Follow-up
The previous review identified several issues. Based on the current diff, none of the major issues appear to have been addressed. Restating them for visibility:
Still Outstanding: Major Issues
1. API Surface Proliferation (unresolved)
The standalone and assertions are still present. These are functionally equivalent to the existing and the new patterns, but add 10 new public classes plus extension methods to the API surface. Once public, these are a permanent maintenance burden.
Recommendation: Remove NumericIsCloseToAssertion.cs and NumericIsWithinPercentOfAssertion.cs entirely. The WithinRelativeTolerance chained API is the right approach — it composes cleanly with the existing pattern users already know.
2. Code Duplication (unresolved)
The two new files each contain 5 near-identical concrete classes duplicating NaN/infinity handling, arithmetic, and error formatting. The existing ToleranceBasedEqualsAssertion<TValue, TTolerance> template was designed to prevent exactly this. This is a significant maintenance liability going forward.
3. Bug: Division by Zero in Error Message (unresolved)
When and the assertion fails (diff > 0), this produces or . The Double and Int variants correctly guard against this; Float does not.
4. Missing for / — Silent Runtime Failure (unresolved)
IntEqualsAssertion and LongEqualsAssertion do not override this. Writing Assert.That(myInt).IsEqualTo(100).WithinRelativeTolerance(5) compiles fine but throws NotSupportedException at runtime. Either implement it for int/long (the math is straightforward: cast to double, apply the formula) or use the type system to prevent it from being called.
5. Precision Loss in (unresolved)
For values near long.MaxValue, the cast to double loses precision. The existing LongEqualsAssertion correctly stays in long arithmetic. LongIsCloseToAssertion should do the same.
Still Outstanding: Minor Issues
6. No Input Validation in Standalone Assertions (unresolved)
IsCloseTo and IsWithinPercentOf accept negative tolerances/percents without any validation, while WithinRelativeTolerance does validate. If kept, add guards.
7. Sentinel Value in (unresolved)
Using -1m as a sentinel for infinity is fragile. A simple if/else on _expected == 0m is clearer.
What Remains Good
The core WithinRelativeTolerance feature on the ToleranceBasedEqualsAssertion base class (the ToleranceMode enum, the switch dispatch, the IsWithinRelativeTolerance override for double/float/decimal) is well-structured and the implementations are correct. The test coverage is thorough. These are solid foundations — the PR just needs the duplicate standalone assertions removed and the int/long gap closed.
There was a problem hiding this comment.
Code Review — Follow-up
The previous review identified several issues. Based on the current diff, none of the major issues appear to have been addressed. Restating them for visibility.
Still Outstanding: Major Issues
1. API Surface Proliferation (unresolved)
The standalone IsCloseTo and IsWithinPercentOf assertions are still present. These are functionally equivalent to the existing IsEqualTo().Within() and the new IsEqualTo().WithinRelativeTolerance() patterns, but add 10 new public classes plus extension methods to the public API. Once merged, these are a permanent maintenance burden.
Recommendation: Remove NumericIsCloseToAssertion.cs and NumericIsWithinPercentOfAssertion.cs entirely. The WithinRelativeTolerance chained API is the right approach — it composes cleanly with the existing pattern users already know.
2. Code Duplication (unresolved)
The two new files each contain 5 near-identical concrete classes duplicating NaN/infinity handling, arithmetic, and error formatting. The existing ToleranceBasedEqualsAssertion<TValue, TTolerance> template was designed to prevent exactly this. This is a significant maintenance liability.
3. Bug: Division by Zero in FloatIsWithinPercentOfAssertion Error Message (unresolved)
// NumericIsWithinPercentOfAssertion.cs — FloatIsWithinPercentOfAssertion
$"found {value}, which differs by {diff} ({(diff / Math.Abs(_expected)) * 100:F2}% of expected)"When _expected == 0f and the assertion fails, this produces NaN% or Infinity% in the message. The Double and Int variants correctly guard against this with a zero check; Float does not.
4. Missing IsWithinRelativeTolerance for int/long — Silent Runtime Failure (unresolved)
The base class default throws:
protected virtual bool IsWithinRelativeTolerance(TValue actual, TValue expected, double percentTolerance)
=> throw new NotSupportedException(...);IntEqualsAssertion and LongEqualsAssertion do not override this. Writing Assert.That(myInt).IsEqualTo(100).WithinRelativeTolerance(5) compiles fine but throws NotSupportedException at runtime — with no compile-time indication. Either implement it for int/long (cast to double, apply the formula) or use the type system to prevent the call.
5. Precision Loss in LongIsCloseToAssertion (unresolved)
var diff = Math.Abs((double) value - (double) _expected); // precision loss for large longsFor values near long.MaxValue, the cast to double loses precision. The existing LongEqualsAssertion correctly stays in long arithmetic. LongIsCloseToAssertion should do the same.
Still Outstanding: Minor Issues
6. No Input Validation in Standalone Assertions (unresolved)
IsCloseTo and IsWithinPercentOf accept negative tolerances without any validation, while WithinRelativeTolerance does validate. If these are kept, add guards at construction time.
7. Sentinel Value in DecimalIsWithinPercentOfAssertion (unresolved)
var actualPercent = _expected != 0 ? ... : -1m;
var percentDisplay = actualPercent >= 0 ? $"{actualPercent:F2}%" : "Infinity%";Using -1m as a sentinel for infinity is fragile. A simple if/else on _expected == 0m is clearer.
What Remains Good
The core WithinRelativeTolerance feature on the ToleranceBasedEqualsAssertion base class — the ToleranceMode enum, the switch dispatch, and the IsWithinRelativeTolerance override for double/float/decimal — is well-structured and the implementations are correct. Test coverage is thorough. These are solid foundations. The PR primarily needs the duplicate standalone assertions removed and the int/long gap closed.
|
I think ignore points 1 & 2 from Claude above. But 3 - 7 seem valid if you're okay to address those? |
I'll progress this in a couple of hours. |
b706d48 to
d214b9a
Compare
|
@thomhurst I've just pushed changes to attempt to address issues 3 through 7. |
There was a problem hiding this comment.
Code Review — Follow-up (Commit 09f600b)
Good progress on the latest commit. Issues 3–7 from the previous reviews have all been addressed. Here's the updated status.
Resolved Since Last Review ✓
- Issue 3 (FloatIsWithinPercentOfAssertion division by zero) — Fixed.
float.PositiveInfinityguard is now present. - Issue 4 (Missing int/long relative tolerance — silent
NotSupportedException) — Fixed. BothIntEqualsAssertionandLongEqualsAssertionnow overrideIsWithinRelativeTolerance. TheLongEqualsAssertionimplementation is particularly nice: it usesdecimalarithmetic to avoid the precision loss that was called out in issue 5. - Issue 5 (Precision loss in
LongIsCloseToAssertion) — Fixed. Now uses(decimal) valueinstead of(double) value. - Issue 6 (Missing input validation in standalone assertions) — Fixed.
IsCloseToandIsWithinPercentOfnow validate at construction time. - Issue 7 (
DecimalIsWithinPercentOfAssertionsentinel value) — Fixed. Now uses a cleanif/elseon_expected == 0m.
Still Outstanding
1. API Surface Proliferation (unresolved — deliberate?)
The standalone IsCloseTo and IsWithinPercentOf assertions are still present. To summarise the concern: these are functionally equivalent to the existing IsEqualTo().Within() and the new IsEqualTo().WithinRelativeTolerance() APIs, which adds 10 new public classes plus extension methods to a surface area that becomes permanent once merged. If the author has considered this and disagrees, that's a valid call — but it's worth an explicit decision.
2. Code Duplication (unresolved)
The two new files still each contain 5 near-identical concrete classes with repeated NaN/infinity handling, arithmetic, and error formatting. The ToleranceBasedEqualsAssertion<TValue, TTolerance> pattern was designed to prevent exactly this pattern. If IsCloseTo and IsWithinPercentOf are kept, consider whether a shared generic base can be extracted.
New Minor Observations
Float percentage precision
In FloatEqualsAssertion.IsWithinRelativeTolerance:
var allowedDifference = Math.Abs(expected) * ((float) percentTolerance / 100f);Casting percentTolerance to float before dividing loses precision in the percentage itself. Consider (float)(percentTolerance / 100.0) to keep the division in double before the final cast, which matches how DoubleEqualsAssertion works.
LongIsWithinPercentOfAssertion uses double, not decimal
In NumericIsWithinPercentOfAssertion.cs, LongIsWithinPercentOfAssertion still computes:
var diff = Math.Abs((double)value - _expected);Meanwhile LongEqualsAssertion.IsWithinRelativeTolerance correctly uses decimal to avoid precision loss. This creates an inconsistency between the two long-percentage code paths in the same PR. If IsWithinPercentOf is kept, it should match the precision approach used in LongEqualsAssertion.
Summary
The core WithinRelativeTolerance feature on the ToleranceBasedEqualsAssertion base class is now solid — all five numeric types are correctly implemented, the int/long runtime trap is closed, and the edge-case bugs are fixed. The outstanding concerns are the design question around the standalone assertions (issues 1 & 2) and two minor precision inconsistencies in float and long paths of the new code.
Add relative and absolute tolerance assertions for numeric types (double, float, int, long, decimal): - IsCloseTo(expected, tolerance): asserts |actual - expected| <= tolerance - IsWithinPercentOf(expected, percent): asserts |actual - expected| <= |expected * percent / 100| Both assertions follow the existing pattern using [AssertionExtension] for source-generated extension methods. Includes proper handling of NaN, infinity, and edge cases for floating-point types. Closes thomhurst#4870
…o double Cast `value` to `double` before subtraction and compare against `(double)_tolerance` so that extreme long values (e.g., MaxValue vs MinValue) no longer silently wrap around.
Seek to address thomhurst#4870
[//]: # (dependabot-start)⚠️ **Dependabot is rebasing this PR**⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.19.57 to 1.21.6. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.21.6 <!-- Release notes generated using configuration in .github/release.yml at v1.21.6 --> ## What's Changed ### Other Changes * perf: replace object locks with Lock type for efficient synchronization by @thomhurst in thomhurst/TUnit#5219 * perf: parallelize test metadata collection for source-generated tests by @thomhurst in thomhurst/TUnit#5221 * perf: use GetOrAdd args overload to eliminate closure allocations in event receivers by @thomhurst in thomhurst/TUnit#5222 * perf: self-contained TestEntry<T> with consolidated switch invokers eliminates per-test JIT by @thomhurst in thomhurst/TUnit#5223 ### Dependencies * chore(deps): update tunit to 1.21.0 by @thomhurst in thomhurst/TUnit#5220 **Full Changelog**: thomhurst/TUnit@v1.21.0...v1.21.6 ## 1.21.0 <!-- Release notes generated using configuration in .github/release.yml at v1.21.0 --> ## What's Changed ### Other Changes * perf: reduce ConcurrentDictionary closure allocations in hot paths by @thomhurst in thomhurst/TUnit#5210 * perf: reduce async state machine overhead in test execution pipeline by @thomhurst in thomhurst/TUnit#5214 * perf: reduce allocations in EventReceiverOrchestrator and TestContextExtensions by @thomhurst in thomhurst/TUnit#5212 * perf: skip timeout machinery when no timeout configured by @thomhurst in thomhurst/TUnit#5211 * perf: reduce allocations and lock contention in ObjectTracker by @thomhurst in thomhurst/TUnit#5213 * Feat/numeric tolerance by @agray in thomhurst/TUnit#5110 * perf: remove unnecessary lock in ObjectTracker.TrackObjects by @thomhurst in thomhurst/TUnit#5217 * perf: eliminate async state machine in TestCoordinator.ExecuteTestAsync by @thomhurst in thomhurst/TUnit#5216 * perf: eliminate LINQ allocation in ObjectTracker.UntrackObjectsAsync by @thomhurst in thomhurst/TUnit#5215 * perf: consolidate module initializers into single .cctor via partial class by @thomhurst in thomhurst/TUnit#5218 ### Dependencies * chore(deps): update tunit to 1.20.0 by @thomhurst in thomhurst/TUnit#5205 * chore(deps): update dependency nunit3testadapter to 6.2.0 by @thomhurst in thomhurst/TUnit#5206 * chore(deps): update dependency cliwrap to 3.10.1 by @thomhurst in thomhurst/TUnit#5207 **Full Changelog**: thomhurst/TUnit@v1.20.0...v1.21.0 ## 1.20.0 <!-- Release notes generated using configuration in .github/release.yml at v1.20.0 --> ## What's Changed ### Other Changes * Fix inverted colors in HTML report ring chart due to locale-dependent decimal formatting by @Copilot in thomhurst/TUnit#5185 * Fix nullable warnings when using Member() on nullable properties by @Copilot in thomhurst/TUnit#5191 * Add CS8629 suppression and member access expression matching to IsNotNullAssertionSuppressor by @Copilot in thomhurst/TUnit#5201 * feat: add ConfigureAppHost hook to AspireFixture by @thomhurst in thomhurst/TUnit#5202 * Fix ConfigureTestConfiguration being invoked twice by @thomhurst in thomhurst/TUnit#5203 * Add IsEquivalentTo assertion for Memory<T> and ReadOnlyMemory<T> by @thomhurst in thomhurst/TUnit#5204 ### Dependencies * chore(deps): update dependency gitversion.tool to v6.6.2 by @thomhurst in thomhurst/TUnit#5181 * chore(deps): update dependency gitversion.msbuild to 6.6.2 by @thomhurst in thomhurst/TUnit#5180 * chore(deps): update tunit to 1.19.74 by @thomhurst in thomhurst/TUnit#5179 * chore(deps): update verify to 31.13.3 by @thomhurst in thomhurst/TUnit#5182 * chore(deps): update verify to 31.13.5 by @thomhurst in thomhurst/TUnit#5183 * chore(deps): update aspire to 13.1.3 by @thomhurst in thomhurst/TUnit#5189 * chore(deps): update dependency stackexchange.redis to 2.12.4 by @thomhurst in thomhurst/TUnit#5193 * chore(deps): update microsoft/setup-msbuild action to v3 by @thomhurst in thomhurst/TUnit#5197 **Full Changelog**: thomhurst/TUnit@v1.19.74...v1.20.0 ## 1.19.74 <!-- Release notes generated using configuration in .github/release.yml at v1.19.74 --> ## What's Changed ### Other Changes * feat: per-hook activity spans with method names by @thomhurst in thomhurst/TUnit#5159 * fix: add tooltip to truncated span names in HTML report by @thomhurst in thomhurst/TUnit#5164 * Use enum names instead of numeric values in test display names by @Copilot in thomhurst/TUnit#5178 * fix: resolve CS8920 when mocking interfaces whose members return static-abstract interfaces by @lucaxchaves in thomhurst/TUnit#5154 ### Dependencies * chore(deps): update tunit to 1.19.57 by @thomhurst in thomhurst/TUnit#5157 * chore(deps): update dependency gitversion.msbuild to 6.6.1 by @thomhurst in thomhurst/TUnit#5160 * chore(deps): update dependency gitversion.tool to v6.6.1 by @thomhurst in thomhurst/TUnit#5161 * chore(deps): update dependency polyfill to 9.20.0 by @thomhurst in thomhurst/TUnit#5163 * chore(deps): update dependency polyfill to 9.20.0 by @thomhurst in thomhurst/TUnit#5162 * chore(deps): update dependency polyfill to 9.21.0 by @thomhurst in thomhurst/TUnit#5166 * chore(deps): update dependency polyfill to 9.21.0 by @thomhurst in thomhurst/TUnit#5167 * chore(deps): update dependency polyfill to 9.22.0 by @thomhurst in thomhurst/TUnit#5168 * chore(deps): update dependency polyfill to 9.22.0 by @thomhurst in thomhurst/TUnit#5169 * chore(deps): update dependency coverlet.collector to 8.0.1 by @thomhurst in thomhurst/TUnit#5177 ## New Contributors * @lucaxchaves made their first contribution in thomhurst/TUnit#5154 **Full Changelog**: thomhurst/TUnit@v1.19.57...v1.19.74 Commits viewable in [compare view](thomhurst/TUnit@v1.19.57...v1.21.6). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.19.57 to 1.21.6. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.21.6 <!-- Release notes generated using configuration in .github/release.yml at v1.21.6 --> ## What's Changed ### Other Changes * perf: replace object locks with Lock type for efficient synchronization by @thomhurst in thomhurst/TUnit#5219 * perf: parallelize test metadata collection for source-generated tests by @thomhurst in thomhurst/TUnit#5221 * perf: use GetOrAdd args overload to eliminate closure allocations in event receivers by @thomhurst in thomhurst/TUnit#5222 * perf: self-contained TestEntry<T> with consolidated switch invokers eliminates per-test JIT by @thomhurst in thomhurst/TUnit#5223 ### Dependencies * chore(deps): update tunit to 1.21.0 by @thomhurst in thomhurst/TUnit#5220 **Full Changelog**: thomhurst/TUnit@v1.21.0...v1.21.6 ## 1.21.0 <!-- Release notes generated using configuration in .github/release.yml at v1.21.0 --> ## What's Changed ### Other Changes * perf: reduce ConcurrentDictionary closure allocations in hot paths by @thomhurst in thomhurst/TUnit#5210 * perf: reduce async state machine overhead in test execution pipeline by @thomhurst in thomhurst/TUnit#5214 * perf: reduce allocations in EventReceiverOrchestrator and TestContextExtensions by @thomhurst in thomhurst/TUnit#5212 * perf: skip timeout machinery when no timeout configured by @thomhurst in thomhurst/TUnit#5211 * perf: reduce allocations and lock contention in ObjectTracker by @thomhurst in thomhurst/TUnit#5213 * Feat/numeric tolerance by @agray in thomhurst/TUnit#5110 * perf: remove unnecessary lock in ObjectTracker.TrackObjects by @thomhurst in thomhurst/TUnit#5217 * perf: eliminate async state machine in TestCoordinator.ExecuteTestAsync by @thomhurst in thomhurst/TUnit#5216 * perf: eliminate LINQ allocation in ObjectTracker.UntrackObjectsAsync by @thomhurst in thomhurst/TUnit#5215 * perf: consolidate module initializers into single .cctor via partial class by @thomhurst in thomhurst/TUnit#5218 ### Dependencies * chore(deps): update tunit to 1.20.0 by @thomhurst in thomhurst/TUnit#5205 * chore(deps): update dependency nunit3testadapter to 6.2.0 by @thomhurst in thomhurst/TUnit#5206 * chore(deps): update dependency cliwrap to 3.10.1 by @thomhurst in thomhurst/TUnit#5207 **Full Changelog**: thomhurst/TUnit@v1.20.0...v1.21.0 ## 1.20.0 <!-- Release notes generated using configuration in .github/release.yml at v1.20.0 --> ## What's Changed ### Other Changes * Fix inverted colors in HTML report ring chart due to locale-dependent decimal formatting by @Copilot in thomhurst/TUnit#5185 * Fix nullable warnings when using Member() on nullable properties by @Copilot in thomhurst/TUnit#5191 * Add CS8629 suppression and member access expression matching to IsNotNullAssertionSuppressor by @Copilot in thomhurst/TUnit#5201 * feat: add ConfigureAppHost hook to AspireFixture by @thomhurst in thomhurst/TUnit#5202 * Fix ConfigureTestConfiguration being invoked twice by @thomhurst in thomhurst/TUnit#5203 * Add IsEquivalentTo assertion for Memory<T> and ReadOnlyMemory<T> by @thomhurst in thomhurst/TUnit#5204 ### Dependencies * chore(deps): update dependency gitversion.tool to v6.6.2 by @thomhurst in thomhurst/TUnit#5181 * chore(deps): update dependency gitversion.msbuild to 6.6.2 by @thomhurst in thomhurst/TUnit#5180 * chore(deps): update tunit to 1.19.74 by @thomhurst in thomhurst/TUnit#5179 * chore(deps): update verify to 31.13.3 by @thomhurst in thomhurst/TUnit#5182 * chore(deps): update verify to 31.13.5 by @thomhurst in thomhurst/TUnit#5183 * chore(deps): update aspire to 13.1.3 by @thomhurst in thomhurst/TUnit#5189 * chore(deps): update dependency stackexchange.redis to 2.12.4 by @thomhurst in thomhurst/TUnit#5193 * chore(deps): update microsoft/setup-msbuild action to v3 by @thomhurst in thomhurst/TUnit#5197 **Full Changelog**: thomhurst/TUnit@v1.19.74...v1.20.0 ## 1.19.74 <!-- Release notes generated using configuration in .github/release.yml at v1.19.74 --> ## What's Changed ### Other Changes * feat: per-hook activity spans with method names by @thomhurst in thomhurst/TUnit#5159 * fix: add tooltip to truncated span names in HTML report by @thomhurst in thomhurst/TUnit#5164 * Use enum names instead of numeric values in test display names by @Copilot in thomhurst/TUnit#5178 * fix: resolve CS8920 when mocking interfaces whose members return static-abstract interfaces by @lucaxchaves in thomhurst/TUnit#5154 ### Dependencies * chore(deps): update tunit to 1.19.57 by @thomhurst in thomhurst/TUnit#5157 * chore(deps): update dependency gitversion.msbuild to 6.6.1 by @thomhurst in thomhurst/TUnit#5160 * chore(deps): update dependency gitversion.tool to v6.6.1 by @thomhurst in thomhurst/TUnit#5161 * chore(deps): update dependency polyfill to 9.20.0 by @thomhurst in thomhurst/TUnit#5163 * chore(deps): update dependency polyfill to 9.20.0 by @thomhurst in thomhurst/TUnit#5162 * chore(deps): update dependency polyfill to 9.21.0 by @thomhurst in thomhurst/TUnit#5166 * chore(deps): update dependency polyfill to 9.21.0 by @thomhurst in thomhurst/TUnit#5167 * chore(deps): update dependency polyfill to 9.22.0 by @thomhurst in thomhurst/TUnit#5168 * chore(deps): update dependency polyfill to 9.22.0 by @thomhurst in thomhurst/TUnit#5169 * chore(deps): update dependency coverlet.collector to 8.0.1 by @thomhurst in thomhurst/TUnit#5177 ## New Contributors * @lucaxchaves made their first contribution in thomhurst/TUnit#5154 **Full Changelog**: thomhurst/TUnit@v1.19.57...v1.19.74 Commits viewable in [compare view](thomhurst/TUnit@v1.19.57...v1.21.6). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Description
Add relative tolerance support for numeric equality assertions
This PR adds support for relative tolerance comparisons to numeric equality assertions.
New API:
This asserts that actual is within 5% of expected.
The comparison uses:
|actual - expected| <= |expected| * (percentTolerance / 100)This enables comparisons where an absolute tolerance is not appropriate (for example when values vary greatly in magnitude).
Scope
Relative tolerance is implemented for numeric equality assertions:
Other ToleranceBasedEqualsAssertion types (e.g. DateTime, DateOnly, TimeOnly) remain unchanged.
Edge cases handled
Tests
Added tests covering:
Related
Closes / addresses: #4870
Fixes #
#4870
Type of Change
Checklist
Required
TUnit-Specific Requirements
TUnit.Core.SourceGenerator)TUnit.Engine)TUnit.Core.SourceGenerator.Testsand/orTUnit.PublicAPItests.received.txtfiles and accepted them as.verified.txt.verified.txtfilesTesting
dotnet test)Additional Notes