diff --git a/TUnit.Assertions.Should.Tests/CollectionTests.cs b/TUnit.Assertions.Should.Tests/CollectionTests.cs index e99e28abef..48104e3a64 100644 --- a/TUnit.Assertions.Should.Tests/CollectionTests.cs +++ b/TUnit.Assertions.Should.Tests/CollectionTests.cs @@ -108,6 +108,45 @@ public async Task Dictionary_ContainKeyWithValue() await dict.Should().ContainKeyWithValue("one", 1); } + [Test] + public async Task Dictionary_size_and_count_methods() + { + IReadOnlyDictionary dict = new Dictionary + { + ["one"] = 1, + ["two"] = 2, + }; + + await dict.Should().NotBeEmpty(); + await dict.Should().HaveAtLeast(1); + await dict.Should().HaveAtMost(5); + await dict.Should().HaveCountBetween(1, 3); + + IReadOnlyDictionary empty = new Dictionary(); + await empty.Should().BeEmpty(); + + IReadOnlyDictionary single = new Dictionary { ["only"] = 1 }; + await single.Should().HaveSingleItem(); + } + + [Test] + public async Task MutableDictionary_size_and_count_methods() + { + IDictionary dict = new Dictionary + { + ["one"] = 1, + ["two"] = 2, + }; + + await dict.Should().NotBeEmpty(); + await dict.Should().HaveAtLeast(1); + await dict.Should().HaveAtMost(5); + await dict.Should().HaveCountBetween(1, 3); + + IDictionary single = new Dictionary { ["only"] = 1 }; + await single.Should().HaveSingleItem(); + } + [Test] public async Task HashSet_BeSupersetOf() { diff --git a/TUnit.Assertions.Should/Core/ShouldDictionarySource.cs b/TUnit.Assertions.Should/Core/ShouldDictionarySource.cs index c058bc04b0..2954b2d9bb 100644 --- a/TUnit.Assertions.Should/Core/ShouldDictionarySource.cs +++ b/TUnit.Assertions.Should/Core/ShouldDictionarySource.cs @@ -117,6 +117,61 @@ public ShouldAssertion> AnyValue( var inner = ApplyBecause(new DictionaryAnyValueAssertion, TKey, TValue>(Context, predicate)); return new ShouldAssertion>(Context, inner); } + + // The count/size methods below are hand-written because the source DictionaryAssertion shadows the + // inherited collection methods (IsEmpty, HasSingleItem, ...) with dictionary-typed `public new` + // overloads whose abstract return type the Should generator can't construct. Without these the + // generated Be/Have counterparts would silently disappear from the dictionary Should surface. + + public ShouldAssertion> BeEmpty() + { + Context.ExpressionBuilder.Append(".BeEmpty()"); + var inner = ApplyBecause(new CollectionIsEmptyAssertion, KeyValuePair>(Context)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> NotBeEmpty() + { + Context.ExpressionBuilder.Append(".NotBeEmpty()"); + var inner = ApplyBecause(new CollectionIsNotEmptyAssertion, KeyValuePair>(Context)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> HaveSingleItem() + { + Context.ExpressionBuilder.Append(".HaveSingleItem()"); + var inner = ApplyBecause(new HasSingleItemAssertion, KeyValuePair>(Context)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> HaveAtLeast( + int minCount, + [CallerArgumentExpression(nameof(minCount))] string? expression = null) + { + Context.ExpressionBuilder.Append(".HaveAtLeast(").Append(expression).Append(')'); + var inner = ApplyBecause(new CollectionHasAtLeastAssertion, KeyValuePair>(Context, minCount)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> HaveAtMost( + int maxCount, + [CallerArgumentExpression(nameof(maxCount))] string? expression = null) + { + Context.ExpressionBuilder.Append(".HaveAtMost(").Append(expression).Append(')'); + var inner = ApplyBecause(new CollectionHasAtMostAssertion, KeyValuePair>(Context, maxCount)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> HaveCountBetween( + int min, + int max, + [CallerArgumentExpression(nameof(min))] string? minExpression = null, + [CallerArgumentExpression(nameof(max))] string? maxExpression = null) + { + Context.ExpressionBuilder.Append($".HaveCountBetween({minExpression}, {maxExpression})"); + var inner = ApplyBecause(new CollectionHasCountBetweenAssertion, KeyValuePair>(Context, min, max)); + return new ShouldAssertion>(Context, inner); + } } [ShouldGeneratePartial(typeof(MutableDictionaryAssertion<,>))] @@ -228,4 +283,57 @@ public ShouldAssertion> AnyValue( var inner = ApplyBecause(new MutableDictionaryAnyValueAssertion, TKey, TValue>(Context, predicate)); return new ShouldAssertion>(Context, inner); } + + // See ShouldDictionarySource: hand-written because MutableDictionaryAssertion shadows the inherited + // collection methods with dictionary-typed `public new` overloads the Should generator can't construct. + + public ShouldAssertion> BeEmpty() + { + Context.ExpressionBuilder.Append(".BeEmpty()"); + var inner = ApplyBecause(new CollectionIsEmptyAssertion, KeyValuePair>(Context)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> NotBeEmpty() + { + Context.ExpressionBuilder.Append(".NotBeEmpty()"); + var inner = ApplyBecause(new CollectionIsNotEmptyAssertion, KeyValuePair>(Context)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> HaveSingleItem() + { + Context.ExpressionBuilder.Append(".HaveSingleItem()"); + var inner = ApplyBecause(new HasSingleItemAssertion, KeyValuePair>(Context)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> HaveAtLeast( + int minCount, + [CallerArgumentExpression(nameof(minCount))] string? expression = null) + { + Context.ExpressionBuilder.Append(".HaveAtLeast(").Append(expression).Append(')'); + var inner = ApplyBecause(new CollectionHasAtLeastAssertion, KeyValuePair>(Context, minCount)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> HaveAtMost( + int maxCount, + [CallerArgumentExpression(nameof(maxCount))] string? expression = null) + { + Context.ExpressionBuilder.Append(".HaveAtMost(").Append(expression).Append(')'); + var inner = ApplyBecause(new CollectionHasAtMostAssertion, KeyValuePair>(Context, maxCount)); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> HaveCountBetween( + int min, + int max, + [CallerArgumentExpression(nameof(min))] string? minExpression = null, + [CallerArgumentExpression(nameof(max))] string? maxExpression = null) + { + Context.ExpressionBuilder.Append($".HaveCountBetween({minExpression}, {maxExpression})"); + var inner = ApplyBecause(new CollectionHasCountBetweenAssertion, KeyValuePair>(Context, min, max)); + return new ShouldAssertion>(Context, inner); + } } diff --git a/TUnit.Assertions.Tests/DictionaryCollectionTests.cs b/TUnit.Assertions.Tests/DictionaryCollectionTests.cs index cc29f16a0b..7efa8333b9 100644 --- a/TUnit.Assertions.Tests/DictionaryCollectionTests.cs +++ b/TUnit.Assertions.Tests/DictionaryCollectionTests.cs @@ -626,4 +626,288 @@ public async Task IDictionary_ContainsKey_With_Custom_Comparer() await Assert.That(dictionary) .ContainsKey("HELLO", StringComparer.OrdinalIgnoreCase); } + + [Test] + public async Task Dictionary_ContainsKey_And_Value_IsEqualTo_Passes() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + await Assert.That(dictionary).ContainsKey("Key").And.Value.IsEqualTo(1234L); + } + + [Test] + public async Task Dictionary_ContainsKey_And_Value_IsEqualTo_Fails_When_Value_Different() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + var exception = await Assert.ThrowsAsync( + async () => await Assert.That(dictionary).ContainsKey("Key").And.Value.IsEqualTo(9999L)); + + await Assert.That(exception.Message).Contains("1234"); + } + + [Test] + public async Task Dictionary_ContainsKey_And_Value_Fails_When_Key_Missing() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + var exception = await Assert.ThrowsAsync( + async () => await Assert.That(dictionary).ContainsKey("Missing").And.Value.IsEqualTo(1234L)); + + // The ContainsKey check runs first (pre-work), so a missing key fails with the + // standard "contain key" message rather than a raw KeyNotFoundException. + await Assert.That(exception.Message).Contains("contain key"); + } + + [Test] + public async Task Dictionary_ContainsKey_And_Value_Member() + { + var dictionary = new Dictionary + { + ["Key"] = new Holder(1234L) + }; + + await Assert.That(dictionary) + .ContainsKey("Key").And.Value.Member(x => x.Inner, p => p.IsEqualTo(1234L)); + } + + [Test] + public async Task Dictionary_ContainsKey_And_Value_Supports_Other_Assertions() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + await Assert.That(dictionary).ContainsKey("Key").And.Value.IsGreaterThan(1000L); + await Assert.That(dictionary).ContainsKey("Key").And.Value.IsNotEqualTo(0L); + } + + [Test] + public async Task Dictionary_ContainsKey_And_Value_Then_Value_Level_And() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + await Assert.That(dictionary) + .ContainsKey("Key").And.Value.IsGreaterThan(1000L).And.IsLessThan(2000L); + } + + [Test] + public async Task Dictionary_And_Value_Still_Allows_Dictionary_Chaining() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L, + ["Other"] = 1L + }; + + // The And continuation still exposes the regular dictionary methods. + await Assert.That(dictionary).ContainsKey("Key").And.ContainsKey("Other"); + } + + [Test] + public async Task Dictionary_LongerChain_Then_Value() + { + var dictionary = new Dictionary + { + ["First"] = 1L, + ["Key"] = 1234L + }; + + // Earlier assertions in the chain run as pre-work before the value is read. + await Assert.That(dictionary) + .ContainsKey("First").And.ContainsKey("Key").And.Value.IsEqualTo(1234L); + } + + [Test] + public async Task Dictionary_ContainsKey_With_Comparer_And_Value() + { + var dictionary = new Dictionary + { + ["Hello"] = 1234L + }; + + await Assert.That(dictionary) + .ContainsKey("HELLO", StringComparer.OrdinalIgnoreCase).And.Value.IsEqualTo(1234L); + } + + [Test] + public async Task IReadOnlyDictionary_ContainsKey_And_Value_IsEqualTo() + { + IReadOnlyDictionary dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + await Assert.That(dictionary).ContainsKey("Key").And.Value.IsEqualTo(1234L); + } + + [Test] + public async Task IDictionary_ContainsKey_And_Value_IsEqualTo() + { + IDictionary dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + await Assert.That(dictionary).ContainsKey("Key").And.Value.IsEqualTo(1234L); + } + + [Test] + public async Task Dictionary_IsNotEmpty_Preserves_Dictionary_Continuation() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + // IsNotEmpty now keeps the dictionary continuation, so ContainsKey/.Value remain available. + await Assert.That(dictionary) + .IsNotEmpty() + .And.ContainsKey("Key").And.Value.IsEqualTo(1234L); + } + + [Test] + public async Task Dictionary_IsEmpty_Preserves_Dictionary_Continuation() + { + var nonEmpty = new Dictionary + { + ["Key"] = 1234L + }; + + await Assert.That(nonEmpty) + .IsEmpty() + .Or.ContainsKey("Key"); + } + + [Test] + public async Task IDictionary_IsNotEmpty_Preserves_Dictionary_Continuation() + { + IDictionary dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + await Assert.That(dictionary) + .IsNotEmpty() + .And.ContainsKey("Key").And.Value.IsEqualTo(1234L); + } + + [Test] + public async Task Dictionary_Count_Preserves_Dictionary_Continuation() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L, + ["Other"] = 1L + }; + + await Assert.That(dictionary) + .Count().IsEqualTo(2).And.ContainsKey("Key").And.Value.IsEqualTo(1234L); + } + + [Test] + public async Task Dictionary_Count_Comparison_Methods_Work() + { + var dictionary = new Dictionary + { + ["a"] = 1L, + ["b"] = 2L, + ["c"] = 3L + }; + + await Assert.That(dictionary).Count().IsGreaterThan(2); + await Assert.That(dictionary).Count().IsLessThanOrEqualTo(3).And.ContainsKey("a"); + await Assert.That(dictionary).Count().IsNotEqualTo(0).And.IsNotEmpty(); + await Assert.That(dictionary).Count().IsPositive(); + } + + [Test] + public async Task Dictionary_Count_Fails_With_Count_Message() + { + var dictionary = new Dictionary + { + ["a"] = 1L + }; + + var exception = await Assert.ThrowsAsync( + async () => await Assert.That(dictionary).Count().IsEqualTo(5)); + + await Assert.That(exception.Message).Contains("count"); + } + + [Test] + public async Task Dictionary_ContainsKey_And_Count() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L, + ["Other"] = 1L + }; + + await Assert.That(dictionary) + .ContainsKey("Key").And.Count().IsEqualTo(2); + } + + [Test] + public async Task Dictionary_HasSingleItem_Preserves_Dictionary_Continuation() + { + var dictionary = new Dictionary + { + ["Key"] = 1234L + }; + + await Assert.That(dictionary) + .HasSingleItem().And.ContainsKey("Key").And.Value.IsEqualTo(1234L); + } + + [Test] + public async Task Dictionary_Size_Methods_Preserve_Dictionary_Continuation() + { + var dictionary = new Dictionary + { + ["a"] = 1L, + ["b"] = 2L, + ["c"] = 3L + }; + + await Assert.That(dictionary).HasAtLeast(2).And.ContainsKey("a"); + await Assert.That(dictionary).HasAtMost(5).And.ContainsKey("b"); + await Assert.That(dictionary).HasCountBetween(1, 5).And.ContainsKey("c"); + } + + [Test] + public async Task IDictionary_Count_And_HasSingleItem_Preserve_Continuation() + { + IDictionary single = new Dictionary + { + ["Key"] = 1234L + }; + + await Assert.That(single) + .HasSingleItem().And.ContainsKey("Key").And.Value.IsEqualTo(1234L); + + IDictionary many = new Dictionary + { + ["a"] = 1L, + ["b"] = 2L + }; + + await Assert.That(many) + .Count().IsEqualTo(2).And.ContainsKey("a"); + } + + private sealed record Holder(long Inner); } diff --git a/TUnit.Assertions/Conditions/DictionaryAssertions.cs b/TUnit.Assertions/Conditions/DictionaryAssertions.cs index 7d8a883fe2..1b850c454b 100644 --- a/TUnit.Assertions/Conditions/DictionaryAssertions.cs +++ b/TUnit.Assertions/Conditions/DictionaryAssertions.cs @@ -39,6 +39,22 @@ public DictionaryContainsKeyAssertion Using(Func(equalityPredicate)); } + /// + /// Returns an And continuation that, in addition to the usual dictionary chaining, exposes + /// to drill + /// into the value stored at the asserted key. + /// Example: await Assert.That(dict).ContainsKey("key").And.Value.IsEqualTo(123); + /// + public new DictionaryContainsKeyAndContinuation And + { + get + { + ThrowIfMixingCombiner>(); + return new DictionaryContainsKeyAndContinuation( + Context, InternalWrappedExecution ?? this, _expectedKey, _comparer); + } + } + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; diff --git a/TUnit.Assertions/Conditions/DictionaryCollectionContinuations.cs b/TUnit.Assertions/Conditions/DictionaryCollectionContinuations.cs new file mode 100644 index 0000000000..d30d20b188 --- /dev/null +++ b/TUnit.Assertions/Conditions/DictionaryCollectionContinuations.cs @@ -0,0 +1,158 @@ +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Conditions; + +/// +/// Dictionary-typed assertion that delegates its check to an inner collection assertion while +/// preserving dictionary-specific chaining (ContainsKey, Value, ...). +/// The inner assertion is built on a detached context so its construction does not consume the +/// And/Or pending link that must stay wired to this (dictionary-typed) assertion. +/// +internal sealed class DictionaryDelegatingAssertion + : Sources.DictionaryAssertionBase + where TDictionary : IReadOnlyDictionary + where TKey : notnull +{ + private readonly Assertion _inner; + + internal DictionaryDelegatingAssertion(AssertionContext context, Assertion inner) + : base(context) + { + _inner = inner; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + => _inner.InternalCheckAsync(metadata); + + protected override string GetExpectation() => _inner.InternalGetExpectation(); +} + +/// +/// Mutable-dictionary twin of . +/// +internal sealed class MutableDictionaryDelegatingAssertion + : Sources.MutableDictionaryAssertionBase + where TDictionary : IDictionary + where TKey : notnull +{ + private readonly Assertion _inner; + + internal MutableDictionaryDelegatingAssertion(AssertionContext context, Assertion inner) + : base(context) + { + _inner = inner; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + => _inner.InternalCheckAsync(metadata); + + protected override string GetExpectation() => _inner.InternalGetExpectation(); +} + +/// +/// Count source for read-only dictionaries that preserves dictionary-specific chaining. +/// Mirrors but returns dictionary-typed +/// assertions so chains like Count().IsEqualTo(2).And.ContainsKey("k") keep working. +/// NOTE: keep the method set in sync with +/// (separate classes are required by the IReadOnlyDictionary vs IDictionary constraint). +/// +public sealed class DictionaryCountSource + where TDictionary : IReadOnlyDictionary + where TKey : notnull +{ + private readonly AssertionContext _context; + + internal DictionaryCountSource(AssertionContext context) => _context = context; + + private Sources.DictionaryAssertionBase Compare( + int expected, CountComparison comparison, string expressionFragment) + { + _context.ExpressionBuilder.Append(expressionFragment); + var inner = new CollectionCountEqualsAssertion>( + _context.CreateDetached(), null, expected, comparison); + return new DictionaryDelegatingAssertion(_context, inner); + } + + public Sources.DictionaryAssertionBase IsEqualTo( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.Equal, $".IsEqualTo({expression})"); + + public Sources.DictionaryAssertionBase IsNotEqualTo( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.NotEqual, $".IsNotEqualTo({expression})"); + + public Sources.DictionaryAssertionBase IsGreaterThan( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.GreaterThan, $".IsGreaterThan({expression})"); + + public Sources.DictionaryAssertionBase IsGreaterThanOrEqualTo( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.GreaterThanOrEqual, $".IsGreaterThanOrEqualTo({expression})"); + + public Sources.DictionaryAssertionBase IsLessThan( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.LessThan, $".IsLessThan({expression})"); + + public Sources.DictionaryAssertionBase IsLessThanOrEqualTo( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.LessThanOrEqual, $".IsLessThanOrEqualTo({expression})"); + + public Sources.DictionaryAssertionBase IsZero() + => Compare(0, CountComparison.Equal, ".IsZero()"); + + public Sources.DictionaryAssertionBase IsPositive() + => Compare(0, CountComparison.GreaterThan, ".IsPositive()"); +} + +/// +/// Mutable-dictionary twin of . +/// NOTE: keep the method set in sync with . +/// +public sealed class MutableDictionaryCountSource + where TDictionary : IDictionary + where TKey : notnull +{ + private readonly AssertionContext _context; + + internal MutableDictionaryCountSource(AssertionContext context) => _context = context; + + private Sources.MutableDictionaryAssertionBase Compare( + int expected, CountComparison comparison, string expressionFragment) + { + _context.ExpressionBuilder.Append(expressionFragment); + var inner = new CollectionCountEqualsAssertion>( + _context.CreateDetached(), null, expected, comparison); + return new MutableDictionaryDelegatingAssertion(_context, inner); + } + + public Sources.MutableDictionaryAssertionBase IsEqualTo( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.Equal, $".IsEqualTo({expression})"); + + public Sources.MutableDictionaryAssertionBase IsNotEqualTo( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.NotEqual, $".IsNotEqualTo({expression})"); + + public Sources.MutableDictionaryAssertionBase IsGreaterThan( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.GreaterThan, $".IsGreaterThan({expression})"); + + public Sources.MutableDictionaryAssertionBase IsGreaterThanOrEqualTo( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.GreaterThanOrEqual, $".IsGreaterThanOrEqualTo({expression})"); + + public Sources.MutableDictionaryAssertionBase IsLessThan( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.LessThan, $".IsLessThan({expression})"); + + public Sources.MutableDictionaryAssertionBase IsLessThanOrEqualTo( + int expected, [CallerArgumentExpression(nameof(expected))] string? expression = null) + => Compare(expected, CountComparison.LessThanOrEqual, $".IsLessThanOrEqualTo({expression})"); + + public Sources.MutableDictionaryAssertionBase IsZero() + => Compare(0, CountComparison.Equal, ".IsZero()"); + + public Sources.MutableDictionaryAssertionBase IsPositive() + => Compare(0, CountComparison.GreaterThan, ".IsPositive()"); +} diff --git a/TUnit.Assertions/Conditions/DictionaryValueSource.cs b/TUnit.Assertions/Conditions/DictionaryValueSource.cs new file mode 100644 index 0000000000..8bee287439 --- /dev/null +++ b/TUnit.Assertions/Conditions/DictionaryValueSource.cs @@ -0,0 +1,153 @@ +using TUnit.Assertions.Core; +using TUnit.Assertions.Sources; + +namespace TUnit.Assertions.Conditions; + +/// +/// Assertion source for the value stored at a dictionary key. +/// Exposes the full value-assertion surface (IsEqualTo, IsNotNull, Member, Satisfies, …) +/// by virtue of being an . +/// Created via ContainsKey(key).And.Value. +/// +/// The dictionary value type. +public sealed class DictionaryValueSource : ValueAssertion +{ + internal DictionaryValueSource(AssertionContext context) + : base(context) + { + } +} + +/// +/// Shared value lookup for the ContainsKey(key).And.Value drill-in, used by both the read-only +/// and mutable continuations (which cannot share a base due to the IReadOnlyDictionary vs IDictionary +/// constraint). Existence/null is already validated by the ContainsKey pre-work, so a miss returns +/// default rather than throwing — letting the pre-work's clean failure surface. +/// +internal static class DictionaryValueLookup +{ + internal static TValue? Extract( + IEnumerable>? dictionary, TKey key, IEqualityComparer? comparer) + where TKey : notnull + { + if (dictionary is null) + { + return default; + } + + if (comparer is not null) + { + foreach (var pair in dictionary) + { + if (comparer.Equals(pair.Key, key)) + { + return pair.Value; + } + } + + return default; + } + + // No custom comparer: use the dictionary's own TryGetValue (O(1), and it honours the + // dictionary's internal comparer — which a default-equality scan would not). + if (dictionary is IReadOnlyDictionary readOnly) + { + return readOnly.TryGetValue(key, out var value) ? value : default; + } + + if (dictionary is IDictionary mutable) + { + return mutable.TryGetValue(key, out var value) ? value : default; + } + + foreach (var pair in dictionary) + { + if (EqualityComparer.Default.Equals(pair.Key, key)) + { + return pair.Value; + } + } + + return default; + } +} + +/// +/// And continuation returned by ContainsKey(key).And on a read-only dictionary. +/// Behaves like the standard +/// but additionally carries the asserted key so the entry's value can be drilled into via +/// . +/// Example: await Assert.That(dict).ContainsKey("key").And.Value.IsEqualTo(123); +/// +public sealed class DictionaryContainsKeyAndContinuation + : DictionaryAndContinuation + where TDictionary : IReadOnlyDictionary + where TKey : notnull +{ + private readonly TKey _expectedKey; + private readonly IEqualityComparer? _comparer; + + internal DictionaryContainsKeyAndContinuation( + AssertionContext context, + Assertion previousAssertion, + TKey expectedKey, + IEqualityComparer? comparer) + : base(context, previousAssertion) + { + _expectedKey = expectedKey; + _comparer = comparer; + } + + /// + /// Drills into the value stored at the key asserted by the preceding ContainsKey, + /// allowing assertions to be made directly against the value. + /// The ContainsKey check runs first (as pre-work transferred by the .And link), + /// so a missing key fails with the standard "to contain key" message before the value is read. + /// + public DictionaryValueSource Value + { + get + { + Context.ExpressionBuilder.Append(".Value"); + var valueContext = Context.Map( + dictionary => DictionaryValueLookup.Extract(dictionary, _expectedKey, _comparer)); + return new DictionaryValueSource(valueContext); + } + } +} + +/// +/// And continuation returned by ContainsKey(key).And on a mutable dictionary (IDictionary). +/// Mutable twin of . +/// +public sealed class MutableDictionaryContainsKeyAndContinuation + : MutableDictionaryAndContinuation + where TDictionary : IDictionary + where TKey : notnull +{ + private readonly TKey _expectedKey; + private readonly IEqualityComparer? _comparer; + + internal MutableDictionaryContainsKeyAndContinuation( + AssertionContext context, + Assertion previousAssertion, + TKey expectedKey, + IEqualityComparer? comparer) + : base(context, previousAssertion) + { + _expectedKey = expectedKey; + _comparer = comparer; + } + + /// + public DictionaryValueSource Value + { + get + { + Context.ExpressionBuilder.Append(".Value"); + var valueContext = Context.Map( + dictionary => DictionaryValueLookup.Extract(dictionary, _expectedKey, _comparer)); + return new DictionaryValueSource(valueContext); + } + } +} diff --git a/TUnit.Assertions/Conditions/MutableDictionaryAssertions.cs b/TUnit.Assertions/Conditions/MutableDictionaryAssertions.cs index 1550aa5d30..95c3270106 100644 --- a/TUnit.Assertions/Conditions/MutableDictionaryAssertions.cs +++ b/TUnit.Assertions/Conditions/MutableDictionaryAssertions.cs @@ -33,6 +33,22 @@ public MutableDictionaryContainsKeyAssertion Using(Fu Context, _expectedKey, new FuncEqualityComparer(equalityPredicate)); } + /// + /// Returns an And continuation that, in addition to the usual dictionary chaining, exposes + /// to + /// drill into the value stored at the asserted key. + /// Example: await Assert.That(dict).ContainsKey("key").And.Value.IsEqualTo(123); + /// + public new MutableDictionaryContainsKeyAndContinuation And + { + get + { + ThrowIfMixingCombiner>(); + return new MutableDictionaryContainsKeyAndContinuation( + Context, InternalWrappedExecution ?? this, _expectedKey, _comparer); + } + } + protected override string GetExpectation() => $"to contain key {_expectedKey}"; protected override Task CheckAsync(EvaluationMetadata metadata) diff --git a/TUnit.Assertions/Core/Assertion.cs b/TUnit.Assertions/Core/Assertion.cs index 68f58ea0c6..4374a8f57b 100644 --- a/TUnit.Assertions/Core/Assertion.cs +++ b/TUnit.Assertions/Core/Assertion.cs @@ -82,6 +82,13 @@ protected virtual Task CheckAsync(EvaluationMetadata me /// internal string InternalGetExpectation() => GetExpectation(); + /// + /// Internal accessor for CheckAsync(), allowing one assertion to delegate its check to another + /// (e.g. dictionary assertions that reuse the collection assertion logic while preserving + /// dictionary-specific chaining). + /// + internal Task InternalCheckAsync(EvaluationMetadata metadata) => CheckAsync(metadata); + /// /// Internal accessor for the because message, used by And/OrAssertion to build combined error messages. /// diff --git a/TUnit.Assertions/Core/AssertionContext.cs b/TUnit.Assertions/Core/AssertionContext.cs index c70a11c514..12a5147006 100644 --- a/TUnit.Assertions/Core/AssertionContext.cs +++ b/TUnit.Assertions/Core/AssertionContext.cs @@ -89,6 +89,13 @@ public AssertionContext Map(Func> asyncMapper) return Map(evalContext => evalContext.Map(asyncMapper)); } + /// + /// Creates a detached context that shares this context's evaluation (so it sees the same value) + /// but has its own expression builder and no pending link. Used to construct an inner assertion + /// whose construction must NOT consume this context's And/Or pending link. + /// + internal AssertionContext CreateDetached() => new(Evaluation, new StringBuilder()); + public AssertionContext MapException() where TException : Exception { return new AssertionContext( diff --git a/TUnit.Assertions/Sources/DictionaryAssertionBase.cs b/TUnit.Assertions/Sources/DictionaryAssertionBase.cs index aea85145de..d7b64d6a1d 100644 --- a/TUnit.Assertions/Sources/DictionaryAssertionBase.cs +++ b/TUnit.Assertions/Sources/DictionaryAssertionBase.cs @@ -58,6 +58,99 @@ private protected DictionaryAssertionBase( return new DictionaryNullAssertion(Context, expectNull: false); } + // The collection methods below (IsEmpty, Count, HasSingleItem, size checks) are hidden with + // `public new` so they return a dictionary-typed assertion instead of the collection-typed one + // from CollectionAssertionBase, keeping dictionary chaining (.And.ContainsKey, etc.) available. + // Each reuses the existing collection assertion's check logic via a delegating wrapper; the inner + // assertion is built on Context.CreateDetached() so its construction doesn't consume our pending link. + + private DictionaryAssertionBase Delegate(Assertion inner) + => new DictionaryDelegatingAssertion(Context, inner); + + /// + /// Asserts that the dictionary is empty while preserving dictionary-specific chaining. + /// + public new DictionaryAssertionBase IsEmpty() + { + Context.ExpressionBuilder.Append(".IsEmpty()"); + return Delegate(new CollectionIsEmptyAssertion>(Context.CreateDetached())); + } + + /// + /// Asserts that the dictionary is not empty while preserving dictionary-specific chaining. + /// + public new DictionaryAssertionBase IsNotEmpty() + { + Context.ExpressionBuilder.Append(".IsNotEmpty()"); + return Delegate(new CollectionIsNotEmptyAssertion>(Context.CreateDetached())); + } + + /// + /// Gets the entry count for numeric assertions while preserving dictionary-specific chaining. + /// Example: await Assert.That(dict).Count().IsEqualTo(2).And.ContainsKey("key"); + /// + public new DictionaryCountSource Count() + { + Context.ExpressionBuilder.Append(".Count()"); + return new DictionaryCountSource(Context); + } + + /// + /// Asserts that the dictionary contains exactly one entry while preserving dictionary-specific chaining. + /// + public new DictionaryAssertionBase HasSingleItem() + { + Context.ExpressionBuilder.Append(".HasSingleItem()"); + return Delegate(new HasSingleItemAssertion>(Context.CreateDetached())); + } + + /// + /// Asserts that the dictionary contains exactly one entry matching the predicate while preserving chaining. + /// + public new DictionaryAssertionBase HasSingleItem( + Func, bool> predicate, + [CallerArgumentExpression(nameof(predicate))] string? expression = null) + { + Context.ExpressionBuilder.Append($".HasSingleItem({expression})"); + return Delegate(new HasSingleItemPredicateAssertion>( + Context.CreateDetached(), predicate, expression ?? "predicate")); + } + + /// + /// Asserts that the dictionary has at least the specified number of entries while preserving chaining. + /// + public new DictionaryAssertionBase HasAtLeast( + int minCount, + [CallerArgumentExpression(nameof(minCount))] string? expression = null) + { + Context.ExpressionBuilder.Append($".HasAtLeast({expression})"); + return Delegate(new CollectionHasAtLeastAssertion>(Context.CreateDetached(), minCount)); + } + + /// + /// Asserts that the dictionary has at most the specified number of entries while preserving chaining. + /// + public new DictionaryAssertionBase HasAtMost( + int maxCount, + [CallerArgumentExpression(nameof(maxCount))] string? expression = null) + { + Context.ExpressionBuilder.Append($".HasAtMost({expression})"); + return Delegate(new CollectionHasAtMostAssertion>(Context.CreateDetached(), maxCount)); + } + + /// + /// Asserts that the dictionary entry count is between min and max (inclusive) while preserving chaining. + /// + public new DictionaryAssertionBase HasCountBetween( + int min, + int max, + [CallerArgumentExpression(nameof(min))] string? minExpression = null, + [CallerArgumentExpression(nameof(max))] string? maxExpression = null) + { + Context.ExpressionBuilder.Append($".HasCountBetween({minExpression}, {maxExpression})"); + return Delegate(new CollectionHasCountBetweenAssertion>(Context.CreateDetached(), min, max)); + } + /// /// Asserts that the dictionary contains the specified key. /// This instance method enables calling ContainsKey with proper type inference. diff --git a/TUnit.Assertions/Sources/MutableDictionaryAssertionBase.cs b/TUnit.Assertions/Sources/MutableDictionaryAssertionBase.cs index c94d8a73ba..8b6bb75922 100644 --- a/TUnit.Assertions/Sources/MutableDictionaryAssertionBase.cs +++ b/TUnit.Assertions/Sources/MutableDictionaryAssertionBase.cs @@ -56,6 +56,97 @@ private protected MutableDictionaryAssertionBase( return new MutableDictionaryNullAssertion(Context, expectNull: false); } + // See DictionaryAssertionBase for the rationale: these `public new` collection methods return a + // mutable-dictionary-typed assertion (preserving .And.ContainsKey, etc.) and reuse the existing + // collection check logic via a delegating wrapper built on a detached context. + + private MutableDictionaryAssertionBase Delegate(Assertion inner) + => new MutableDictionaryDelegatingAssertion(Context, inner); + + /// + /// Asserts that the dictionary is empty while preserving mutable-dictionary-specific chaining. + /// + public new MutableDictionaryAssertionBase IsEmpty() + { + Context.ExpressionBuilder.Append(".IsEmpty()"); + return Delegate(new CollectionIsEmptyAssertion>(Context.CreateDetached())); + } + + /// + /// Asserts that the dictionary is not empty while preserving mutable-dictionary-specific chaining. + /// + public new MutableDictionaryAssertionBase IsNotEmpty() + { + Context.ExpressionBuilder.Append(".IsNotEmpty()"); + return Delegate(new CollectionIsNotEmptyAssertion>(Context.CreateDetached())); + } + + /// + /// Gets the entry count for numeric assertions while preserving mutable-dictionary-specific chaining. + /// Example: await Assert.That(dict).Count().IsEqualTo(2).And.ContainsKey("key"); + /// + public new MutableDictionaryCountSource Count() + { + Context.ExpressionBuilder.Append(".Count()"); + return new MutableDictionaryCountSource(Context); + } + + /// + /// Asserts that the dictionary contains exactly one entry while preserving mutable-dictionary chaining. + /// + public new MutableDictionaryAssertionBase HasSingleItem() + { + Context.ExpressionBuilder.Append(".HasSingleItem()"); + return Delegate(new HasSingleItemAssertion>(Context.CreateDetached())); + } + + /// + /// Asserts that the dictionary contains exactly one entry matching the predicate while preserving chaining. + /// + public new MutableDictionaryAssertionBase HasSingleItem( + Func, bool> predicate, + [CallerArgumentExpression(nameof(predicate))] string? expression = null) + { + Context.ExpressionBuilder.Append($".HasSingleItem({expression})"); + return Delegate(new HasSingleItemPredicateAssertion>( + Context.CreateDetached(), predicate, expression ?? "predicate")); + } + + /// + /// Asserts that the dictionary has at least the specified number of entries while preserving chaining. + /// + public new MutableDictionaryAssertionBase HasAtLeast( + int minCount, + [CallerArgumentExpression(nameof(minCount))] string? expression = null) + { + Context.ExpressionBuilder.Append($".HasAtLeast({expression})"); + return Delegate(new CollectionHasAtLeastAssertion>(Context.CreateDetached(), minCount)); + } + + /// + /// Asserts that the dictionary has at most the specified number of entries while preserving chaining. + /// + public new MutableDictionaryAssertionBase HasAtMost( + int maxCount, + [CallerArgumentExpression(nameof(maxCount))] string? expression = null) + { + Context.ExpressionBuilder.Append($".HasAtMost({expression})"); + return Delegate(new CollectionHasAtMostAssertion>(Context.CreateDetached(), maxCount)); + } + + /// + /// Asserts that the dictionary entry count is between min and max (inclusive) while preserving chaining. + /// + public new MutableDictionaryAssertionBase HasCountBetween( + int min, + int max, + [CallerArgumentExpression(nameof(min))] string? minExpression = null, + [CallerArgumentExpression(nameof(max))] string? maxExpression = null) + { + Context.ExpressionBuilder.Append($".HasCountBetween({minExpression}, {maxExpression})"); + return Delegate(new CollectionHasCountBetweenAssertion>(Context.CreateDetached(), min, max)); + } + /// /// Asserts that the dictionary contains the specified key. /// Example: await Assert.That(dictionary).ContainsKey("key1"); 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 25a10902ac..c3c67b371c 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 @@ -1052,11 +1052,18 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class DictionaryContainsKeyAndContinuation : . + where TDictionary : . + where TKey : notnull + { + public . Value { get; } + } public class DictionaryContainsKeyAssertion : . where TDictionary : . where TKey : notnull { public DictionaryContainsKeyAssertion(. context, TKey expectedKey, .? comparer = null) { } + public new . And { get; } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1078,6 +1085,19 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class DictionaryCountSource + where TDictionary : . + where TKey : notnull + { + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } public class DictionaryDoesNotContainKeyAssertion : . where TDictionary : . where TKey : notnull @@ -1097,6 +1117,7 @@ namespace .Conditions public class DictionaryMemberAssertionAdapter : . where TDictionary : . where TKey : notnull { } + public sealed class DictionaryValueSource : . { } [.("HasFiles")] public class DirectoryHasFilesAssertion : .<.DirectoryInfo> { @@ -1656,11 +1677,18 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class MutableDictionaryContainsKeyAndContinuation : . + where TDictionary : . + where TKey : notnull + { + public . Value { get; } + } public class MutableDictionaryContainsKeyAssertion : . where TDictionary : . where TKey : notnull { public MutableDictionaryContainsKeyAssertion(. context, TKey expectedKey, .? comparer = null) { } + public new . And { get; } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1682,6 +1710,19 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class MutableDictionaryCountSource + where TDictionary : . + where TKey : notnull + { + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } public class MutableDictionaryDoesNotContainKeyAssertion : . where TDictionary : . where TKey : notnull @@ -6296,9 +6337,17 @@ namespace .Sources public . ContainsKey(TKey expectedKey, .? comparer, [.("expectedKey")] string? keyExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsKeyWithValue(TKey expectedKey, TValue expectedValue, [.("expectedKey")] string? keyExpression = null, [.("expectedValue")] string? valueExpression = null) { } public . ContainsValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } + public new . Count() { } public . DoesNotContainKey(TKey expectedKey, [.("expectedKey")] string? expression = null) { } public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } + public new . HasAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public new . HasAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public new . HasCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public new . HasSingleItem() { } + public . HasSingleItem(<., bool> predicate, [.("predicate")] string? expression = null) { } + public new . IsEmpty() { } + public new . IsNotEmpty() { } public new . IsNotNull() { } public new . IsNull() { } } @@ -6415,9 +6464,17 @@ namespace .Sources public . ContainsKey(TKey expectedKey, .? comparer, [.("expectedKey")] string? keyExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsKeyWithValue(TKey expectedKey, TValue expectedValue, [.("expectedKey")] string? keyExpression = null, [.("expectedValue")] string? valueExpression = null) { } public . ContainsValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } + public new . Count() { } public . DoesNotContainKey(TKey expectedKey, [.("expectedKey")] string? expression = null) { } public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } + public new . HasAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public new . HasAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public new . HasCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public new . HasSingleItem() { } + public . HasSingleItem(<., bool> predicate, [.("predicate")] string? expression = null) { } + public new . IsEmpty() { } + public new . IsNotEmpty() { } public new . IsNotNull() { } public new . IsNull() { } } 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 d26f0e59c7..e8d5b32487 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 @@ -1035,11 +1035,18 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class DictionaryContainsKeyAndContinuation : . + where TDictionary : . + where TKey : notnull + { + public . Value { get; } + } public class DictionaryContainsKeyAssertion : . where TDictionary : . where TKey : notnull { public DictionaryContainsKeyAssertion(. context, TKey expectedKey, .? comparer = null) { } + public new . And { get; } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1061,6 +1068,19 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class DictionaryCountSource + where TDictionary : . + where TKey : notnull + { + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } public class DictionaryDoesNotContainKeyAssertion : . where TDictionary : . where TKey : notnull @@ -1080,6 +1100,7 @@ namespace .Conditions public class DictionaryMemberAssertionAdapter : . where TDictionary : . where TKey : notnull { } + public sealed class DictionaryValueSource : . { } [.("HasFiles")] public class DirectoryHasFilesAssertion : .<.DirectoryInfo> { @@ -1639,11 +1660,18 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class MutableDictionaryContainsKeyAndContinuation : . + where TDictionary : . + where TKey : notnull + { + public . Value { get; } + } public class MutableDictionaryContainsKeyAssertion : . where TDictionary : . where TKey : notnull { public MutableDictionaryContainsKeyAssertion(. context, TKey expectedKey, .? comparer = null) { } + public new . And { get; } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1665,6 +1693,19 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class MutableDictionaryCountSource + where TDictionary : . + where TKey : notnull + { + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } public class MutableDictionaryDoesNotContainKeyAssertion : . where TDictionary : . where TKey : notnull @@ -6218,9 +6259,17 @@ namespace .Sources public . ContainsKey(TKey expectedKey, .? comparer, [.("expectedKey")] string? keyExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsKeyWithValue(TKey expectedKey, TValue expectedValue, [.("expectedKey")] string? keyExpression = null, [.("expectedValue")] string? valueExpression = null) { } public . ContainsValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } + public new . Count() { } public . DoesNotContainKey(TKey expectedKey, [.("expectedKey")] string? expression = null) { } public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } + public new . HasAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public new . HasAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public new . HasCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public new . HasSingleItem() { } + public . HasSingleItem(<., bool> predicate, [.("predicate")] string? expression = null) { } + public new . IsEmpty() { } + public new . IsNotEmpty() { } public new . IsNotNull() { } public new . IsNull() { } } @@ -6337,9 +6386,17 @@ namespace .Sources public . ContainsKey(TKey expectedKey, .? comparer, [.("expectedKey")] string? keyExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsKeyWithValue(TKey expectedKey, TValue expectedValue, [.("expectedKey")] string? keyExpression = null, [.("expectedValue")] string? valueExpression = null) { } public . ContainsValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } + public new . Count() { } public . DoesNotContainKey(TKey expectedKey, [.("expectedKey")] string? expression = null) { } public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } + public new . HasAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public new . HasAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public new . HasCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public new . HasSingleItem() { } + public . HasSingleItem(<., bool> predicate, [.("predicate")] string? expression = null) { } + public new . IsEmpty() { } + public new . IsNotEmpty() { } public new . IsNotNull() { } public new . IsNull() { } } 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 6906a4914f..81abde7ca6 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 @@ -1052,11 +1052,18 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class DictionaryContainsKeyAndContinuation : . + where TDictionary : . + where TKey : notnull + { + public . Value { get; } + } public class DictionaryContainsKeyAssertion : . where TDictionary : . where TKey : notnull { public DictionaryContainsKeyAssertion(. context, TKey expectedKey, .? comparer = null) { } + public new . And { get; } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1078,6 +1085,19 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class DictionaryCountSource + where TDictionary : . + where TKey : notnull + { + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } public class DictionaryDoesNotContainKeyAssertion : . where TDictionary : . where TKey : notnull @@ -1097,6 +1117,7 @@ namespace .Conditions public class DictionaryMemberAssertionAdapter : . where TDictionary : . where TKey : notnull { } + public sealed class DictionaryValueSource : . { } [.("HasFiles")] public class DirectoryHasFilesAssertion : .<.DirectoryInfo> { @@ -1656,11 +1677,18 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class MutableDictionaryContainsKeyAndContinuation : . + where TDictionary : . + where TKey : notnull + { + public . Value { get; } + } public class MutableDictionaryContainsKeyAssertion : . where TDictionary : . where TKey : notnull { public MutableDictionaryContainsKeyAssertion(. context, TKey expectedKey, .? comparer = null) { } + public new . And { get; } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1682,6 +1710,19 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class MutableDictionaryCountSource + where TDictionary : . + where TKey : notnull + { + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } public class MutableDictionaryDoesNotContainKeyAssertion : . where TDictionary : . where TKey : notnull @@ -6296,9 +6337,17 @@ namespace .Sources public . ContainsKey(TKey expectedKey, .? comparer, [.("expectedKey")] string? keyExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsKeyWithValue(TKey expectedKey, TValue expectedValue, [.("expectedKey")] string? keyExpression = null, [.("expectedValue")] string? valueExpression = null) { } public . ContainsValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } + public new . Count() { } public . DoesNotContainKey(TKey expectedKey, [.("expectedKey")] string? expression = null) { } public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } + public new . HasAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public new . HasAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public new . HasCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public new . HasSingleItem() { } + public . HasSingleItem(<., bool> predicate, [.("predicate")] string? expression = null) { } + public new . IsEmpty() { } + public new . IsNotEmpty() { } public new . IsNotNull() { } public new . IsNull() { } } @@ -6415,9 +6464,17 @@ namespace .Sources public . ContainsKey(TKey expectedKey, .? comparer, [.("expectedKey")] string? keyExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsKeyWithValue(TKey expectedKey, TValue expectedValue, [.("expectedKey")] string? keyExpression = null, [.("expectedValue")] string? valueExpression = null) { } public . ContainsValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } + public new . Count() { } public . DoesNotContainKey(TKey expectedKey, [.("expectedKey")] string? expression = null) { } public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } + public new . HasAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public new . HasAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public new . HasCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public new . HasSingleItem() { } + public . HasSingleItem(<., bool> predicate, [.("predicate")] string? expression = null) { } + public new . IsEmpty() { } + public new . IsNotEmpty() { } public new . IsNotNull() { } public new . IsNull() { } } 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 171550131d..ea85ff13c7 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 @@ -880,11 +880,18 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class DictionaryContainsKeyAndContinuation : . + where TDictionary : . + where TKey : notnull + { + public . Value { get; } + } public class DictionaryContainsKeyAssertion : . where TDictionary : . where TKey : notnull { public DictionaryContainsKeyAssertion(. context, TKey expectedKey, .? comparer = null) { } + public new . And { get; } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -906,6 +913,19 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class DictionaryCountSource + where TDictionary : . + where TKey : notnull + { + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } public class DictionaryDoesNotContainKeyAssertion : . where TDictionary : . where TKey : notnull @@ -925,6 +945,7 @@ namespace .Conditions public class DictionaryMemberAssertionAdapter : . where TDictionary : . where TKey : notnull { } + public sealed class DictionaryValueSource : . { } [.("HasFiles")] public class DirectoryHasFilesAssertion : .<.DirectoryInfo> { @@ -1469,11 +1490,18 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class MutableDictionaryContainsKeyAndContinuation : . + where TDictionary : . + where TKey : notnull + { + public . Value { get; } + } public class MutableDictionaryContainsKeyAssertion : . where TDictionary : . where TKey : notnull { public MutableDictionaryContainsKeyAssertion(. context, TKey expectedKey, .? comparer = null) { } + public new . And { get; } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1495,6 +1523,19 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public sealed class MutableDictionaryCountSource + where TDictionary : . + where TKey : notnull + { + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } public class MutableDictionaryDoesNotContainKeyAssertion : . where TDictionary : . where TKey : notnull @@ -5412,9 +5453,17 @@ namespace .Sources public . ContainsKey(TKey expectedKey, .? comparer, [.("expectedKey")] string? keyExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsKeyWithValue(TKey expectedKey, TValue expectedValue, [.("expectedKey")] string? keyExpression = null, [.("expectedValue")] string? valueExpression = null) { } public . ContainsValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } + public new . Count() { } public . DoesNotContainKey(TKey expectedKey, [.("expectedKey")] string? expression = null) { } public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } + public new . HasAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public new . HasAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public new . HasCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public new . HasSingleItem() { } + public . HasSingleItem(<., bool> predicate, [.("predicate")] string? expression = null) { } + public new . IsEmpty() { } + public new . IsNotEmpty() { } public new . IsNotNull() { } public new . IsNull() { } } @@ -5483,9 +5532,17 @@ namespace .Sources public . ContainsKey(TKey expectedKey, .? comparer, [.("expectedKey")] string? keyExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsKeyWithValue(TKey expectedKey, TValue expectedValue, [.("expectedKey")] string? keyExpression = null, [.("expectedValue")] string? valueExpression = null) { } public . ContainsValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } + public new . Count() { } public . DoesNotContainKey(TKey expectedKey, [.("expectedKey")] string? expression = null) { } public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } + public new . HasAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public new . HasAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public new . HasCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public new . HasSingleItem() { } + public . HasSingleItem(<., bool> predicate, [.("predicate")] string? expression = null) { } + public new . IsEmpty() { } + public new . IsNotEmpty() { } public new . IsNotNull() { } public new . IsNull() { } }