Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
###############################################################################
# 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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<AggregateException>()
.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<AggregateException>()
.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<AggregateException>()
.WithInnerExceptions(exceptions => exceptions
.All().Satisfy(e => e.IsTypeOf<ArgumentException>()));
}

[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<AggregateException>()
.WithInnerExceptions(exceptions => exceptions
.All().Satisfy(e => e.IsTypeOf<ArgumentException>()));

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<AggregateException>()
.WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(2));
}
}
}
57 changes: 57 additions & 0 deletions TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using TUnit.Assertions.Core;
using TUnit.Assertions.Sources;

namespace TUnit.Assertions.Conditions;

/// <summary>
/// Asserts on the InnerExceptions collection of an AggregateException using an inline delegate.
/// The delegate receives a CollectionAssertion&lt;Exception&gt; for the InnerExceptions, enabling
/// full collection assertion chaining (Count, All().Satisfy, Contains, etc.).
/// </summary>
public class WithInnerExceptionsAssertion : Assertion<AggregateException>
{
private readonly Func<CollectionAssertion<Exception>, Assertion<IEnumerable<Exception>>?> _innerExceptionsAssertion;

internal WithInnerExceptionsAssertion(
AssertionContext<AggregateException> context,
Func<CollectionAssertion<Exception>, Assertion<IEnumerable<Exception>>?> innerExceptionsAssertion)
: base(context)
{
_innerExceptionsAssertion = innerExceptionsAssertion;
}

protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<AggregateException> metadata)
{
var evaluationException = metadata.Exception;
if (evaluationException != null)
{
return AssertionResult.Failed($"threw {evaluationException.GetType().FullName}");
}

var aggregateException = metadata.Value;

if (aggregateException == null)
{
return AssertionResult.Failed("exception was null");
}

var collectionSource = new CollectionAssertion<Exception>(aggregateException.InnerExceptions, "InnerExceptions");
var resultingAssertion = _innerExceptionsAssertion(collectionSource);

if (resultingAssertion != null)
{
try
{
await resultingAssertion.AssertAsync();
}
catch (Exception ex)
{
return AssertionResult.Failed($"inner exceptions did not satisfy assertion: {ex.Message}");
}
}

return AssertionResult.Passed;
}

protected override string GetExpectation() => "to have inner exceptions satisfying assertion";
}
28 changes: 28 additions & 0 deletions TUnit.Assertions/Extensions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,34 @@ public static ThrowsExactlyAssertion<TException> ThrowsExactly<TException>(this
return new ThrowsExactlyAssertion<TException>(mappedContext);
}

/// <summary>
/// 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&lt;AggregateException&gt;().WithInnerExceptions(e => e.Count().IsEqualTo(3));
/// </summary>
public static WithInnerExceptionsAssertion WithInnerExceptions(
this ThrowsAssertion<AggregateException> source,
Func<CollectionAssertion<Exception>, Assertion<IEnumerable<Exception>>?> innerExceptionsAssertion,
[CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null)
{
source.InternalContext.ExpressionBuilder.Append($".WithInnerExceptions({expression})");
return new WithInnerExceptionsAssertion(source.InternalContext, innerExceptionsAssertion);
}

/// <summary>
/// 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&lt;AggregateException&gt;().WithInnerExceptions(e => e.Count().IsEqualTo(3));
/// </summary>
public static WithInnerExceptionsAssertion WithInnerExceptions(
this ThrowsExactlyAssertion<AggregateException> source,
Func<CollectionAssertion<Exception>, Assertion<IEnumerable<Exception>>?> innerExceptionsAssertion,
[CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null)
{
source.InternalContext.ExpressionBuilder.Append($".WithInnerExceptions({expression})");
return new WithInnerExceptionsAssertion(source.InternalContext, innerExceptionsAssertion);
}

/// <summary>
/// Asserts that an exception's Message property exactly equals the expected string.
/// Works with both direct exception assertions and chained exception assertions (via .And).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
{
Expand Down Expand Up @@ -2725,6 +2730,8 @@ namespace .Extensions
public static .<TException, TInnerException> WithInnerException<TException, TInnerException>(this .<TException> 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 .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, [.("expectedMessage")] string? expression = null)
where TException : { }
public static .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
{
Expand Down Expand Up @@ -2697,6 +2702,8 @@ namespace .Extensions
public static .<TException, TInnerException> WithInnerException<TException, TInnerException>(this .<TException> 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 .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, [.("expectedMessage")] string? expression = null)
where TException : { }
public static .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
{
Expand Down Expand Up @@ -2725,6 +2730,8 @@ namespace .Extensions
public static .<TException, TInnerException> WithInnerException<TException, TInnerException>(this .<TException> 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 .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, [.("expectedMessage")] string? expression = null)
where TException : { }
public static .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
{
Expand Down Expand Up @@ -2437,6 +2442,8 @@ namespace .Extensions
public static .<TException, TInnerException> WithInnerException<TException, TInnerException>(this .<TException> 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 .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, [.("expectedMessage")] string? expression = null)
where TException : { }
public static .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null)
Expand Down
Loading