diff --git a/Docs/pages/docs/expectations/07-collections.md b/Docs/pages/docs/expectations/07-collections.md index 848fccc67..a75327aff 100644 --- a/Docs/pages/docs/expectations/07-collections.md +++ b/Docs/pages/docs/expectations/07-collections.md @@ -345,7 +345,7 @@ await Expect.That(["FOO", "BAR"]).EndsWith(["bar"]).IgnoringCase(); ## Have -Specifications that count the elements in a collection. +Specifications that count the elements in a collection that satisfy specific conditions. ### All @@ -487,6 +487,21 @@ await Expect.That(result).IsGreaterThan(41); *Note: The same expectation works also for `IAsyncEnumerable`.* + +## Have item at index + +You can verify that the collection contains an item that satisfies the expectation on a given index (or any index). + +```csharp +IEnumerable values = ["0th item", "1st item", "2nd item", "3rd item"]; + +await Expect.That(values).HasItem("1st item").AtIndex(1); +await Expect.That(values).HasItem(a => a.StartsWith("2nd")).AtAnyIndex(); +``` + +*Note: The same expectation works also for `IAsyncEnumerable`.* + + ## Dictionaries ### Contain key(s) diff --git a/Pipeline/Build.cs b/Pipeline/Build.cs index a07a0cc08..25356ef59 100644 --- a/Pipeline/Build.cs +++ b/Pipeline/Build.cs @@ -19,7 +19,7 @@ partial class Build : NukeBuild /// /// Afterward, you can update the package reference in `Directory.Packages.props` and reset this flag. /// - readonly BuildScope BuildScope = BuildScope.Default; + readonly BuildScope BuildScope = BuildScope.CoreOnly; [Parameter("Github Token")] readonly string GithubToken; diff --git a/Source/aweXpect.Core/Options/CollectionIndexOptions.cs b/Source/aweXpect.Core/Options/CollectionIndexOptions.cs new file mode 100644 index 000000000..09a65b8c1 --- /dev/null +++ b/Source/aweXpect.Core/Options/CollectionIndexOptions.cs @@ -0,0 +1,66 @@ +namespace aweXpect.Options; + +/// +/// Options for limitations on a collection index. +/// +public class CollectionIndexOptions +{ + private int? _maximum; + private int? _minimum; + + /// + /// Checks if the is in range. + /// + /// + /// , if the is in range, , + /// if the is not in range, but could be in range for a larger index, + /// otherwise when the is not in range + /// and will also not be in range for larger values. + /// + public bool? IsIndexInRange(int index) + { + if (_maximum.HasValue && index > _maximum) + { + return false; + } + + if ((_minimum is null || index >= _minimum) && + (_maximum is null || index <= _maximum)) + { + return true; + } + + return null; + } + + /// + /// Flag indicating, if only a single index is considered in range. + /// + public bool HasOnlySingleIndex() + => _maximum == _minimum && _minimum is not null; + + /// + /// Set the checked index to be in range between and . + /// + /// When either parameter is set to , the corresponding range direction is unlimited. + public void SetIndexRange(int? minimum, int? maximum) + { + _minimum = minimum; + _maximum = maximum; + } + + /// + /// Returns the description of the . + /// + public string GetDescription() + { + if (_minimum is null && _maximum is null) + { + return ""; + } + + return _minimum == _maximum + ? $" at index {_minimum}" + : $" with index between {_minimum} and {_maximum}"; + } +} diff --git a/Source/aweXpect.Core/Results/HasItemResult.cs b/Source/aweXpect.Core/Results/HasItemResult.cs new file mode 100644 index 000000000..23be805b8 --- /dev/null +++ b/Source/aweXpect.Core/Results/HasItemResult.cs @@ -0,0 +1,34 @@ +using aweXpect.Core; +using aweXpect.Options; + +namespace aweXpect.Results; + +/// +/// The result for verifying that a collection has a matching item at a given index. +/// +/// +/// +/// +public class HasItemResult( + ExpectationBuilder expectationBuilder, + IThat collection, + CollectionIndexOptions collectionIndexOptions) +{ + /// + /// …at any index. + /// + public AndOrResult> AtAnyIndex() + { + collectionIndexOptions.SetIndexRange(null, null); + return new AndOrResult>(expectationBuilder, collection); + } + + /// + /// …at the given . + /// + public AndOrResult> AtIndex(int index) + { + collectionIndexOptions.SetIndexRange(index, index); + return new AndOrResult>(expectationBuilder, collection); + } +} diff --git a/Source/aweXpect/Helpers/IMaterializedEnumerable.cs b/Source/aweXpect/Helpers/IMaterializedEnumerable.cs index b32c9d3b6..cfec3486a 100644 --- a/Source/aweXpect/Helpers/IMaterializedEnumerable.cs +++ b/Source/aweXpect/Helpers/IMaterializedEnumerable.cs @@ -1,10 +1,12 @@ #if NET8_0_OR_GREATER using System.Collections.Generic; +using System.Threading.Tasks; namespace aweXpect.Helpers; internal interface IMaterializedEnumerable : ICountable { IReadOnlyList MaterializedItems { get; } + Task MaterializeItems(int minimumNumberOfItems); } #endif diff --git a/Source/aweXpect/Helpers/MaterializingAsyncEnumerable.cs b/Source/aweXpect/Helpers/MaterializingAsyncEnumerable.cs index 1463f1fa9..d7b177d54 100644 --- a/Source/aweXpect/Helpers/MaterializingAsyncEnumerable.cs +++ b/Source/aweXpect/Helpers/MaterializingAsyncEnumerable.cs @@ -1,6 +1,7 @@ #if NET8_0_OR_GREATER using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; namespace aweXpect.Helpers; @@ -26,13 +27,14 @@ public async IAsyncEnumerator GetAsyncEnumerator( while (await _enumerator.MoveNextAsync()) { + T item = _enumerator.Current; + _materializedItems.Add(item); + if (cancellationToken.IsCancellationRequested) { break; } - T item = _enumerator.Current; - _materializedItems.Add(item); yield return item; } @@ -47,6 +49,21 @@ public async IAsyncEnumerator GetAsyncEnumerator( /// IReadOnlyList IMaterializedEnumerable.MaterializedItems => _materializedItems; + /// + public async Task MaterializeItems(int minimumNumberOfItems) + { + int index = 0; + await foreach (T _ in this) + { + if (index++ > minimumNumberOfItems) + { + return; + } + } + + Count = _materializedItems.Count; + } + public static IAsyncEnumerable Wrap(IAsyncEnumerable enumerable) { if (enumerable is MaterializingAsyncEnumerable) diff --git a/Source/aweXpect/Results/HasItemObjectResult.cs b/Source/aweXpect/Results/HasItemObjectResult.cs new file mode 100644 index 000000000..ebfc92dda --- /dev/null +++ b/Source/aweXpect/Results/HasItemObjectResult.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using aweXpect.Core; +using aweXpect.Equivalency; +using aweXpect.Options; + +namespace aweXpect.Results; + +/// +/// The result for verifying that a collection has a specific item at a given index. +/// +/// +/// +/// +public class HasItemObjectResult( + ExpectationBuilder expectationBuilder, + IThat collection, + CollectionIndexOptions collectionIndexOptions, + ObjectEqualityOptions options) + : HasItemObjectResult>( + expectationBuilder, + collection, + collectionIndexOptions, + options); + + +/// +/// The result for verifying that a collection has a specific item at a given index. +/// +/// +/// +/// +public class HasItemObjectResult( + ExpectationBuilder expectationBuilder, + IThat collection, + CollectionIndexOptions collectionIndexOptions, + ObjectEqualityOptions options) + : HasItemResult(expectationBuilder, collection, collectionIndexOptions) + where TSelf : HasItemObjectResult +{ + /// + /// Use equivalency to compare objects. + /// + public TSelf Equivalent(Func? optionsCallback = null) + { + options.Equivalent(EquivalencyOptionsExtensions.FromCallback(optionsCallback)); + return (TSelf)this; + } + + /// + /// Uses the provided for comparing s. + /// + public TSelf Using(IEqualityComparer comparer) + { + options.Using(comparer); + return (TSelf)this; + } +} diff --git a/Source/aweXpect/That/Collections/CollectionHelpers.cs b/Source/aweXpect/That/Collections/CollectionHelpers.cs index e4e19ef62..e10d85f6c 100644 --- a/Source/aweXpect/That/Collections/CollectionHelpers.cs +++ b/Source/aweXpect/That/Collections/CollectionHelpers.cs @@ -2,7 +2,9 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using aweXpect.Core; +using aweXpect.Customization; using aweXpect.Helpers; namespace aweXpect; @@ -101,7 +103,7 @@ internal static ExpectationBuilder AddCollectionContext(this ExpectationBuilder } #if NET8_0_OR_GREATER - internal static ExpectationBuilder AddCollectionContext(this ExpectationBuilder expectationBuilder, + internal static async Task AddCollectionContext(this ExpectationBuilder expectationBuilder, IMaterializedEnumerable? value, bool isIncomplete = false) { if (value is null) @@ -109,6 +111,8 @@ internal static ExpectationBuilder AddCollectionContext(this ExpectationB return expectationBuilder; } + await value.MaterializeItems(Customize.aweXpect.Formatting().MaximumNumberOfCollectionItems.Get()); + return expectationBuilder.UpdateContexts(contexts => { diff --git a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.EndsWith.cs b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.EndsWith.cs index 537a85f83..0b7cb5574 100644 --- a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.EndsWith.cs +++ b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.EndsWith.cs @@ -266,7 +266,7 @@ public async Task IsMetBy(IAsyncEnumerable? actual, IEv if (_index + _offset < 0) { Outcome = Outcome.Failure; - _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable); + await _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable); return this; } @@ -276,8 +276,7 @@ public async Task IsMetBy(IAsyncEnumerable? actual, IEv { _firstMismatchItem = item; _foundMismatch = true; - _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable, - true); + await _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable); Outcome = Outcome.Failure; return this; } diff --git a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.HasItem.cs b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.HasItem.cs new file mode 100644 index 000000000..bb9766454 --- /dev/null +++ b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.HasItem.cs @@ -0,0 +1,165 @@ +#if NET8_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Core.EvaluationContext; +using aweXpect.Helpers; +using aweXpect.Options; +using aweXpect.Results; + +// ReSharper disable PossibleMultipleEnumeration + +namespace aweXpect; + +public static partial class ThatAsyncEnumerable +{ + /// + /// Verifies that the collection has an item matching the … + /// + public static HasItemResult?> HasItem( + this IThat?> source, Func predicate, + [CallerArgumentExpression("predicate")] + string doNotPopulateThisValue = "") + { + CollectionIndexOptions indexOptions = new(); + ExpectationBuilder expectationBuilder = source.Get().ExpectationBuilder; + return new HasItemResult?>( + expectationBuilder.AddConstraint((it, grammars) + => new HasItemConstraint(expectationBuilder, it, grammars, predicate, + () => doNotPopulateThisValue, indexOptions)), + source, + indexOptions); + } + + /// + /// Verifies that the collection has the item… + /// + public static HasItemObjectResult?, TItem> HasItem( + this IThat?> source, TItem expected) + { + CollectionIndexOptions indexOptions = new(); + ExpectationBuilder expectationBuilder = source.Get().ExpectationBuilder; + ObjectEqualityOptions options = new(); + return new HasItemObjectResult?, TItem>( + expectationBuilder.AddConstraint((it, grammars) + => new HasItemConstraint(expectationBuilder, it, grammars, + a => options.AreConsideredEqual(a, expected), + () => $"{Formatter.Format(expected)}{options}", + indexOptions)), + source, + indexOptions, + options); + } + + private sealed class HasItemConstraint( + ExpectationBuilder expectationBuilder, + string it, + ExpectationGrammars grammars, + Func predicate, + Func predicateDescription, + CollectionIndexOptions options) + : ConstraintResult.WithValue?>(grammars), + IAsyncContextConstraint?> + { + private TItem? _actual; + private bool _hasIndex; + + public async Task IsMetBy(IAsyncEnumerable? actual, IEvaluationContext context, + CancellationToken cancellationToken) + { + Actual = actual; + if (actual is null) + { + Outcome = Outcome.Failure; + return this; + } + + IAsyncEnumerable materialized = + context.UseMaterializedAsyncEnumerable>(actual); + await expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable); + _hasIndex = false; + Outcome = Outcome.Failure; + + int index = -1; + await foreach (TItem item in materialized.WithCancellation(cancellationToken)) + { + index++; + bool? isIndexInRange = options.IsIndexInRange(index); + if (isIndexInRange != true) + { + if (isIndexInRange == false) + { + break; + } + + continue; + } + + _hasIndex = true; + _actual = item; + Outcome = predicate(item) ? Outcome.Success : Outcome.Failure; + if (Outcome == Outcome.Success) + { + break; + } + } + + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("has item ").Append(predicateDescription()).Append(options.GetDescription()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + if (Actual is null) + { + stringBuilder.ItWasNull(it); + } + else if (_hasIndex) + { + if (options.HasOnlySingleIndex()) + { + stringBuilder.Append(it).Append(" had item "); + Formatter.Format(stringBuilder, _actual); + stringBuilder.Append(options.GetDescription()); + } + else + { + string optionDescription = options.GetDescription(); + if (string.IsNullOrEmpty(optionDescription)) + { + optionDescription = " at any index"; + } + + stringBuilder.Append(it).Append(" did not match").Append(optionDescription); + } + } + else + { + stringBuilder.Append(it).Append(" did not contain any item").Append(options.GetDescription()); + } + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("does not have item ").Append(predicateDescription()) + .Append(options.GetDescription()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + if (_actual is null) + { + stringBuilder.ItWasNull(it); + } + else + { + stringBuilder.Append(it).Append(" did"); + } + } + } +} +#endif diff --git a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.HasSingle.cs b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.HasSingle.cs index e79575a74..2191d1c38 100644 --- a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.HasSingle.cs +++ b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.HasSingle.cs @@ -87,7 +87,7 @@ public async Task IsMetBy(IAsyncEnumerable? actual, IEv Outcome = _count == 1 ? Outcome.Success : Outcome.Failure; if (_count > 1) { - expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable); + await expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable); } return this; diff --git a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.StartsWith.cs b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.StartsWith.cs index 590dc7d0f..0116f0e14 100644 --- a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.StartsWith.cs +++ b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.StartsWith.cs @@ -259,8 +259,7 @@ public async Task IsMetBy(IAsyncEnumerable? actual, IEv { _firstMismatchItem = item; _foundMismatch = true; - _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable, - true); + await _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable); Outcome = Outcome.Failure; return this; } @@ -273,7 +272,7 @@ public async Task IsMetBy(IAsyncEnumerable? actual, IEv } } - _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable); + await _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable); Outcome = Outcome.Failure; return this; } diff --git a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.cs b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.cs index e773549e1..f34808f29 100644 --- a/Source/aweXpect/That/Collections/ThatAsyncEnumerable.cs +++ b/Source/aweXpect/That/Collections/ThatAsyncEnumerable.cs @@ -249,7 +249,7 @@ public async Task IsMetBy( if (_quantifier.IsDeterminable(_matchingCount, _notMatchingCount)) { Outcome = _quantifier.GetOutcome(_matchingCount, _notMatchingCount, _totalCount); - _expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable, true); + await _expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable); return this; } } @@ -257,13 +257,13 @@ public async Task IsMetBy( if (cancellationToken.IsCancellationRequested) { Outcome = Outcome.Undecided; - _expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable, true); + await _expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable, true); return this; } _totalCount = _matchingCount + _notMatchingCount; Outcome = _quantifier.GetOutcome(_matchingCount, _notMatchingCount, _totalCount); - _expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable); + await _expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable); return this; } @@ -363,13 +363,12 @@ public async Task IsMetBy(IAsyncEnumerable? actual, IEv { _failure ??= TooManyDeviationsError(); Outcome = Outcome.Failure; - expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable, - true); + await expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable); return this; } } - expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable); + await expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable); if (matcher.VerifyComplete(It, options, maximumNumber, out _failure)) { _failure ??= TooManyDeviationsError(); @@ -452,7 +451,7 @@ public async Task IsMetBy(IAsyncEnumerable? actual, IEv IAsyncEnumerable materialized = context .UseMaterializedAsyncEnumerable>(actual); - expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable); + await expectationBuilder.AddCollectionContext(materialized as IMaterializedEnumerable); TMember previous = default!; int index = 0; diff --git a/Source/aweXpect/That/Collections/ThatEnumerable.HasItem.cs b/Source/aweXpect/That/Collections/ThatEnumerable.HasItem.cs new file mode 100644 index 000000000..8ed042eda --- /dev/null +++ b/Source/aweXpect/That/Collections/ThatEnumerable.HasItem.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Core.EvaluationContext; +using aweXpect.Helpers; +using aweXpect.Options; +using aweXpect.Results; +#if NET8_0_OR_GREATER +using System.Collections.Immutable; +#endif + +// ReSharper disable PossibleMultipleEnumeration + +namespace aweXpect; + +public static partial class ThatEnumerable +{ + /// + /// Verifies that the collection has an item matching the … + /// + public static HasItemResult?> HasItem( + this IThat?> source, Func predicate, + [CallerArgumentExpression("predicate")] + string doNotPopulateThisValue = "") + { + CollectionIndexOptions indexOptions = new(); + ExpectationBuilder expectationBuilder = source.Get().ExpectationBuilder; + return new HasItemResult?>( + expectationBuilder.AddConstraint((it, grammars) + => new HasItemConstraint(expectationBuilder, it, grammars, predicate, + () => doNotPopulateThisValue, indexOptions)), + source, + indexOptions); + } + + /// + /// Verifies that the collection has the item… + /// + public static HasItemObjectResult?, TItem> HasItem( + this IThat?> source, TItem expected) + { + CollectionIndexOptions indexOptions = new(); + ExpectationBuilder expectationBuilder = source.Get().ExpectationBuilder; + ObjectEqualityOptions options = new(); + return new HasItemObjectResult?, TItem>( + expectationBuilder.AddConstraint((it, grammars) + => new HasItemConstraint(expectationBuilder, it, grammars, + a => options.AreConsideredEqual(a, expected), + () => $"{Formatter.Format(expected)}{options}", + indexOptions)), + source, + indexOptions, + options); + } + + /// + /// Verifies that the collection has an item matching the … + /// + public static HasItemResult HasItem( + this IThat source, Func predicate, + [CallerArgumentExpression("predicate")] + string doNotPopulateThisValue = "") + { + CollectionIndexOptions indexOptions = new(); + ExpectationBuilder expectationBuilder = source.Get().ExpectationBuilder; + return new HasItemResult( + expectationBuilder.AddConstraint((it, grammars) + => new HasItemForEnumerableConstraint( + expectationBuilder, it, grammars, + predicate, () => doNotPopulateThisValue, + indexOptions)), + source, + indexOptions); + } + + /// + /// Verifies that the collection has the item… + /// + public static HasItemObjectResult HasItem( + this IThat source, object? expected) + { + CollectionIndexOptions indexOptions = new(); + ExpectationBuilder expectationBuilder = source.Get().ExpectationBuilder; + ObjectEqualityOptions options = new(); + return new HasItemObjectResult( + expectationBuilder.AddConstraint((it, grammars) + => new HasItemForEnumerableConstraint( + expectationBuilder, it, grammars, + a => options.AreConsideredEqual(a, expected), + () => $"{Formatter.Format(expected)}{options}", + indexOptions)), + source, + indexOptions, + options); + } + +#if NET8_0_OR_GREATER + /// + /// Verifies that the collection has an item matching the … + /// + public static HasItemResult> HasItem( + this IThat> source, Func predicate, + [CallerArgumentExpression("predicate")] + string doNotPopulateThisValue = "") + { + CollectionIndexOptions indexOptions = new(); + ExpectationBuilder expectationBuilder = source.Get().ExpectationBuilder; + return new HasItemResult>( + expectationBuilder.AddConstraint((it, grammars) + => new HasItemForEnumerableConstraint, TItem>( + expectationBuilder, it, grammars, + predicate, () => doNotPopulateThisValue, + indexOptions)), + source, + indexOptions); + } +#endif + +#if NET8_0_OR_GREATER + /// + /// Verifies that the collection has the item… + /// + public static HasItemObjectResult, TItem> HasItem( + this IThat> source, TItem expected) + { + CollectionIndexOptions indexOptions = new(); + ExpectationBuilder expectationBuilder = source.Get().ExpectationBuilder; + ObjectEqualityOptions options = new(); + return new HasItemObjectResult, TItem>( + expectationBuilder.AddConstraint((it, grammars) + => new HasItemForEnumerableConstraint, TItem>( + expectationBuilder, it, grammars, + a => options.AreConsideredEqual(a, expected), + () => $"{Formatter.Format(expected)}{options}", + indexOptions)), + source, + indexOptions, + options); + } +#endif + + private sealed class HasItemConstraint( + ExpectationBuilder expectationBuilder, + string it, + ExpectationGrammars grammars, + Func predicate, + Func predicateDescription, + CollectionIndexOptions options) + : ConstraintResult.WithValue?>(grammars), + IContextConstraint?> + { + private TItem? _actual; + private bool _hasIndex; + + public ConstraintResult IsMetBy(IEnumerable? actual, IEvaluationContext context) + { + Actual = actual; + if (actual is null) + { + Outcome = Outcome.Failure; + return this; + } + + IEnumerable materialized = context.UseMaterializedEnumerable>(actual); + expectationBuilder.AddCollectionContext(materialized); + _hasIndex = false; + Outcome = Outcome.Failure; + + int index = -1; + foreach (TItem item in materialized) + { + index++; + bool? isIndexInRange = options.IsIndexInRange(index); + if (isIndexInRange != true) + { + if (isIndexInRange == false) + { + break; + } + + continue; + } + + _hasIndex = true; + _actual = item; + Outcome = predicate(item) ? Outcome.Success : Outcome.Failure; + if (Outcome == Outcome.Success) + { + break; + } + } + + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("has item ").Append(predicateDescription()).Append(options.GetDescription()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + if (Actual is null) + { + stringBuilder.ItWasNull(it); + } + else if (_hasIndex) + { + if (options.HasOnlySingleIndex()) + { + stringBuilder.Append(it).Append(" had item "); + Formatter.Format(stringBuilder, _actual); + stringBuilder.Append(options.GetDescription()); + } + else + { + string optionDescription = options.GetDescription(); + if (string.IsNullOrEmpty(optionDescription)) + { + optionDescription = " at any index"; + } + + stringBuilder.Append(it).Append(" did not match").Append(optionDescription); + } + } + else + { + stringBuilder.Append(it).Append(" did not contain any item").Append(options.GetDescription()); + } + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("does not have item ").Append(predicateDescription()) + .Append(options.GetDescription()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + if (_actual is null) + { + stringBuilder.ItWasNull(it); + } + else + { + stringBuilder.Append(it).Append(" did"); + } + } + } + + private sealed class HasItemForEnumerableConstraint( + ExpectationBuilder expectationBuilder, + string it, + ExpectationGrammars grammars, + Func predicate, + Func predicateDescription, + CollectionIndexOptions options) + : ConstraintResult.WithValue(grammars), + IContextConstraint + where TEnumerable : IEnumerable? + { + private object? _actual; + + public ConstraintResult IsMetBy(TEnumerable actual, IEvaluationContext context) + { + Actual = actual; + if (actual is null) + { + Outcome = Outcome.Failure; + return this; + } + + IEnumerable materialized = context.UseMaterializedEnumerable(actual); + expectationBuilder.AddCollectionContext(materialized); + Outcome = Outcome.Failure; + + int index = -1; + foreach (TItem item in materialized.Cast()) + { + index++; + bool? isIndexInRange = options.IsIndexInRange(index); + if (isIndexInRange != true) + { + if (isIndexInRange == false) + { + break; + } + + continue; + } + + _actual = item; + Outcome = predicate(item) ? Outcome.Success : Outcome.Failure; + if (Outcome == Outcome.Success) + { + break; + } + } + + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("has item ").Append(predicateDescription()).Append(options.GetDescription()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + if (Actual is null) + { + stringBuilder.ItWasNull(it); + } + else if (_actual is not null) + { + if (options.HasOnlySingleIndex()) + { + stringBuilder.Append(it).Append(" had item "); + Formatter.Format(stringBuilder, _actual); + stringBuilder.Append(options.GetDescription()); + } + else + { + string optionDescription = options.GetDescription(); + if (string.IsNullOrEmpty(optionDescription)) + { + optionDescription = " at any index"; + } + + stringBuilder.Append(it).Append(" did not match").Append(optionDescription); + } + } + else + { + stringBuilder.Append(it).Append(" did not contain any item").Append(options.GetDescription()); + } + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("does not have item ").Append(predicateDescription()) + .Append(options.GetDescription()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + if (_actual is null) + { + stringBuilder.ItWasNull(it); + } + else + { + stringBuilder.Append(it).Append(" did"); + } + } + } +} diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt index 30d77f281..253aeb4ee 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt @@ -88,6 +88,8 @@ namespace aweXpect public static aweXpect.ThatAsyncEnumerable.Elements Exactly(this aweXpect.Core.IThat?> subject, int expected) { } public static aweXpect.CollectionCountResult, aweXpect.Core.IThat?>>> HasCount(this aweXpect.Core.IThat?> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat?>> HasCount(this aweXpect.Core.IThat?> subject, int expected) { } + public static aweXpect.Results.HasItemObjectResult?, TItem> HasItem(this aweXpect.Core.IThat?> source, TItem expected) { } + public static aweXpect.Results.HasItemResult?> HasItem(this aweXpect.Core.IThat?> source, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } public static aweXpect.Results.SingleItemResult, TItem>.Async HasSingle(this aweXpect.Core.IThat?> source) { } public static aweXpect.Results.StringCollectionBeContainedInResult, aweXpect.Core.IThat?>> IsContainedIn(this aweXpect.Core.IThat?> source, System.Collections.Generic.IEnumerable expected, [System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string doNotPopulateThisValue = "") { } public static aweXpect.Results.ObjectCollectionBeContainedInResult, aweXpect.Core.IThat?>, TItem> IsContainedIn(this aweXpect.Core.IThat?> source, System.Collections.Generic.IEnumerable expected, [System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string doNotPopulateThisValue = "") { } @@ -479,6 +481,12 @@ namespace aweXpect public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat?>> HasCount(this aweXpect.Core.IThat?> subject, int expected) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HasCount(this aweXpect.Core.IThat> subject, int expected) { } public static aweXpect.Results.AndOrResult> HasCount(this aweXpect.Core.IThat subject, int expected) { } + public static aweXpect.Results.HasItemObjectResult HasItem(this aweXpect.Core.IThat source, object? expected) { } + public static aweXpect.Results.HasItemResult HasItem(this aweXpect.Core.IThat source, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } + public static aweXpect.Results.HasItemObjectResult?, TItem> HasItem(this aweXpect.Core.IThat?> source, TItem expected) { } + public static aweXpect.Results.HasItemObjectResult, TItem> HasItem(this aweXpect.Core.IThat> source, TItem expected) { } + public static aweXpect.Results.HasItemResult?> HasItem(this aweXpect.Core.IThat?> source, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } + public static aweXpect.Results.HasItemResult> HasItem(this aweXpect.Core.IThat> source, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } public static aweXpect.Results.SingleItemResult HasSingle(this aweXpect.Core.IThat source) { } public static aweXpect.Results.SingleItemResult, TItem> HasSingle(this aweXpect.Core.IThat?> source) { } public static aweXpect.Results.SingleItemResult, TItem> HasSingle(this aweXpect.Core.IThat> source) { } @@ -1230,6 +1238,17 @@ namespace aweXpect.Results aweXpect.Results.EventTriggerResult WithParameter(string expression, int? position, System.Func predicate); } } + public class HasItemObjectResult : aweXpect.Results.HasItemObjectResult> + { + public HasItemObjectResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat collection, aweXpect.Options.CollectionIndexOptions collectionIndexOptions, aweXpect.Options.ObjectEqualityOptions options) { } + } + public class HasItemObjectResult : aweXpect.Results.HasItemResult + where TSelf : aweXpect.Results.HasItemObjectResult + { + public HasItemObjectResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat collection, aweXpect.Options.CollectionIndexOptions collectionIndexOptions, aweXpect.Options.ObjectEqualityOptions options) { } + public TSelf Equivalent(System.Func? optionsCallback = null) { } + public TSelf Using(System.Collections.Generic.IEqualityComparer comparer) { } + } public class ObjectCollectionBeContainedInResult : aweXpect.Results.ObjectCollectionMatchResult { public ObjectCollectionBeContainedInResult(aweXpect.Core.ExpectationBuilder expectationBuilder, TThat returnValue, aweXpect.Options.ObjectEqualityOptions options, aweXpect.Options.CollectionMatchOptions collectionMatchOptions) { } diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt index 5693094e9..4e99c2639 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt @@ -291,6 +291,10 @@ namespace aweXpect public static aweXpect.CollectionCountResult>> HasCount(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat?>> HasCount(this aweXpect.Core.IThat?> subject, int expected) { } public static aweXpect.Results.AndOrResult> HasCount(this aweXpect.Core.IThat subject, int expected) { } + public static aweXpect.Results.HasItemObjectResult HasItem(this aweXpect.Core.IThat source, object? expected) { } + public static aweXpect.Results.HasItemResult HasItem(this aweXpect.Core.IThat source, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } + public static aweXpect.Results.HasItemObjectResult?, TItem> HasItem(this aweXpect.Core.IThat?> source, TItem expected) { } + public static aweXpect.Results.HasItemResult?> HasItem(this aweXpect.Core.IThat?> source, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") { } public static aweXpect.Results.SingleItemResult HasSingle(this aweXpect.Core.IThat source) { } public static aweXpect.Results.SingleItemResult, TItem> HasSingle(this aweXpect.Core.IThat?> source) { } public static aweXpect.Results.StringCollectionBeContainedInResult, aweXpect.Core.IThat?>> IsContainedIn(this aweXpect.Core.IThat?> source, System.Collections.Generic.IEnumerable expected, [System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string doNotPopulateThisValue = "") { } @@ -1213,6 +1217,17 @@ namespace aweXpect.Results aweXpect.Results.EventTriggerResult WithParameter(string expression, int? position, System.Func predicate); } } + public class HasItemObjectResult : aweXpect.Results.HasItemObjectResult> + { + public HasItemObjectResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat collection, aweXpect.Options.CollectionIndexOptions collectionIndexOptions, aweXpect.Options.ObjectEqualityOptions options) { } + } + public class HasItemObjectResult : aweXpect.Results.HasItemResult + where TSelf : aweXpect.Results.HasItemObjectResult + { + public HasItemObjectResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat collection, aweXpect.Options.CollectionIndexOptions collectionIndexOptions, aweXpect.Options.ObjectEqualityOptions options) { } + public TSelf Equivalent(System.Func? optionsCallback = null) { } + public TSelf Using(System.Collections.Generic.IEqualityComparer comparer) { } + } public class ObjectCollectionBeContainedInResult : aweXpect.Results.ObjectCollectionMatchResult { public ObjectCollectionBeContainedInResult(aweXpect.Core.ExpectationBuilder expectationBuilder, TThat returnValue, aweXpect.Options.ObjectEqualityOptions options, aweXpect.Options.CollectionMatchOptions collectionMatchOptions) { } diff --git a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt index cfb0e40e9..d051fdc08 100644 --- a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt +++ b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt @@ -750,6 +750,14 @@ namespace aweXpect.Formatting } namespace aweXpect.Options { + public class CollectionIndexOptions + { + public CollectionIndexOptions() { } + public string GetDescription() { } + public bool HasOnlySingleIndex() { } + public bool? IsIndexInRange(int index) { } + public void SetIndexRange(int? minimum, int? maximum) { } + } public class CollectionMatchOptions { public CollectionMatchOptions(aweXpect.Options.CollectionMatchOptions.EquivalenceRelations equivalenceRelations = 1) { } @@ -1079,6 +1087,12 @@ namespace aweXpect.Results public TSelf WithCancellation(System.Threading.CancellationToken cancellationToken) { } public TSelf WithTimeout(System.TimeSpan timeout) { } } + public class HasItemResult + { + public HasItemResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat collection, aweXpect.Options.CollectionIndexOptions collectionIndexOptions) { } + public aweXpect.Results.AndOrResult> AtAnyIndex() { } + public aweXpect.Results.AndOrResult> AtIndex(int index) { } + } public class NullableNumberToleranceResult : aweXpect.Results.NullableNumberToleranceResult> where TType : struct, System.Numerics.INumber { diff --git a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt index af0fb21ba..09f752909 100644 --- a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt +++ b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt @@ -733,6 +733,14 @@ namespace aweXpect.Formatting } namespace aweXpect.Options { + public class CollectionIndexOptions + { + public CollectionIndexOptions() { } + public string GetDescription() { } + public bool HasOnlySingleIndex() { } + public bool? IsIndexInRange(int index) { } + public void SetIndexRange(int? minimum, int? maximum) { } + } public class CollectionMatchOptions { public CollectionMatchOptions(aweXpect.Options.CollectionMatchOptions.EquivalenceRelations equivalenceRelations = 1) { } @@ -1062,6 +1070,12 @@ namespace aweXpect.Results public TSelf WithCancellation(System.Threading.CancellationToken cancellationToken) { } public TSelf WithTimeout(System.TimeSpan timeout) { } } + public class HasItemResult + { + public HasItemResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat collection, aweXpect.Options.CollectionIndexOptions collectionIndexOptions) { } + public aweXpect.Results.AndOrResult> AtAnyIndex() { } + public aweXpect.Results.AndOrResult> AtIndex(int index) { } + } public class NullableNumberToleranceResult : aweXpect.Results.NullableNumberToleranceResult> where TType : struct, System.IComparable { diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.DoesNotHaveCount.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.DoesNotHaveCount.Tests.cs index 7c46354b8..0905c883f 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.DoesNotHaveCount.Tests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.DoesNotHaveCount.Tests.cs @@ -31,7 +31,7 @@ Expected that subject but could not verify, because it was already cancelled Collection: - [0, 1, 2, 3, 4, 5, (… and maybe others)] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, (… and maybe others)] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.EndsWith.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.EndsWith.Tests.cs index 21320281c..d95008b1f 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.EndsWith.Tests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.EndsWith.Tests.cs @@ -66,7 +66,7 @@ Expected that subject but it contained 2 at index 3 instead of 1 Collection: - [0, 0, 1, 2, 3, (… and maybe others)] + [0, 0, 1, 2, 3] """); } @@ -152,8 +152,7 @@ but it contained "bar" at index 1 instead of "FOO" [ "foo", "bar", - "baz", - (… and maybe others) + "baz" ] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.AtLeastTests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.AtLeastTests.cs index 0ebd13f33..2fdeb03d9 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.AtLeastTests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.AtLeastTests.cs @@ -31,7 +31,7 @@ Expected that subject but could not verify, because it was already cancelled Collection: - [0, 1, 2, 3, (… and maybe others)] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, (… and maybe others)] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.AtMostTests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.AtMostTests.cs index 3f40b9bca..4a092c2eb 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.AtMostTests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.AtMostTests.cs @@ -31,7 +31,7 @@ Expected that subject but could not verify, because it was already cancelled Collection: - [0, 1, 2, 3, 4, 5, (… and maybe others)] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, (… and maybe others)] """); } @@ -72,12 +72,7 @@ Expected that subject but found at least 3 Collection: - [ - 1, - 2, - 3, - (… and maybe others) - ] + [1, 2, 3] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.BetweenTests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.BetweenTests.cs index 4ec7d22c2..f8aa01145 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.BetweenTests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.BetweenTests.cs @@ -30,7 +30,7 @@ Expected that subject but could not verify, because it was already cancelled Collection: - [0, 1, 2, 3, 4, 5, (… and maybe others)] + [0, 1, 2, 3, 4, 5, 6, (… and maybe others)] """); } @@ -79,16 +79,7 @@ Expected that subject but found at least 7 Collection: - [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - (… and maybe others) - ] + [1, 2, 3, 4, 5, 6, 7] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.EqualToTests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.EqualToTests.cs index 94040c854..e9e7b9043 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.EqualToTests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.EqualToTests.cs @@ -31,7 +31,7 @@ Expected that subject but could not verify, because it was already cancelled Collection: - [0, 1, 2, 3, 4, 5, (… and maybe others)] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, (… and maybe others)] """); } @@ -80,12 +80,7 @@ Expected that subject but found at least 3 Collection: - [ - 1, - 2, - 3, - (… and maybe others) - ] + [1, 2, 3] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.LessThanTests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.LessThanTests.cs index 2ab0a9a59..1f8960b75 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.LessThanTests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.LessThanTests.cs @@ -31,7 +31,7 @@ Expected that subject but could not verify, because it was already cancelled Collection: - [0, 1, 2, 3, 4, 5, (… and maybe others)] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, (… and maybe others)] """); } @@ -50,12 +50,7 @@ Expected that subject but found at least 3 Collection: - [ - 1, - 2, - 3, - (… and maybe others) - ] + [1, 2, 3] """); } @@ -85,11 +80,7 @@ Expected that subject but found at least 2 Collection: - [ - 1, - 2, - (… and maybe others) - ] + [1, 2, 3] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.MoreThanTests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.MoreThanTests.cs index 0c70652c1..ee2ba0185 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.MoreThanTests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.MoreThanTests.cs @@ -31,7 +31,7 @@ Expected that subject but could not verify, because it was already cancelled Collection: - [0, 1, 2, 3, (… and maybe others)] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, (… and maybe others)] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.Tests.cs index e741f6314..1653e26b1 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.Tests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasCount.Tests.cs @@ -31,7 +31,7 @@ Expected that subject but could not verify, because it was already cancelled Collection: - [0, 1, 2, 3, 4, 5, (… and maybe others)] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, (… and maybe others)] """); } @@ -80,12 +80,7 @@ Expected that subject but found at least 3 Collection: - [ - 1, - 2, - 3, - (… and maybe others) - ] + [1, 2, 3] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasItem.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasItem.Tests.cs new file mode 100644 index 000000000..1caf3d44b --- /dev/null +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasItem.Tests.cs @@ -0,0 +1,445 @@ +#if NET8_0_OR_GREATER +using System.Collections.Generic; +using System.Linq; + +// ReSharper disable PossibleMultipleEnumeration + +namespace aweXpect.Tests; + +public sealed partial class ThatAsyncEnumerable +{ + public sealed class HasItem + { + public sealed class PredicateTests + { + [Fact] + public async Task DoesNotEnumerateTwice() + { + ThrowWhenIteratingTwiceAsyncEnumerable subject = new(); + + async Task Act() + => await That(subject).HasItem(_ => true).AtAnyIndex() + .And.HasItem(_ => true).AtIndex(0); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task DoesNotMaterializeEnumerable() + { + IAsyncEnumerable subject = Factory.GetAsyncFibonacciNumbers(); + + async Task Act() + => await That(subject).HasItem(a => a == 5).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsDifferentItemAtGivenIndex_ShouldSucceed() + { + IAsyncEnumerable subject = ToAsyncEnumerable(0, 1, 2); + + async Task Act() + => await That(subject).HasItem(_ => false).AtIndex(2); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => false at index 2, + but it had item 2 at index 2 + + Collection: + [0, 1, 2] + """); + } + + [Fact] + public async Task WhenEnumerableContainsExpectedItemAtGivenIndex_ShouldSucceed() + { + IAsyncEnumerable subject = ToAsyncEnumerable(0, 1, 2); + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(2); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsNoItemAtGivenIndex_ShouldSucceed() + { + IAsyncEnumerable subject = ToAsyncEnumerable(0, 1, 2); + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(3); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true at index 3, + but it did not contain any item at index 3 + + Collection: + [0, 1, 2] + """); + } + + [Fact] + public async Task WhenEnumerableIsEmpty_ShouldFail() + { + IAsyncEnumerable subject = ToAsyncEnumerable(Array.Empty()); + + async Task Act() + => await That(subject).HasItem(_ => true).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true, + but it did not contain any item + + Collection: + [] + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithAnyIndex_ShouldFail() + { + IAsyncEnumerable? subject = null; + + async Task Act() + => await That(subject).HasItem(_ => true).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true, + but it was + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithFixedIndex_ShouldFail() + { + IAsyncEnumerable? subject = null; + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(0); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true at index 0, + but it was + """); + } + + [Fact] + public async Task WithMultipleFailures_ShouldIncludeCollectionOnlyOnce() + { + IAsyncEnumerable subject = ToAsyncEnumerable(["a", "b", "c",]); + + async Task Act() + => await That(subject).HasItem(_ => false).AtIndex(0).And.HasItem(_ => false).AtIndex(1).And + .HasItem(_ => false) + .AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => false at index 0 and has item _ => false at index 1 and has item _ => false, + but it had item "a" at index 0 and it had item "b" at index 1 and it did not match at any index + + Collection: + [ + "a", + "b", + "c" + ] + """); + } + } + + public sealed class ItemTests + { + [Fact] + public async Task DoesNotEnumerateTwice() + { + ThrowWhenIteratingTwiceAsyncEnumerable subject = new(); + + async Task Act() + => await That(subject).HasItem(1).AtAnyIndex() + .And.HasItem(1).AtIndex(0); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task DoesNotMaterializeEnumerable() + { + IAsyncEnumerable subject = Factory.GetAsyncFibonacciNumbers(); + + async Task Act() + => await That(subject).HasItem(5).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsDifferentItemAtGivenIndex_ShouldSucceed() + { + IAsyncEnumerable subject = ToAsyncEnumerable(0, 1, 2, 3, 4, 5); + + async Task Act() + => await That(subject).HasItem(3).AtIndex(2); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 3 at index 2, + but it had item 2 at index 2 + + Collection: + [0, 1, 2, 3, 4, 5] + """); + } + + [Fact] + public async Task WhenEnumerableContainsExpectedItemAtGivenIndex_ShouldSucceed() + { + IAsyncEnumerable subject = ToAsyncEnumerable(0, 1, 2, 3, 4, 5); + + async Task Act() + => await That(subject).HasItem(2).AtIndex(2); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsNoItemAtGivenIndex_ShouldSucceed() + { + IAsyncEnumerable subject = ToAsyncEnumerable(0, 1, 2); + + async Task Act() + => await That(subject).HasItem(5).AtIndex(3); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 5 at index 3, + but it did not contain any item at index 3 + + Collection: + [0, 1, 2] + """); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableIsEmpty_ShouldFail(int expected) + { + IAsyncEnumerable subject = ToAsyncEnumerable(Array.Empty()); + + async Task Act() + => await That(subject).HasItem(expected).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item {expected}, + but it did not contain any item + + Collection: + [] + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithAnyIndex_ShouldFail() + { + int expected = 42; + IAsyncEnumerable? subject = null; + + async Task Act() + => await That(subject).HasItem(expected).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 42, + but it was + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithFixedIndex_ShouldFail() + { + int expected = 42; + IAsyncEnumerable? subject = null; + + async Task Act() + => await That(subject).HasItem(expected).AtIndex(0); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 42 at index 0, + but it was + """); + } + + [Fact] + public async Task WithMultipleFailures_ShouldIncludeCollectionOnlyOnce() + { + IAsyncEnumerable subject = ToAsyncEnumerable(["a", "b", "c",]); + + async Task Act() + => await That(subject).HasItem("d").AtIndex(0).And.HasItem("e").AtIndex(1).And.HasItem("f") + .AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item "d" at index 0 and has item "e" at index 1 and has item "f", + but it had item "a" at index 0 and it had item "b" at index 1 and it did not match at any index + + Collection: + [ + "a", + "b", + "c" + ] + """); + } + } + + public sealed class EquivalentTests + { + [Fact] + public async Task WhenEquivalentItemIsFound_ShouldSucceed() + { + IAsyncEnumerable subject = + ToAsyncEnumerable(Factory.GetFibonacciNumbers(20).Select(x => new MyClass(x)).ToArray()); + MyClass expected = new(5); + + async Task Act() + => await That(subject).HasItem(expected).Equivalent().AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEquivalentItemIsNotFound_ShouldFail() + { + IAsyncEnumerable subject = + ToAsyncEnumerable(Factory.GetFibonacciNumbers(20).Select(x => new MyClass(x)).ToArray()); + MyClass expected = new(4); + + async Task Act() + => await That(subject).HasItem(expected).Equivalent().AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item MyClass { + StringValue = "", + Value = 4 + } equivalent, + but it did not match at any index + + Collection: + [ + MyClass { + StringValue = "", + Value = 1 + }, + MyClass { + StringValue = "", + Value = 1 + }, + MyClass { + StringValue = "", + Value = 2 + }, + MyClass { + StringValue = "", + Value = 3 + }, + MyClass { + StringValue = "", + Value = 5 + }, + MyClass { + StringValue = "", + Value = 8 + }, + MyClass { + StringValue = "", + Value = 13 + }, + MyClass { + StringValue = "", + Value = 21 + }, + MyClass { + StringValue = "", + Value = 34 + }, + MyClass { + StringValue = "", + Value = 55 + }, + … + ] + """); + } + } + + public sealed class UsingTests + { + [Fact] + public async Task WithAllDifferentComparer_ShouldFail() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20); + + async Task Act() + => await That(subject).HasItem(1).Using(new AllDifferentComparer()).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 1 using AllDifferentComparer, + but it did not match at any index + + Collection: + [ + 1, + 1, + 2, + 3, + 5, + 8, + 13, + 21, + 34, + 55, + … + ] + """); + } + + [Fact] + public async Task WithAllEqualComparer_ShouldSucceed() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20); + + async Task Act() + => await That(subject).HasItem(4).Using(new AllEqualComparer()).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + } + } +} +#endif diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasSingle.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasSingle.Tests.cs index 072575ba9..db0df7470 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasSingle.Tests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.HasSingle.Tests.cs @@ -36,10 +36,7 @@ Expected that subject but it contained more than one item Collection: - [ - 1, - 2 - ] + [1, 2, 3] """); } @@ -107,7 +104,14 @@ but it contained more than one item 1, 1, 2, - 3 + 3, + 5, + 8, + 13, + 21, + 34, + 55, + … ] """); } @@ -137,11 +141,7 @@ Expected that subject but it contained more than one item Collection: - [ - 1, - 2, - 3 - ] + [1, 2, 3] """); } @@ -443,10 +443,7 @@ Expected that subject but it contained more than one item Collection: - [ - 1, - 2 - ] + [1, 2, 3] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.IsEqualTo.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.IsEqualTo.Tests.cs index e739301a7..b9a7562f2 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.IsEqualTo.Tests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.IsEqualTo.Tests.cs @@ -38,7 +38,7 @@ but it had more than 20 deviations 8, 9, 10, - (… and maybe others) + … ] Expected: @@ -602,7 +602,7 @@ but it had more than 20 deviations 8, 9, 10, - (… and maybe others) + … ] Expected: @@ -1077,7 +1077,7 @@ but it had more than 20 deviations 8, 9, 10, - (… and maybe others) + … ] Expected: @@ -1604,7 +1604,7 @@ but it had more than 20 deviations 8, 9, 10, - (… and maybe others) + … ] Expected: diff --git a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.StartsWith.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.StartsWith.Tests.cs index d19c2c04a..a6a411b5a 100644 --- a/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.StartsWith.Tests.cs +++ b/Tests/aweXpect.Tests/Collections/ThatAsyncEnumerable.StartsWith.Tests.cs @@ -77,11 +77,7 @@ Expected that subject but it contained 2 at index 1 instead of 3 Collection: - [ - 1, - 2, - (… and maybe others) - ] + [1, 2, 3] """); } @@ -166,7 +162,7 @@ but it contained "bar" at index 1 instead of "BAZ" [ "foo", "bar", - (… and maybe others) + "baz" ] """); } diff --git a/Tests/aweXpect.Tests/Collections/ThatEnumerable.HasItem.EnumerableTests.cs b/Tests/aweXpect.Tests/Collections/ThatEnumerable.HasItem.EnumerableTests.cs new file mode 100644 index 000000000..21083ad65 --- /dev/null +++ b/Tests/aweXpect.Tests/Collections/ThatEnumerable.HasItem.EnumerableTests.cs @@ -0,0 +1,453 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +// ReSharper disable PossibleMultipleEnumeration + +namespace aweXpect.Tests; + +public sealed partial class ThatEnumerable +{ + public sealed partial class HasItem + { + public sealed class EnumerablePredicateTests + { + [Fact] + public async Task DoesNotEnumerateTwice() + { + IEnumerable subject = new ThrowWhenIteratingTwiceEnumerable(); + + async Task Act() + => await That(subject).HasItem(_ => true).AtAnyIndex() + .And.HasItem(_ => true).AtIndex(0); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task DoesNotMaterializeEnumerable() + { + IEnumerable subject = Factory.GetFibonacciNumbers(); + + async Task Act() + => await That(subject).HasItem(a => 5.Equals(a)).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsDifferentItemAtGivenIndex_ShouldSucceed() + { + IEnumerable subject = new []{0, 1, 2,}; + + async Task Act() + => await That(subject).HasItem(_ => false).AtIndex(2); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item _ => false at index 2, + but it had item 2 at index 2 + + Collection: + {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEnumerableContainsExpectedItemAtGivenIndex_ShouldSucceed() + { + IEnumerable subject = new []{0, 1, 2,}; + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(2); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsNoItemAtGivenIndex_ShouldSucceed() + { + IEnumerable subject = new []{0, 1, 2,}; + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(3); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item _ => true at index 3, + but it did not contain any item at index 3 + + Collection: + {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEnumerableIsEmpty_ShouldFail() + { + IEnumerable subject = Array.Empty(); + + async Task Act() + => await That(subject).HasItem(_ => true).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true, + but it did not contain any item + + Collection: + [] + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithAnyIndex_ShouldFail() + { + IEnumerable? subject = null; + + async Task Act() + => await That(subject!).HasItem(_ => true).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true, + but it was + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithFixedIndex_ShouldFail() + { + IEnumerable? subject = null; + + async Task Act() + => await That(subject!).HasItem(_ => true).AtIndex(0); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true at index 0, + but it was + """); + } + + [Fact] + public async Task WithMultipleFailures_ShouldIncludeCollectionOnlyOnce() + { + IEnumerable subject = ToEnumerable(["a", "b", "c",]); + + async Task Act() + => await That(subject).HasItem(_ => false).AtIndex(0).And.HasItem(_ => false).AtIndex(1).And + .HasItem(_ => false) + .AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => false at index 0 and has item _ => false at index 1 and has item _ => false, + but it had item "a" at index 0 and it had item "b" at index 1 and it did not match at any index + + Collection: + [ + "a", + "b", + "c" + ] + """); + } + } + + public sealed class EnumerableItemTests + { + [Fact] + public async Task DoesNotEnumerateTwice() + { + IEnumerable subject = new ThrowWhenIteratingTwiceEnumerable(); + + async Task Act() + => await That(subject).HasItem(1).AtAnyIndex() + .And.HasItem(1).AtIndex(0); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task DoesNotMaterializeEnumerable() + { + IEnumerable subject = Factory.GetFibonacciNumbers(); + + async Task Act() + => await That(subject).HasItem(5).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableContainsDifferentItemAtGivenIndex_ShouldSucceed( + List values, int expected) + { + values.Add(0); + values.Add(1); + values.Insert(2, expected); + IEnumerable subject = values; + + async Task Act() + => await That(subject).HasItem(expected - 1).AtIndex(2); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item {expected - 1} at index 2, + but it had item {expected} at index 2 + + Collection: + {Formatter.Format(values)} + """); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableContainsExpectedItemAtGivenIndex_ShouldSucceed( + List values, int expected) + { + values.Add(0); + values.Add(1); + values.Insert(2, expected); + IEnumerable subject = values; + + async Task Act() + => await That(subject).HasItem(expected).AtIndex(2); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableContainsNoItemAtGivenIndex_ShouldSucceed(int expected) + { + IEnumerable subject = new []{0, 1, expected,}; + + async Task Act() + => await That(subject).HasItem(expected).AtIndex(3); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item {expected} at index 3, + but it did not contain any item at index 3 + + Collection: + {Formatter.Format(subject)} + """); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableIsEmpty_ShouldFail(int expected) + { + IEnumerable subject = Array.Empty(); + + async Task Act() + => await That(subject).HasItem(expected).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item {expected}, + but it did not contain any item + + Collection: + [] + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithAnyIndex_ShouldFail() + { + int expected = 42; + IEnumerable? subject = null; + + async Task Act() + => await That(subject!).HasItem(expected).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 42, + but it was + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithFixedIndex_ShouldFail() + { + int expected = 42; + IEnumerable? subject = null; + + async Task Act() + => await That(subject!).HasItem(expected).AtIndex(0); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 42 at index 0, + but it was + """); + } + + [Fact] + public async Task WithMultipleFailures_ShouldIncludeCollectionOnlyOnce() + { + IEnumerable subject = ToEnumerable(["a", "b", "c",]); + + async Task Act() + => await That(subject).HasItem("d").AtIndex(0).And.HasItem("e").AtIndex(1).And.HasItem("f") + .AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item "d" at index 0 and has item "e" at index 1 and has item "f", + but it had item "a" at index 0 and it had item "b" at index 1 and it did not match at any index + + Collection: + [ + "a", + "b", + "c" + ] + """); + } + } + + public sealed class EnumerableEquivalentTests + { + [Fact] + public async Task WhenEquivalentItemIsFound_ShouldSucceed() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20).Select(x => new MyClass(x)); + MyClass expected = new(5); + + async Task Act() + => await That(subject).HasItem(expected).Equivalent().AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEquivalentItemIsNotFound_ShouldFail() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20).Select(x => new MyClass(x)); + MyClass expected = new(4); + + async Task Act() + => await That(subject).HasItem(expected).Equivalent().AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item MyClass { + StringValue = "", + Value = 4 + } equivalent, + but it did not match at any index + + Collection: + [ + MyClass { + StringValue = "", + Value = 1 + }, + MyClass { + StringValue = "", + Value = 1 + }, + MyClass { + StringValue = "", + Value = 2 + }, + MyClass { + StringValue = "", + Value = 3 + }, + MyClass { + StringValue = "", + Value = 5 + }, + MyClass { + StringValue = "", + Value = 8 + }, + MyClass { + StringValue = "", + Value = 13 + }, + MyClass { + StringValue = "", + Value = 21 + }, + MyClass { + StringValue = "", + Value = 34 + }, + MyClass { + StringValue = "", + Value = 55 + }, + … + ] + """); + } + } + + public sealed class EnumerableUsingTests + { + [Fact] + public async Task WithAllDifferentComparer_ShouldFail() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20); + + async Task Act() + => await That(subject).HasItem(1).Using(new AllDifferentComparer()).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 1 using AllDifferentComparer, + but it did not match at any index + + Collection: + [ + 1, + 1, + 2, + 3, + 5, + 8, + 13, + 21, + 34, + 55, + … + ] + """); + } + + [Fact] + public async Task WithAllEqualComparer_ShouldSucceed() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20); + + async Task Act() + => await That(subject).HasItem(4).Using(new AllEqualComparer()).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/Tests/aweXpect.Tests/Collections/ThatEnumerable.HasItem.ImmutableTests.cs b/Tests/aweXpect.Tests/Collections/ThatEnumerable.HasItem.ImmutableTests.cs new file mode 100644 index 000000000..9a86ba174 --- /dev/null +++ b/Tests/aweXpect.Tests/Collections/ThatEnumerable.HasItem.ImmutableTests.cs @@ -0,0 +1,331 @@ +#if NET8_0_OR_GREATER +using System.Collections.Immutable; +using System.Linq; + +// ReSharper disable PossibleMultipleEnumeration + +namespace aweXpect.Tests; + +public sealed partial class ThatEnumerable +{ + public sealed partial class HasItem + { + public sealed class ImmutablePredicateTests + { + [Fact] + public async Task WhenEnumerableContainsDifferentItemAtGivenIndex_ShouldSucceed() + { + ImmutableArray subject = [0, 1, 2,]; + + async Task Act() + => await That(subject).HasItem(_ => false).AtIndex(2); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item _ => false at index 2, + but it had item 2 at index 2 + + Collection: + {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEnumerableContainsExpectedItemAtGivenIndex_ShouldSucceed() + { + ImmutableArray subject = [0, 1, 2,]; + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(2); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsNoItemAtGivenIndex_ShouldSucceed() + { + ImmutableArray subject = [0, 1, 2,]; + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(3); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item _ => true at index 3, + but it did not contain any item at index 3 + + Collection: + {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEnumerableIsEmpty_ShouldFail() + { + ImmutableArray subject = []; + + async Task Act() + => await That(subject).HasItem(_ => true).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true, + but it did not contain any item + + Collection: + [] + """); + } + + [Fact] + public async Task WithMultipleFailures_ShouldIncludeCollectionOnlyOnce() + { + ImmutableArray subject = ["a", "b", "c",]; + + async Task Act() + => await That(subject).HasItem(_ => false).AtIndex(0).And.HasItem(_ => false).AtIndex(1).And + .HasItem(_ => false) + .AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => false at index 0 and has item _ => false at index 1 and has item _ => false, + but it had item "a" at index 0 and it had item "b" at index 1 and it did not match at any index + + Collection: + [ + "a", + "b", + "c" + ] + """); + } + } + + public sealed class ImmutableItemTests + { + [Fact] + public async Task WhenEnumerableContainsDifferentItemAtGivenIndex_ShouldSucceed() + { + ImmutableArray subject = [0, 1, 2, 3, 4, 5,]; + + async Task Act() + => await That(subject).HasItem(3).AtIndex(2); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 3 at index 2, + but it had item 2 at index 2 + + Collection: + [0, 1, 2, 3, 4, 5] + """); + } + + [Fact] + public async Task WhenEnumerableContainsExpectedItemAtGivenIndex_ShouldSucceed() + { + ImmutableArray subject = [0, 1, 2, 3, 4, 5,]; + + async Task Act() + => await That(subject).HasItem(2).AtIndex(2); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsNoItemAtGivenIndex_ShouldSucceed() + { + ImmutableArray subject = [0, 1, 2,]; + + async Task Act() + => await That(subject).HasItem(2).AtIndex(3); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 2 at index 3, + but it did not contain any item at index 3 + + Collection: + [0, 1, 2] + """); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableIsEmpty_ShouldFail(int expected) + { + ImmutableArray subject = []; + + async Task Act() + => await That(subject).HasItem(expected).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item {expected}, + but it did not contain any item + + Collection: + [] + """); + } + + [Fact] + public async Task WithMultipleFailures_ShouldIncludeCollectionOnlyOnce() + { + ImmutableArray subject = ["a", "b", "c",]; + + async Task Act() + => await That(subject).HasItem("d").AtIndex(0).And.HasItem("e").AtIndex(1).And.HasItem("f") + .AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item "d" at index 0 and has item "e" at index 1 and has item "f", + but it had item "a" at index 0 and it had item "b" at index 1 and it did not match at any index + + Collection: + [ + "a", + "b", + "c" + ] + """); + } + } + + public sealed class ImmutableEquivalentTests + { + [Fact] + public async Task WhenEquivalentItemIsFound_ShouldSucceed() + { + ImmutableArray subject = [..Factory.GetFibonacciNumbers(20).Select(x => new MyClass(x)),]; + MyClass expected = new(5); + + async Task Act() + => await That(subject).HasItem(expected).Equivalent().AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEquivalentItemIsNotFound_ShouldFail() + { + ImmutableArray subject = [..Factory.GetFibonacciNumbers(20).Select(x => new MyClass(x)),]; + MyClass expected = new(4); + + async Task Act() + => await That(subject).HasItem(expected).Equivalent().AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item MyClass { + StringValue = "", + Value = 4 + } equivalent, + but it did not match at any index + + Collection: + [ + MyClass { + StringValue = "", + Value = 1 + }, + MyClass { + StringValue = "", + Value = 1 + }, + MyClass { + StringValue = "", + Value = 2 + }, + MyClass { + StringValue = "", + Value = 3 + }, + MyClass { + StringValue = "", + Value = 5 + }, + MyClass { + StringValue = "", + Value = 8 + }, + MyClass { + StringValue = "", + Value = 13 + }, + MyClass { + StringValue = "", + Value = 21 + }, + MyClass { + StringValue = "", + Value = 34 + }, + MyClass { + StringValue = "", + Value = 55 + }, + … + ] + """); + } + } + + public sealed class ImmutableUsingTests + { + [Fact] + public async Task WithAllDifferentComparer_ShouldFail() + { + ImmutableArray subject = [..Factory.GetFibonacciNumbers(20),]; + + async Task Act() + => await That(subject).HasItem(1).Using(new AllDifferentComparer()).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 1 using AllDifferentComparer, + but it did not match at any index + + Collection: + [ + 1, + 1, + 2, + 3, + 5, + 8, + 13, + 21, + 34, + 55, + … + ] + """); + } + + [Fact] + public async Task WithAllEqualComparer_ShouldSucceed() + { + ImmutableArray subject = [..Factory.GetFibonacciNumbers(20),]; + + async Task Act() + => await That(subject).HasItem(4).Using(new AllEqualComparer()).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + } + } +} +#endif diff --git a/Tests/aweXpect.Tests/Collections/ThatEnumerable.HasItem.Tests.cs b/Tests/aweXpect.Tests/Collections/ThatEnumerable.HasItem.Tests.cs new file mode 100644 index 000000000..ce9e5750b --- /dev/null +++ b/Tests/aweXpect.Tests/Collections/ThatEnumerable.HasItem.Tests.cs @@ -0,0 +1,460 @@ +using System.Collections.Generic; +using System.Linq; + +// ReSharper disable PossibleMultipleEnumeration + +namespace aweXpect.Tests; + +public sealed partial class ThatEnumerable +{ + public sealed partial class HasItem + { + public sealed class PredicateTests + { + [Fact] + public async Task DoesNotEnumerateTwice() + { + ThrowWhenIteratingTwiceEnumerable subject = new(); + + async Task Act() + => await That(subject).HasItem(_ => true).AtAnyIndex() + .And.HasItem(_ => true).AtIndex(0); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task DoesNotMaterializeEnumerable() + { + IEnumerable subject = Factory.GetFibonacciNumbers(); + + async Task Act() + => await That(subject).HasItem(a => a == 5).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsDifferentItemAtGivenIndex_ShouldSucceed() + { + int[] subject = [0, 1, 2,]; + + async Task Act() + => await That(subject).HasItem(_ => false).AtIndex(2); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item _ => false at index 2, + but it had item 2 at index 2 + + Collection: + {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEnumerableContainsExpectedItemAtGivenIndex_ShouldSucceed() + { + int[] subject = [0, 1, 2,]; + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(2); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEnumerableContainsNoItemAtGivenIndex_ShouldSucceed() + { + List subject = + [ + 0, + 1, + 2, + ]; + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(3); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item _ => true at index 3, + but it did not contain any item at index 3 + + Collection: + {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEnumerableIsEmpty_ShouldFail() + { + List subject = []; + + async Task Act() + => await That(subject).HasItem(_ => true).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true, + but it did not contain any item + + Collection: + [] + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithAnyIndex_ShouldFail() + { + IEnumerable? subject = null; + + async Task Act() + => await That(subject).HasItem(_ => true).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true, + but it was + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithFixedIndex_ShouldFail() + { + IEnumerable? subject = null; + + async Task Act() + => await That(subject).HasItem(_ => true).AtIndex(0); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => true at index 0, + but it was + """); + } + + [Fact] + public async Task WithMultipleFailures_ShouldIncludeCollectionOnlyOnce() + { + IEnumerable subject = ToEnumerable(["a", "b", "c",]); + + async Task Act() + => await That(subject).HasItem(_ => false).AtIndex(0).And.HasItem(_ => false).AtIndex(1).And + .HasItem(_ => false) + .AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item _ => false at index 0 and has item _ => false at index 1 and has item _ => false, + but it had item "a" at index 0 and it had item "b" at index 1 and it did not match at any index + + Collection: + [ + "a", + "b", + "c" + ] + """); + } + } + + public sealed class ItemTests + { + [Fact] + public async Task DoesNotEnumerateTwice() + { + ThrowWhenIteratingTwiceEnumerable subject = new(); + + async Task Act() + => await That(subject).HasItem(1).AtAnyIndex() + .And.HasItem(1).AtIndex(0); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task DoesNotMaterializeEnumerable() + { + IEnumerable subject = Factory.GetFibonacciNumbers(); + + async Task Act() + => await That(subject).HasItem(5).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableContainsDifferentItemAtGivenIndex_ShouldSucceed( + List subject, int expected) + { + subject.Add(0); + subject.Add(1); + subject.Insert(2, expected); + + async Task Act() + => await That(subject).HasItem(expected - 1).AtIndex(2); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item {expected - 1} at index 2, + but it had item {expected} at index 2 + + Collection: + {Formatter.Format(subject)} + """); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableContainsExpectedItemAtGivenIndex_ShouldSucceed( + List subject, int expected) + { + subject.Add(0); + subject.Add(1); + subject.Insert(2, expected); + + async Task Act() + => await That(subject).HasItem(expected).AtIndex(2); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableContainsNoItemAtGivenIndex_ShouldSucceed(int expected) + { + List subject = + [ + 0, + 1, + expected, + ]; + + async Task Act() + => await That(subject).HasItem(expected).AtIndex(3); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item {expected} at index 3, + but it did not contain any item at index 3 + + Collection: + {Formatter.Format(subject)} + """); + } + + [Theory] + [AutoData] + public async Task WhenEnumerableIsEmpty_ShouldFail(int expected) + { + List subject = []; + + async Task Act() + => await That(subject).HasItem(expected).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has item {expected}, + but it did not contain any item + + Collection: + [] + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithAnyIndex_ShouldFail() + { + int expected = 42; + IEnumerable? subject = null; + + async Task Act() + => await That(subject).HasItem(expected).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 42, + but it was + """); + } + + [Fact] + public async Task WhenSubjectIsNull_WithFixedIndex_ShouldFail() + { + int expected = 42; + IEnumerable? subject = null; + + async Task Act() + => await That(subject).HasItem(expected).AtIndex(0); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 42 at index 0, + but it was + """); + } + + [Fact] + public async Task WithMultipleFailures_ShouldIncludeCollectionOnlyOnce() + { + IEnumerable subject = ToEnumerable(["a", "b", "c",]); + + async Task Act() + => await That(subject).HasItem("d").AtIndex(0).And.HasItem("e").AtIndex(1).And.HasItem("f") + .AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item "d" at index 0 and has item "e" at index 1 and has item "f", + but it had item "a" at index 0 and it had item "b" at index 1 and it did not match at any index + + Collection: + [ + "a", + "b", + "c" + ] + """); + } + } + + public sealed class EquivalentTests + { + [Fact] + public async Task WhenEquivalentItemIsFound_ShouldSucceed() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20).Select(x => new MyClass(x)); + MyClass expected = new(5); + + async Task Act() + => await That(subject).HasItem(expected).Equivalent().AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEquivalentItemIsNotFound_ShouldFail() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20).Select(x => new MyClass(x)); + MyClass expected = new(4); + + async Task Act() + => await That(subject).HasItem(expected).Equivalent().AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item MyClass { + StringValue = "", + Value = 4 + } equivalent, + but it did not match at any index + + Collection: + [ + MyClass { + StringValue = "", + Value = 1 + }, + MyClass { + StringValue = "", + Value = 1 + }, + MyClass { + StringValue = "", + Value = 2 + }, + MyClass { + StringValue = "", + Value = 3 + }, + MyClass { + StringValue = "", + Value = 5 + }, + MyClass { + StringValue = "", + Value = 8 + }, + MyClass { + StringValue = "", + Value = 13 + }, + MyClass { + StringValue = "", + Value = 21 + }, + MyClass { + StringValue = "", + Value = 34 + }, + MyClass { + StringValue = "", + Value = 55 + }, + … + ] + """); + } + } + + public sealed class UsingTests + { + [Fact] + public async Task WithAllDifferentComparer_ShouldFail() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20); + + async Task Act() + => await That(subject).HasItem(1).Using(new AllDifferentComparer()).AtAnyIndex(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + has item 1 using AllDifferentComparer, + but it did not match at any index + + Collection: + [ + 1, + 1, + 2, + 3, + 5, + 8, + 13, + 21, + 34, + 55, + … + ] + """); + } + + [Fact] + public async Task WithAllEqualComparer_ShouldSucceed() + { + IEnumerable subject = Factory.GetFibonacciNumbers(20); + + async Task Act() + => await That(subject).HasItem(4).Using(new AllEqualComparer()).AtAnyIndex(); + + await That(Act).DoesNotThrow(); + } + } + } +}