From bd7665feb7826f9434dc9880e7ee61aea5eaa286 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:52:55 +0100 Subject: [PATCH 1/6] +semver:minor - Add WithInnerExceptions() for fluent AggregateException assertion chaining (#5345) --- .../Throws.WithInnerExceptionsTests.cs | 92 +++++++++++++++++++ .../Conditions/ThrowsAssertion.cs | 30 ++++++ .../WithInnerExceptionsAssertion.cs | 57 ++++++++++++ ...Has_No_API_Changes.DotNet10_0.verified.txt | 8 ++ 4 files changed, 187 insertions(+) create mode 100644 TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionsTests.cs create mode 100644 TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionsTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionsTests.cs new file mode 100644 index 0000000000..d4e7fb75e1 --- /dev/null +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionsTests.cs @@ -0,0 +1,92 @@ +namespace TUnit.Assertions.Tests.Assertions.Delegates; + +public partial class Throws +{ + public class WithInnerExceptionsTests + { + [Test] + public async Task WithInnerExceptions_Count_Succeeds() + { + var aggregate = new AggregateException( + new InvalidOperationException("one"), + new ArgumentException("two"), + new FormatException("three")); + Action action = () => throw aggregate; + + await Assert.That(action) + .Throws() + .WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(3)); + } + + [Test] + public async Task WithInnerExceptions_Count_Fails() + { + var aggregate = new AggregateException( + new InvalidOperationException("one"), + new ArgumentException("two")); + Action action = () => throw aggregate; + + var sut = async () => await Assert.That(action) + .Throws() + .WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(5)); + + await Assert.That(sut).ThrowsException(); + } + + [Test] + public async Task WithInnerExceptions_AllSatisfy_Succeeds() + { + var aggregate = new AggregateException( + new ArgumentException("one", "param1"), + new ArgumentException("two", "param2"), + new ArgumentException("three", "param3")); + Action action = () => throw aggregate; + + await Assert.That(action) + .Throws() + .WithInnerExceptions(exceptions => exceptions + .All().Satisfy(e => e.IsTypeOf())); + } + + [Test] + public async Task WithInnerExceptions_AllSatisfy_Fails_When_Mixed_Types() + { + var aggregate = new AggregateException( + new ArgumentException("one"), + new FormatException("two")); + Action action = () => throw aggregate; + + var sut = async () => await Assert.That(action) + .Throws() + .WithInnerExceptions(exceptions => exceptions + .All().Satisfy(e => e.IsTypeOf())); + + await Assert.That(sut).ThrowsException(); + } + + [Test] + public async Task WithInnerExceptions_ThrowsExactly_Count_Succeeds() + { + var aggregate = new AggregateException( + new InvalidOperationException("one"), + new ArgumentException("two")); + Action action = () => throw aggregate; + + await Assert.That(action) + .ThrowsExactly() + .WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(2)); + } + + [Test] + public async Task WithInnerExceptions_Fails_When_Not_AggregateException() + { + Action action = () => throw new InvalidOperationException("not aggregate"); + + var sut = async () => await Assert.That(action) + .Throws() + .WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(1)); + + await Assert.That(sut).ThrowsException(); + } + } +} diff --git a/TUnit.Assertions/Conditions/ThrowsAssertion.cs b/TUnit.Assertions/Conditions/ThrowsAssertion.cs index 3b4de430b7..e92cbd94b6 100644 --- a/TUnit.Assertions/Conditions/ThrowsAssertion.cs +++ b/TUnit.Assertions/Conditions/ThrowsAssertion.cs @@ -1,4 +1,6 @@ +using System.Runtime.CompilerServices; using TUnit.Assertions.Core; +using TUnit.Assertions.Sources; namespace TUnit.Assertions.Conditions; @@ -110,6 +112,20 @@ public ThrowsAssertion WithInnerException() return new ThrowsAssertion(new AssertionContext(innerExceptionContext, Context.ExpressionBuilder)); } + /// + /// Asserts on the InnerExceptions collection of an AggregateException using an inline assertion delegate. + /// The delegate receives a collection assertion source for InnerExceptions, enabling full collection + /// assertion chaining (Count, All().Satisfy, Contains, etc.). + /// Example: await Assert.That(action).Throws<AggregateException>().WithInnerExceptions(e => e.Count().IsEqualTo(3)); + /// + public WithInnerExceptionsAssertion WithInnerExceptions( + Func, Assertion>?> innerExceptionsAssertion, + [CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null) + { + Context.ExpressionBuilder.Append($".WithInnerExceptions({expression})"); + return new WithInnerExceptionsAssertion(Context, innerExceptionsAssertion); + } + /// /// Instance method for backward compatibility - delegates to extension method. /// Asserts that the exception message contains the specified substring. @@ -384,6 +400,20 @@ public ExceptionInnerExceptionOfTypeAssertion WithI return new ExceptionInnerExceptionOfTypeAssertion(Context); } + /// + /// Asserts on the InnerExceptions collection of an AggregateException using an inline assertion delegate. + /// The delegate receives a collection assertion source for InnerExceptions, enabling full collection + /// assertion chaining (Count, All().Satisfy, Contains, etc.). + /// Example: await Assert.That(action).ThrowsExactly<AggregateException>().WithInnerExceptions(e => e.Count().IsEqualTo(3)); + /// + public WithInnerExceptionsAssertion WithInnerExceptions( + Func, Assertion>?> innerExceptionsAssertion, + [CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null) + { + Context.ExpressionBuilder.Append($".WithInnerExceptions({expression})"); + return new WithInnerExceptionsAssertion(Context, innerExceptionsAssertion); + } + /// /// Asserts that the exception's stack trace contains the specified substring. /// Example: await Assert.That(() => ThrowingMethod()).ThrowsExactly<Exception>().WithStackTraceContaining("MyClass.MyMethod"); diff --git a/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs b/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs new file mode 100644 index 0000000000..8f9487f88b --- /dev/null +++ b/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs @@ -0,0 +1,57 @@ +using TUnit.Assertions.Core; +using TUnit.Assertions.Sources; + +namespace TUnit.Assertions.Conditions; + +/// +/// Asserts on the InnerExceptions collection of an AggregateException using an inline delegate. +/// The delegate receives a CollectionAssertion<Exception> for the InnerExceptions, enabling +/// full collection assertion chaining (Count, All().Satisfy, Contains, etc.). +/// +public class WithInnerExceptionsAssertion : Assertion + where TException : Exception +{ + private readonly Func, Assertion>?> _innerExceptionsAssertion; + + internal WithInnerExceptionsAssertion( + AssertionContext context, + Func, Assertion>?> innerExceptionsAssertion) + : base(context) + { + _innerExceptionsAssertion = innerExceptionsAssertion; + } + + protected override async Task CheckAsync(EvaluationMetadata metadata) + { + var exception = metadata.Value; + + if (exception is not AggregateException aggregateException) + { + return AssertionResult.Failed( + exception == null + ? "exception was null" + : $"exception was {exception.GetType().Name}, not AggregateException"); + } + + var innerExceptions = aggregateException.InnerExceptions; + var collectionSource = new CollectionAssertion(innerExceptions, "InnerExceptions"); + var resultingAssertion = _innerExceptionsAssertion(collectionSource); + + if (resultingAssertion != null) + { + try + { + await resultingAssertion.AssertAsync(); + return AssertionResult.Passed; + } + catch + { + return AssertionResult.Failed("inner exceptions assertion failed"); + } + } + + return AssertionResult.Passed; + } + + protected override string GetExpectation() => "to have inner exceptions satisfying assertion"; +} 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 b897d0c5d6..1c02257604 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 @@ -2109,6 +2109,7 @@ namespace .Conditions public .<> WithInnerException() { } public . WithInnerException() where TInnerException : { } + public . WithInnerExceptions(<.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } public . WithMessage(string expectedMessage) { } public . WithMessage(string expectedMessage, comparison) { } public . WithMessageContaining(string expectedSubstring) { } @@ -2129,6 +2130,7 @@ namespace .Conditions protected override bool CheckExceptionType( actualException, out string? errorMessage) { } public . WithInnerException() where TInnerException : { } + public . WithInnerExceptions(<.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } public . WithMessage(string expectedMessage) { } public . WithMessage(string expectedMessage, comparison) { } public . WithMessageContaining(string expectedSubstring) { } @@ -2272,6 +2274,12 @@ namespace .Conditions [.<>("TrackResurrection", CustomName="DoesNotTrackResurrection", ExpectationMessage="track resurrection", NegateLogic=true)] [.<>("TrackResurrection", ExpectationMessage="track resurrection")] public static class WeakReferenceAssertionExtensions { } + public class WithInnerExceptionsAssertion : . + where TException : + { + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } } namespace . { From 5e7ddb37fd42f0e945682afdc5d494477bcfa629 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:00:44 +0100 Subject: [PATCH 2/6] Address PR review: add metadata.Exception guard and fix error message --- .../Conditions/WithInnerExceptionsAssertion.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs b/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs index 8f9487f88b..55f40e59ef 100644 --- a/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs +++ b/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs @@ -23,6 +23,12 @@ internal WithInnerExceptionsAssertion( protected override async Task CheckAsync(EvaluationMetadata metadata) { + var evaluationException = metadata.Exception; + if (evaluationException != null) + { + return AssertionResult.Failed($"threw {evaluationException.GetType().FullName}"); + } + var exception = metadata.Value; if (exception is not AggregateException aggregateException) @@ -42,11 +48,10 @@ protected override async Task CheckAsync(EvaluationMetadata Date: Sat, 4 Apr 2026 19:09:42 +0100 Subject: [PATCH 3/6] Constrain WithInnerExceptions to AggregateException at compile time Move WithInnerExceptions from instance methods on ThrowsAssertion and ThrowsExactlyAssertion to extension methods constrained to AggregateException. This prevents misuse at compile time rather than runtime. Also simplify WithInnerExceptionsAssertion to a non-generic class since TException is always AggregateException. --- .../Throws.WithInnerExceptionsTests.cs | 12 -------- .../Conditions/ThrowsAssertion.cs | 30 ------------------- .../WithInnerExceptionsAssertion.cs | 19 +++++------- .../Extensions/AssertionExtensions.cs | 28 +++++++++++++++++ ...Has_No_API_Changes.DotNet10_0.verified.txt | 9 +++--- 5 files changed, 39 insertions(+), 59 deletions(-) diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionsTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionsTests.cs index d4e7fb75e1..4b06aea0e7 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionsTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionsTests.cs @@ -76,17 +76,5 @@ await Assert.That(action) .ThrowsExactly() .WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(2)); } - - [Test] - public async Task WithInnerExceptions_Fails_When_Not_AggregateException() - { - Action action = () => throw new InvalidOperationException("not aggregate"); - - var sut = async () => await Assert.That(action) - .Throws() - .WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(1)); - - await Assert.That(sut).ThrowsException(); - } } } diff --git a/TUnit.Assertions/Conditions/ThrowsAssertion.cs b/TUnit.Assertions/Conditions/ThrowsAssertion.cs index e92cbd94b6..3b4de430b7 100644 --- a/TUnit.Assertions/Conditions/ThrowsAssertion.cs +++ b/TUnit.Assertions/Conditions/ThrowsAssertion.cs @@ -1,6 +1,4 @@ -using System.Runtime.CompilerServices; using TUnit.Assertions.Core; -using TUnit.Assertions.Sources; namespace TUnit.Assertions.Conditions; @@ -112,20 +110,6 @@ public ThrowsAssertion WithInnerException() return new ThrowsAssertion(new AssertionContext(innerExceptionContext, Context.ExpressionBuilder)); } - /// - /// Asserts on the InnerExceptions collection of an AggregateException using an inline assertion delegate. - /// The delegate receives a collection assertion source for InnerExceptions, enabling full collection - /// assertion chaining (Count, All().Satisfy, Contains, etc.). - /// Example: await Assert.That(action).Throws<AggregateException>().WithInnerExceptions(e => e.Count().IsEqualTo(3)); - /// - public WithInnerExceptionsAssertion WithInnerExceptions( - Func, Assertion>?> innerExceptionsAssertion, - [CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null) - { - Context.ExpressionBuilder.Append($".WithInnerExceptions({expression})"); - return new WithInnerExceptionsAssertion(Context, innerExceptionsAssertion); - } - /// /// Instance method for backward compatibility - delegates to extension method. /// Asserts that the exception message contains the specified substring. @@ -400,20 +384,6 @@ public ExceptionInnerExceptionOfTypeAssertion WithI return new ExceptionInnerExceptionOfTypeAssertion(Context); } - /// - /// Asserts on the InnerExceptions collection of an AggregateException using an inline assertion delegate. - /// The delegate receives a collection assertion source for InnerExceptions, enabling full collection - /// assertion chaining (Count, All().Satisfy, Contains, etc.). - /// Example: await Assert.That(action).ThrowsExactly<AggregateException>().WithInnerExceptions(e => e.Count().IsEqualTo(3)); - /// - public WithInnerExceptionsAssertion WithInnerExceptions( - Func, Assertion>?> innerExceptionsAssertion, - [CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null) - { - Context.ExpressionBuilder.Append($".WithInnerExceptions({expression})"); - return new WithInnerExceptionsAssertion(Context, innerExceptionsAssertion); - } - /// /// Asserts that the exception's stack trace contains the specified substring. /// Example: await Assert.That(() => ThrowingMethod()).ThrowsExactly<Exception>().WithStackTraceContaining("MyClass.MyMethod"); diff --git a/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs b/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs index 55f40e59ef..88428d61b9 100644 --- a/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs +++ b/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs @@ -8,20 +8,19 @@ namespace TUnit.Assertions.Conditions; /// The delegate receives a CollectionAssertion<Exception> for the InnerExceptions, enabling /// full collection assertion chaining (Count, All().Satisfy, Contains, etc.). /// -public class WithInnerExceptionsAssertion : Assertion - where TException : Exception +public class WithInnerExceptionsAssertion : Assertion { private readonly Func, Assertion>?> _innerExceptionsAssertion; internal WithInnerExceptionsAssertion( - AssertionContext context, + AssertionContext context, Func, Assertion>?> innerExceptionsAssertion) : base(context) { _innerExceptionsAssertion = innerExceptionsAssertion; } - protected override async Task CheckAsync(EvaluationMetadata metadata) + protected override async Task CheckAsync(EvaluationMetadata metadata) { var evaluationException = metadata.Exception; if (evaluationException != null) @@ -29,18 +28,14 @@ protected override async Task CheckAsync(EvaluationMetadata(innerExceptions, "InnerExceptions"); + var collectionSource = new CollectionAssertion(aggregateException.InnerExceptions, "InnerExceptions"); var resultingAssertion = _innerExceptionsAssertion(collectionSource); if (resultingAssertion != null) diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs index 97b713d664..efc3667aba 100644 --- a/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -1163,6 +1163,34 @@ public static ThrowsExactlyAssertion ThrowsExactly(this return new ThrowsExactlyAssertion(mappedContext); } + /// + /// Asserts on the InnerExceptions collection of an AggregateException using an inline assertion delegate. + /// Only available when the thrown exception type is AggregateException. + /// Example: await Assert.That(action).Throws<AggregateException>().WithInnerExceptions(e => e.Count().IsEqualTo(3)); + /// + public static WithInnerExceptionsAssertion WithInnerExceptions( + this ThrowsAssertion source, + Func, Assertion>?> innerExceptionsAssertion, + [CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null) + { + source.InternalContext.ExpressionBuilder.Append($".WithInnerExceptions({expression})"); + return new WithInnerExceptionsAssertion(source.InternalContext, innerExceptionsAssertion); + } + + /// + /// Asserts on the InnerExceptions collection of an AggregateException using an inline assertion delegate. + /// Only available when the thrown exception type is AggregateException. + /// Example: await Assert.That(action).ThrowsExactly<AggregateException>().WithInnerExceptions(e => e.Count().IsEqualTo(3)); + /// + public static WithInnerExceptionsAssertion WithInnerExceptions( + this ThrowsExactlyAssertion source, + Func, Assertion>?> innerExceptionsAssertion, + [CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null) + { + source.InternalContext.ExpressionBuilder.Append($".WithInnerExceptions({expression})"); + return new WithInnerExceptionsAssertion(source.InternalContext, innerExceptionsAssertion); + } + /// /// Asserts that an exception's Message property exactly equals the expected string. /// Works with both direct exception assertions and chained exception assertions (via .And). 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 1c02257604..d275bc4517 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 @@ -2109,7 +2109,6 @@ namespace .Conditions public .<> WithInnerException() { } public . WithInnerException() where TInnerException : { } - public . WithInnerExceptions(<.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } public . WithMessage(string expectedMessage) { } public . WithMessage(string expectedMessage, comparison) { } public . WithMessageContaining(string expectedSubstring) { } @@ -2130,7 +2129,6 @@ namespace .Conditions protected override bool CheckExceptionType( actualException, out string? errorMessage) { } public . WithInnerException() where TInnerException : { } - public . WithInnerExceptions(<.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } public . WithMessage(string expectedMessage) { } public . WithMessage(string expectedMessage, comparison) { } public . WithMessageContaining(string expectedSubstring) { } @@ -2274,10 +2272,9 @@ namespace .Conditions [.<>("TrackResurrection", CustomName="DoesNotTrackResurrection", ExpectationMessage="track resurrection", NegateLogic=true)] [.<>("TrackResurrection", ExpectationMessage="track resurrection")] public static class WeakReferenceAssertionExtensions { } - public class WithInnerExceptionsAssertion : . - where TException : + public class WithInnerExceptionsAssertion : .<> { - protected override .<.> CheckAsync(. metadata) { } + protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } } @@ -2733,6 +2730,8 @@ namespace .Extensions public static . WithInnerException(this . source) where TException : where TInnerException : { } + public static . WithInnerExceptions(this .<> source, <.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } + public static . WithInnerExceptions(this .<> source, <.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } public static . WithMessage(this . source, string expectedMessage, [.("expectedMessage")] string? expression = null) where TException : { } public static . WithMessage(this . source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null) From 99d1044c231f9f5a957625e036207493956493aa Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:12:22 +0100 Subject: [PATCH 4/6] Update .gitattributes with EOF best practices --- .gitattributes | 56 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index 92aeca4250..91e319e9a8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,56 @@ ############################################################################### * text=auto -# Verify -*.verified.txt text eol=lf working-tree-encoding=UTF-8 -*.received.txt text eol=lf working-tree-encoding=UTF-8 \ No newline at end of file +############################################################################### +# Source code - normalize to LF in repo, native on checkout +############################################################################### +*.cs text diff=csharp +*.csx text diff=csharp +*.csproj text +*.sln text +*.slnx text +*.props text +*.targets text +*.json text +*.xml text +*.yml text +*.yaml text +*.md text +*.txt text +*.config text +*.editorconfig text +*.razor text +*.cshtml text + +############################################################################### +# Shell scripts - always LF +############################################################################### +*.sh text eol=lf +*.bash text eol=lf + +############################################################################### +# Windows scripts - always CRLF +############################################################################### +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf + +############################################################################### +# Verify snapshots - LF for consistent cross-platform diffs +############################################################################### +*.verified.txt text eol=lf +*.received.txt text eol=lf + +############################################################################### +# Binary files - no normalization +############################################################################### +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.dll binary +*.exe binary +*.nupkg binary +*.snk binary +*.pfx binary From b1d9070829ee9fa20aa38af13aef55132cbddbc9 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:50:59 +0100 Subject: [PATCH 5/6] Regenerate Assertions public API snapshots for WithInnerExceptions changes --- ...tions_Library_Has_No_API_Changes.DotNet8_0.verified.txt | 7 +++++++ ...tions_Library_Has_No_API_Changes.DotNet9_0.verified.txt | 7 +++++++ ...sertions_Library_Has_No_API_Changes.Net4_7.verified.txt | 7 +++++++ 3 files changed, 21 insertions(+) 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 68131a895b..8e96e7b507 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 @@ -2255,6 +2255,11 @@ namespace .Conditions [.<>("TrackResurrection", CustomName="DoesNotTrackResurrection", ExpectationMessage="track resurrection", NegateLogic=true)] [.<>("TrackResurrection", ExpectationMessage="track resurrection")] public static class WeakReferenceAssertionExtensions { } + public class WithInnerExceptionsAssertion : .<> + { + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } } namespace . { @@ -2697,6 +2702,8 @@ namespace .Extensions public static . WithInnerException(this . source) where TException : where TInnerException : { } + public static . WithInnerExceptions(this .<> source, <.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } + public static . WithInnerExceptions(this .<> source, <.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } public static . WithMessage(this . source, string expectedMessage, [.("expectedMessage")] string? expression = null) where TException : { } public static . WithMessage(this . source, string expectedMessage, comparison, [.("expectedMessage")] 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 e896d0349d..130da6da01 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 @@ -2272,6 +2272,11 @@ namespace .Conditions [.<>("TrackResurrection", CustomName="DoesNotTrackResurrection", ExpectationMessage="track resurrection", NegateLogic=true)] [.<>("TrackResurrection", ExpectationMessage="track resurrection")] public static class WeakReferenceAssertionExtensions { } + public class WithInnerExceptionsAssertion : .<> + { + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } } namespace . { @@ -2725,6 +2730,8 @@ namespace .Extensions public static . WithInnerException(this . source) where TException : where TInnerException : { } + public static . WithInnerExceptions(this .<> source, <.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } + public static . WithInnerExceptions(this .<> source, <.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } public static . WithMessage(this . source, string expectedMessage, [.("expectedMessage")] string? expression = null) where TException : { } public static . WithMessage(this . source, string expectedMessage, comparison, [.("expectedMessage")] 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 a88feff098..d6535daa29 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 @@ -2033,6 +2033,11 @@ namespace .Conditions [.<>("TrackResurrection", CustomName="DoesNotTrackResurrection", ExpectationMessage="track resurrection", NegateLogic=true)] [.<>("TrackResurrection", ExpectationMessage="track resurrection")] public static class WeakReferenceAssertionExtensions { } + public class WithInnerExceptionsAssertion : .<> + { + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } } namespace . { @@ -2437,6 +2442,8 @@ namespace .Extensions public static . WithInnerException(this . source) where TException : where TInnerException : { } + public static . WithInnerExceptions(this .<> source, <.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } + public static . WithInnerExceptions(this .<> source, <.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { } public static . WithMessage(this . source, string expectedMessage, [.("expectedMessage")] string? expression = null) where TException : { } public static . WithMessage(this . source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null) From 45f5f1c627dbafe435ec56721b01d01f9d0cac8c Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:00:38 +0100 Subject: [PATCH 6/6] Propagate inner assertion failure message in WithInnerExceptions catch block Change bare `catch` to `catch (Exception ex)` and include ex.Message in the failure string, consistent with MappedSatisfiesAssertion and other sibling assertion patterns in the codebase. --- TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs b/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs index 88428d61b9..302451d121 100644 --- a/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs +++ b/TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs @@ -44,9 +44,9 @@ protected override async Task CheckAsync(EvaluationMetadata