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
12 changes: 12 additions & 0 deletions TUnit.Assertions.Tests/Old/DictionaryAssertionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ await TUnitAssert.That(ImmutableDictionary<string, int>.Empty.Add("Hello", 1))
);
}

// Picks up collection methods via inheritance
[Test]
public async Task HasSingleItem()
{
var dictionary = new Dictionary<string, byte[]>
{
["Blah"] = [1]
};

await TUnitAssert.That(dictionary).HasSingleItem();
}

public class ReadDictionary : IReadOnlyDictionary<string, string>
{
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
Expand Down
127 changes: 72 additions & 55 deletions TUnit.Assertions/Conditions/CollectionAssertions.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ namespace TUnit.Assertions.Conditions;
/// Base class for collection assertions that support custom equality comparers.
/// Inherits from CollectionAssertionBase to ensure And/Or continuations preserve collection type awareness.
/// </summary>
/// <typeparam name="TCollection">The concrete collection type</typeparam>
/// <typeparam name="TItem">The type of items being compared</typeparam>
public abstract class CollectionComparerBasedAssertion<TItem> : CollectionAssertionBase<TItem>
public abstract class CollectionComparerBasedAssertion<TCollection, TItem> : CollectionAssertionBase<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
private IEqualityComparer<TItem>? _comparer;

protected CollectionComparerBasedAssertion(AssertionContext<IEnumerable<TItem>> context)
protected CollectionComparerBasedAssertion(AssertionContext<TCollection> context)
: base(context)
{
}
Expand Down
7 changes: 4 additions & 3 deletions TUnit.Assertions/Conditions/CollectionCountValueAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ namespace TUnit.Assertions.Conditions;
/// Assertion that evaluates the count of a collection and provides numeric assertions on that count.
/// Implements IAssertionSource&lt;int&gt; to enable all numeric assertion methods.
/// </summary>
public class CollectionCountValueAssertion<TItem> : Sources.ValueAssertion<int>
public class CollectionCountValueAssertion<TCollection, TItem> : Sources.ValueAssertion<int>
where TCollection : IEnumerable<TItem>
{
public CollectionCountValueAssertion(
AssertionContext<IEnumerable<TItem>> collectionContext,
AssertionContext<TCollection> collectionContext,
Func<TItem, bool>? predicate)
: base(CreateIntContext(collectionContext, predicate))
{
}

private static AssertionContext<int> CreateIntContext(
AssertionContext<IEnumerable<TItem>> collectionContext,
AssertionContext<TCollection> collectionContext,
Func<TItem, bool>? predicate)
{
return collectionContext.Map<int>(collection =>
Expand Down
11 changes: 6 additions & 5 deletions TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ namespace TUnit.Assertions.Conditions;
/// </summary>
[AssertionExtension("IsEquivalentTo")]
[RequiresDynamicCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")]
public class IsEquivalentToAssertion<TItem> : CollectionComparerBasedAssertion<TItem>
public class IsEquivalentToAssertion<TCollection, TItem> : CollectionComparerBasedAssertion<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
private readonly IEnumerable<TItem> _expected;
private readonly CollectionOrdering _ordering;

public IsEquivalentToAssertion(
AssertionContext<IEnumerable<TItem>> context,
AssertionContext<TCollection> context,
IEnumerable<TItem> expected,
CollectionOrdering ordering = CollectionOrdering.Any)
: base(context)
Expand All @@ -31,7 +32,7 @@ public IsEquivalentToAssertion(
}

public IsEquivalentToAssertion(
AssertionContext<IEnumerable<TItem>> context,
AssertionContext<TCollection> context,
IEnumerable<TItem> expected,
IEqualityComparer<TItem> comparer,
CollectionOrdering ordering = CollectionOrdering.Any)
Expand All @@ -42,14 +43,14 @@ public IsEquivalentToAssertion(
SetComparer(comparer);
}

public IsEquivalentToAssertion<TItem> Using(IEqualityComparer<TItem> comparer)
public IsEquivalentToAssertion<TCollection, TItem> Using(IEqualityComparer<TItem> comparer)
{
SetComparer(comparer);
return this;
}

[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Collection equivalency uses structural comparison which requires reflection")]
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<IEnumerable<TItem>> metadata)
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TCollection> metadata)
{
var value = metadata.Value;
var exception = metadata.Exception;
Expand Down
9 changes: 5 additions & 4 deletions TUnit.Assertions/Conditions/NotEquivalentToAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ namespace TUnit.Assertions.Conditions;
/// </summary>
[AssertionExtension("IsNotEquivalentTo")]
[RequiresDynamicCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")]
public class NotEquivalentToAssertion<TItem> : CollectionComparerBasedAssertion<TItem>
public class NotEquivalentToAssertion<TCollection, TItem> : CollectionComparerBasedAssertion<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
private readonly IEnumerable<TItem> _notExpected;
private readonly CollectionOrdering _ordering;

public NotEquivalentToAssertion(
AssertionContext<IEnumerable<TItem>> context,
AssertionContext<TCollection> context,
IEnumerable<TItem> notExpected,
CollectionOrdering ordering = CollectionOrdering.Any)
: base(context)
Expand All @@ -29,14 +30,14 @@ public NotEquivalentToAssertion(
_ordering = ordering;
}

public NotEquivalentToAssertion<TItem> Using(IEqualityComparer<TItem> comparer)
public NotEquivalentToAssertion<TCollection, TItem> Using(IEqualityComparer<TItem> comparer)
{
SetComparer(comparer);
return this;
}

[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Collection equivalency uses structural comparison which requires reflection")]
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<IEnumerable<TItem>> metadata)
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TCollection> metadata)
{
var value = metadata.Value;
var exception = metadata.Exception;
Expand Down
17 changes: 9 additions & 8 deletions TUnit.Assertions/Conditions/Wrappers/CountWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,27 @@ namespace TUnit.Assertions.Conditions.Wrappers;
/// Wrapper for collection count assertions that provides .EqualTo() method.
/// Example: await Assert.That(list).Count().EqualTo(5);
/// </summary>
public class CountWrapper<TItem> : IAssertionSource<IEnumerable<TItem>>
public class CountWrapper<TCollection, TItem> : IAssertionSource<TCollection>
where TCollection : IEnumerable<TItem>
{
private readonly AssertionContext<IEnumerable<TItem>> _context;
private readonly AssertionContext<TCollection> _context;

public CountWrapper(AssertionContext<IEnumerable<TItem>> context)
public CountWrapper(AssertionContext<TCollection> context)
{
_context = context;
}

AssertionContext<IEnumerable<TItem>> IAssertionSource<IEnumerable<TItem>>.Context => _context;
AssertionContext<TCollection> IAssertionSource<TCollection>.Context => _context;

/// <summary>
/// Asserts that the collection count is equal to the expected count.
/// </summary>
public CollectionCountAssertion<TItem> EqualTo(
public CollectionCountAssertion<TCollection, TItem> EqualTo(
int expectedCount,
[CallerArgumentExpression(nameof(expectedCount))] string? expression = null)
{
_context.ExpressionBuilder.Append($".EqualTo({expression})");
return new CollectionCountAssertion<TItem>(_context, expectedCount);
return new CollectionCountAssertion<TCollection, TItem>(_context, expectedCount);
}

/// <summary>
Expand Down Expand Up @@ -191,10 +192,10 @@ public BetweenAssertion<int> Between(
/// <summary>
/// Asserts that the collection count is zero (empty collection).
/// </summary>
public CollectionCountAssertion<TItem> Zero()
public CollectionCountAssertion<TCollection, TItem> Zero()
{
_context.ExpressionBuilder.Append(".Zero()");
return new CollectionCountAssertion<TItem>(_context, 0);
return new CollectionCountAssertion<TCollection, TItem>(_context, 0);
}

/// <summary>
Expand Down
14 changes: 8 additions & 6 deletions TUnit.Assertions/Core/CollectionContinuations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,26 @@
namespace TUnit.Assertions.Core;

/// <summary>
/// And continuation for collection assertions that preserves item type.
/// And continuation for collection assertions that preserves collection type and item type.
/// Inherits from CollectionAssertionBase to automatically expose all collection methods.
/// </summary>
public class CollectionAndContinuation<TItem> : CollectionAssertionBase<TItem>
public class CollectionAndContinuation<TCollection, TItem> : CollectionAssertionBase<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
internal CollectionAndContinuation(AssertionContext<IEnumerable<TItem>> context, Assertion<IEnumerable<TItem>> previousAssertion)
internal CollectionAndContinuation(AssertionContext<TCollection> context, Assertion<TCollection> previousAssertion)
: base(context, previousAssertion, ".And", CombinerType.And)
{
}
}

/// <summary>
/// Or continuation for collection assertions that preserves item type.
/// Or continuation for collection assertions that preserves collection type and item type.
/// Inherits from CollectionAssertionBase to automatically expose all collection methods.
/// </summary>
public class CollectionOrContinuation<TItem> : CollectionAssertionBase<TItem>
public class CollectionOrContinuation<TCollection, TItem> : CollectionAssertionBase<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
internal CollectionOrContinuation(AssertionContext<IEnumerable<TItem>> context, Assertion<IEnumerable<TItem>> previousAssertion)
internal CollectionOrContinuation(AssertionContext<TCollection> context, Assertion<TCollection> previousAssertion)
: base(context, previousAssertion, ".Or", CombinerType.Or)
{
}
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Assertions/Sources/CollectionAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace TUnit.Assertions.Sources;
/// Inherits from CollectionAssertionBase to get all collection-specific instance methods (Contains, IsInOrder, etc.)
/// that persist through And/Or continuations.
/// </summary>
public class CollectionAssertion<TItem> : CollectionAssertionBase<TItem>
public class CollectionAssertion<TItem> : CollectionAssertionBase<IEnumerable<TItem>, TItem>
{
public CollectionAssertion(IEnumerable<TItem> value, string? expression)
: base(new AssertionContext<IEnumerable<TItem>>(value, CreateExpressionBuilder(expression)))
Expand Down
Loading
Loading