From d3592d7b490c8890b1776b629b4a2984e6d6067c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 16 May 2026 10:22:06 +0200 Subject: [PATCH 1/2] feat: add `ComplyWith` to string-specialized Elements The Elements class for IEnumerable and IAsyncEnumerable was missing the ComplyWith overload that its generic siblings expose. This made `That(stringCollection).All().ComplyWith(it => ...)` and related quantifier chains fail to compile. Add the parallel overload on both sync and async sides. --- ...ThatAsyncEnumerable.Elements.ComplyWith.cs | 17 +++ .../ThatEnumerable.Elements.ComplyWith.cs | 110 ++++++++++++++++++ .../Expected/aweXpect_net10.0.txt | 2 + .../Expected/aweXpect_net8.0.txt | 2 + .../Expected/aweXpect_netstandard2.0.txt | 1 + ...hatAsyncEnumerable.All.ComplyWith.Tests.cs | 38 +++++- ...atEnumerable.All.ComplyWith.StringTests.cs | 64 ++++++++++ 7 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 Tests/aweXpect.Tests/Collections/ThatEnumerable.All.ComplyWith.StringTests.cs diff --git a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.Elements.ComplyWith.cs b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.Elements.ComplyWith.cs index f280b0ae6..5b4500e23 100644 --- a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.Elements.ComplyWith.cs +++ b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.Elements.ComplyWith.cs @@ -31,6 +31,23 @@ public partial class Elements } } + public partial class Elements + { + /// + /// …comply with the . + /// + public ObjectEqualityResult, IThat?>, string?> + ComplyWith(Action> expectations) + { + ObjectEqualityOptions options = new(); + return new ObjectEqualityResult, IThat?>, string?>( + _subject.Get().ExpectationBuilder.AddConstraint((expectationBuilder, it, grammars) + => new ComplyWithConstraint(expectationBuilder, it, grammars, _quantifier, expectations)), + _subject, + options); + } + } + private sealed class ComplyWithConstraint : ConstraintResult.WithValue?>, IAsyncContextConstraint?> diff --git a/Source/aweXpect/That/Collections/ThatEnumerable.Elements.ComplyWith.cs b/Source/aweXpect/That/Collections/ThatEnumerable.Elements.ComplyWith.cs index 0584b5380..ac395c55c 100644 --- a/Source/aweXpect/That/Collections/ThatEnumerable.Elements.ComplyWith.cs +++ b/Source/aweXpect/That/Collections/ThatEnumerable.Elements.ComplyWith.cs @@ -126,6 +126,116 @@ protected override void AppendNegatedResult(StringBuilder stringBuilder, string? } } + public partial class Elements + { + /// + /// …comply with the . + /// + public ObjectEqualityResult, IThat?>, string?> + ComplyWith(Action> expectations) + { + ObjectEqualityOptions options = new(); + return new ObjectEqualityResult, IThat?>, string?>( + _subject.Get().ExpectationBuilder.AddConstraint((expectationBuilder, it, grammars) + => new ComplyWithConstraint(expectationBuilder, it, grammars, _quantifier, expectations)), + _subject, + options); + } + + private sealed class ComplyWithConstraint + : ConstraintResult.WithNotNullValue?>, + IAsyncContextConstraint?> + { + private readonly ManualExpectationBuilder _itemExpectationBuilder; + private readonly EnumerableQuantifier _quantifier; + private int _matchingCount; + private int _notMatchingCount; + private int? _totalCount; + + public ComplyWithConstraint(ExpectationBuilder expectationBuilder, string it, ExpectationGrammars grammars, + EnumerableQuantifier quantifier, + Action> expectations) + : base(it, grammars) + { + _quantifier = quantifier; + _itemExpectationBuilder = new ManualExpectationBuilder(expectationBuilder, grammars); + expectations.Invoke(new ThatSubject(_itemExpectationBuilder)); + } + + public async Task IsMetBy( + IEnumerable? actual, + IEvaluationContext context, + CancellationToken cancellationToken) + { + Actual = actual; + if (actual is null) + { + Outcome = Outcome.Failure; + return this; + } + + IEnumerable materialized = context.UseMaterializedEnumerable>(actual); + bool cancelEarly = actual is not ICollection; + _matchingCount = 0; + _notMatchingCount = 0; + + foreach (string? item in materialized) + { + ConstraintResult isMatch = await _itemExpectationBuilder.IsMetBy(item, context, cancellationToken); + if (isMatch.Outcome == Outcome.Success) + { + _matchingCount++; + } + else + { + _notMatchingCount++; + } + + if (cancelEarly && _quantifier.IsDeterminable(_matchingCount, _notMatchingCount)) + { + Outcome = _quantifier.GetOutcome(_matchingCount, _notMatchingCount, _totalCount); + return this; + } + + if (cancellationToken.IsCancellationRequested) + { + Outcome = Outcome.Undecided; + return this; + } + } + + _totalCount = _matchingCount + _notMatchingCount; + Outcome = _quantifier.GetOutcome(_matchingCount, _notMatchingCount, _totalCount); + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + { + _itemExpectationBuilder.AppendExpectation(stringBuilder, indentation); + stringBuilder.Append(For); + stringBuilder.Append(_quantifier); + stringBuilder.Append(' '); + stringBuilder.Append(_quantifier.GetItemString()); + } + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + => _quantifier.AppendResult(stringBuilder, Grammars, _matchingCount, _notMatchingCount, _totalCount); + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(_quantifier); + stringBuilder.Append(For); + _itemExpectationBuilder.AppendExpectation(stringBuilder, indentation); + stringBuilder.Append(' '); + stringBuilder.Append(_quantifier.GetItemString()); + } + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + => _quantifier.AppendResult(stringBuilder, Grammars, _matchingCount, _notMatchingCount, + _totalCount); + } + } + public partial class ElementsForEnumerable { /// diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net10.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net10.0.txt index 2649ba3b1..32be042d4 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net10.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net10.0.txt @@ -152,6 +152,7 @@ namespace aweXpect public static aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat?>, TItem> StartsWith(this aweXpect.Core.IThat?> source, System.Collections.Generic.IEnumerable expected, [System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string doNotPopulateThisValue = "") { } public class Elements : aweXpect.ThatAsyncEnumerable.IElements { + public aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat?>, string?> ComplyWith(System.Action> expectations) { } public aweXpect.Results.AndOrResult, aweXpect.Core.IThat?>> Satisfy(System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } } public class Elements : aweXpect.ThatAsyncEnumerable.IElements @@ -631,6 +632,7 @@ namespace aweXpect public static aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat>, TItem> StartsWith(this aweXpect.Core.IThat> source, System.Collections.Generic.IEnumerable expected, [System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string doNotPopulateThisValue = "") { } public class Elements : aweXpect.ThatEnumerable.IElements { + public aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat?>, string?> ComplyWith(System.Action> expectations) { } public aweXpect.Results.AndOrResult, aweXpect.Core.IThat?>> Satisfy(System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } } public class Elements : aweXpect.ThatEnumerable.IElements diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt index 1e109dab5..bc644bbac 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt @@ -152,6 +152,7 @@ namespace aweXpect public static aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat?>, TItem> StartsWith(this aweXpect.Core.IThat?> source, System.Collections.Generic.IEnumerable expected, [System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string doNotPopulateThisValue = "") { } public class Elements : aweXpect.ThatAsyncEnumerable.IElements { + public aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat?>, string?> ComplyWith(System.Action> expectations) { } public aweXpect.Results.AndOrResult, aweXpect.Core.IThat?>> Satisfy(System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } } public class Elements : aweXpect.ThatAsyncEnumerable.IElements @@ -631,6 +632,7 @@ namespace aweXpect public static aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat>, TItem> StartsWith(this aweXpect.Core.IThat> source, System.Collections.Generic.IEnumerable expected, [System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string doNotPopulateThisValue = "") { } public class Elements : aweXpect.ThatEnumerable.IElements { + public aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat?>, string?> ComplyWith(System.Action> expectations) { } public aweXpect.Results.AndOrResult, aweXpect.Core.IThat?>> Satisfy(System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } } public class Elements : aweXpect.ThatEnumerable.IElements diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt index 77feab62a..921975285 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt @@ -394,6 +394,7 @@ namespace aweXpect public static aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat?>, TItem> StartsWith(this aweXpect.Core.IThat?> source, System.Collections.Generic.IEnumerable expected, [System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string doNotPopulateThisValue = "") { } public class Elements : aweXpect.ThatEnumerable.IElements { + public aweXpect.Results.ObjectEqualityResult, aweXpect.Core.IThat?>, string?> ComplyWith(System.Action> expectations) { } public aweXpect.Results.AndOrResult, aweXpect.Core.IThat?>> Satisfy(System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } } public class Elements : aweXpect.ThatEnumerable.IElements diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.All.ComplyWith.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.All.ComplyWith.Tests.cs index 3d074368f..fc648e361 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.All.ComplyWith.Tests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.All.ComplyWith.Tests.cs @@ -79,7 +79,7 @@ but not all were [Fact] public async Task WhenEnumerableIsEmpty_ShouldSucceed() { - IAsyncEnumerable subject = ToAsyncEnumerable((int[]) []); + IAsyncEnumerable subject = ToAsyncEnumerable((int[])[]); async Task Act() => await That(subject).All().ComplyWith(x => x.IsEqualTo(0)); @@ -115,6 +115,42 @@ but it was } } + public sealed class StringTests + { + [Fact] + public async Task WhenAllItemsMatchExpectation_ShouldSucceed() + { + IAsyncEnumerable subject = ToAsyncEnumerable("apple", "ant", "avocado"); + + async Task Act() + => await That(subject).All().ComplyWith(it => it.StartsWith("a")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenExactlyOneItemMatchesExpectation_ShouldSucceed() + { + IAsyncEnumerable subject = ToAsyncEnumerable("apple", "banana", "cherry"); + + async Task Act() + => await That(subject).Exactly(1).ComplyWith(it => it.StartsWith("b")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenNotAllItemsMatch_ShouldFail() + { + IAsyncEnumerable subject = ToAsyncEnumerable("apple", "banana", "avocado"); + + async Task Act() + => await That(subject).All().ComplyWith(it => it.StartsWith("a")); + + await That(Act).Throws(); + } + } + public sealed class NegatedTests { [Fact] diff --git a/Tests/aweXpect.Tests/Collections/ThatEnumerable.All.ComplyWith.StringTests.cs b/Tests/aweXpect.Tests/Collections/ThatEnumerable.All.ComplyWith.StringTests.cs new file mode 100644 index 000000000..30f1cefa6 --- /dev/null +++ b/Tests/aweXpect.Tests/Collections/ThatEnumerable.All.ComplyWith.StringTests.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; + +namespace aweXpect.Tests; + +public sealed partial class ThatEnumerable +{ + public sealed partial class All + { + public sealed partial class ComplyWith + { + public sealed class StringTests + { + [Fact] + public async Task WhenAllItemsMatchExpectation_ShouldSucceed() + { + IEnumerable subject = ["apple", "ant", "avocado",]; + + async Task Act() + => await That(subject).All().ComplyWith(it => it.StartsWith("a")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenExactlyOneItemMatchesExpectation_ShouldSucceed() + { + IEnumerable subject = ["apple", "banana", "cherry",]; + + async Task Act() + => await That(subject).Exactly(1).ComplyWith(it => it.StartsWith("b")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenNotAllItemsMatch_ShouldFail() + { + IEnumerable subject = ["apple", "banana", "avocado",]; + + async Task Act() + => await That(subject).All().ComplyWith(it => it.StartsWith("a")); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + starts with "a" for all items, + but only 2 of 3 were + """).AsPrefix(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + IEnumerable? subject = null; + + async Task Act() + => await That(subject).All().ComplyWith(it => it.StartsWith("a")); + + await That(Act).Throws(); + } + } + } + } +} From 9ad9a34f17ee550ac34c25a7e944193f760fb5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 16 May 2026 18:59:50 +0200 Subject: [PATCH 2/2] Fix review issues --- ...hatAsyncEnumerable.All.ComplyWith.Tests.cs | 32 ++++++++++++++++++- ...atEnumerable.All.ComplyWith.StringTests.cs | 7 +++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.All.ComplyWith.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.All.ComplyWith.Tests.cs index fc648e361..8c6faa376 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.All.ComplyWith.Tests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.All.ComplyWith.Tests.cs @@ -147,7 +147,37 @@ public async Task WhenNotAllItemsMatch_ShouldFail() async Task Act() => await That(subject).All().ComplyWith(it => it.StartsWith("a")); - await That(Act).Throws(); + await That(Act).Throws() + .WithMessage(""" + Expected that subject + starts with "a" for all items, + but not all were + + Actual: + apple + + Actual: + banana + + Expected: + a + """); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + IAsyncEnumerable? subject = null; + + async Task Act() + => await That(subject).All().ComplyWith(it => it.StartsWith("a")); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + starts with "a" for all items, + but it was + """); } } diff --git a/Tests/aweXpect.Tests/Collections/ThatEnumerable.All.ComplyWith.StringTests.cs b/Tests/aweXpect.Tests/Collections/ThatEnumerable.All.ComplyWith.StringTests.cs index 30f1cefa6..d20b2eec4 100644 --- a/Tests/aweXpect.Tests/Collections/ThatEnumerable.All.ComplyWith.StringTests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatEnumerable.All.ComplyWith.StringTests.cs @@ -56,7 +56,12 @@ public async Task WhenSubjectIsNull_ShouldFail() async Task Act() => await That(subject).All().ComplyWith(it => it.StartsWith("a")); - await That(Act).Throws(); + await That(Act).Throws() + .WithMessage(""" + Expected that subject + starts with "a" for all items, + but it was + """); } } }