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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Docs/pages/docs/expectations/07-collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -487,6 +487,21 @@ await Expect.That(result).IsGreaterThan(41);

*Note: The same expectation works also for `IAsyncEnumerable<T>`.*


## 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<string> 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<T>`.*


## Dictionaries

### Contain key(s)
Expand Down
2 changes: 1 addition & 1 deletion Pipeline/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ partial class Build : NukeBuild
/// <para />
/// Afterward, you can update the package reference in `Directory.Packages.props` and reset this flag.
/// </summary>
readonly BuildScope BuildScope = BuildScope.Default;
readonly BuildScope BuildScope = BuildScope.CoreOnly;

[Parameter("Github Token")] readonly string GithubToken;

Expand Down
66 changes: 66 additions & 0 deletions Source/aweXpect.Core/Options/CollectionIndexOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace aweXpect.Options;

/// <summary>
/// Options for limitations on a collection index.
/// </summary>
public class CollectionIndexOptions
{
private int? _maximum;
private int? _minimum;

/// <summary>
/// Checks if the <paramref name="index" /> is in range.
/// </summary>
/// <returns>
/// <see langword="true" />, if the <paramref name="index" /> is in range, <see langword="null" />,
/// if the <paramref name="index" /> is not in range, but could be in range for a larger index,
/// otherwise <see langword="false" /> when the <paramref name="index" /> is not in range
/// and will also not be in range for larger values.
/// </returns>
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;
}

/// <summary>
/// Flag indicating, if only a single index is considered in range.
/// </summary>
public bool HasOnlySingleIndex()
=> _maximum == _minimum && _minimum is not null;

/// <summary>
/// Set the checked index to be in range between <paramref name="minimum" /> and <paramref name="maximum" />.
/// </summary>
/// <remarks>When either parameter is set to <see langword="null" />, the corresponding range direction is unlimited.</remarks>
public void SetIndexRange(int? minimum, int? maximum)
{
_minimum = minimum;
_maximum = maximum;
}

/// <summary>
/// Returns the description of the <see cref="CollectionIndexOptions" />.
/// </summary>
public string GetDescription()
{
if (_minimum is null && _maximum is null)
{
return "";
}

return _minimum == _maximum
? $" at index {_minimum}"
: $" with index between {_minimum} and {_maximum}";
}
}
34 changes: 34 additions & 0 deletions Source/aweXpect.Core/Results/HasItemResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using aweXpect.Core;
using aweXpect.Options;

namespace aweXpect.Results;

/// <summary>
/// The result for verifying that a collection has a matching item at a given index.
/// </summary>
/// <remarks>
/// <seealso cref="ExpectationResult{TType,TSelf}" />
/// </remarks>
public class HasItemResult<TCollection>(
ExpectationBuilder expectationBuilder,
IThat<TCollection> collection,
CollectionIndexOptions collectionIndexOptions)
{
/// <summary>
/// …at any index.
/// </summary>
public AndOrResult<TCollection, IThat<TCollection>> AtAnyIndex()
{
collectionIndexOptions.SetIndexRange(null, null);
return new AndOrResult<TCollection, IThat<TCollection>>(expectationBuilder, collection);
}

/// <summary>
/// …at the given <paramref name="index" />.
/// </summary>
public AndOrResult<TCollection, IThat<TCollection>> AtIndex(int index)
{
collectionIndexOptions.SetIndexRange(index, index);
return new AndOrResult<TCollection, IThat<TCollection>>(expectationBuilder, collection);
}
}
2 changes: 2 additions & 0 deletions Source/aweXpect/Helpers/IMaterializedEnumerable.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#if NET8_0_OR_GREATER
using System.Collections.Generic;
using System.Threading.Tasks;

namespace aweXpect.Helpers;

internal interface IMaterializedEnumerable<out T> : ICountable
{
IReadOnlyList<T> MaterializedItems { get; }
Task MaterializeItems(int minimumNumberOfItems);
}
#endif
21 changes: 19 additions & 2 deletions Source/aweXpect/Helpers/MaterializingAsyncEnumerable.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#if NET8_0_OR_GREATER
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace aweXpect.Helpers;

Expand All @@ -26,13 +27,14 @@ public async IAsyncEnumerator<T> 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;
}

Expand All @@ -47,6 +49,21 @@ public async IAsyncEnumerator<T> GetAsyncEnumerator(
/// <inheritdoc cref="IMaterializedEnumerable{T}.MaterializedItems" />
IReadOnlyList<T> IMaterializedEnumerable<T>.MaterializedItems => _materializedItems;

/// <inheritdoc cref="IMaterializedEnumerable{T}.MaterializeItems(int)" />
public async Task MaterializeItems(int minimumNumberOfItems)
{
int index = 0;
await foreach (T _ in this)
{
if (index++ > minimumNumberOfItems)
{
return;
}
}

Count = _materializedItems.Count;
}

public static IAsyncEnumerable<T> Wrap(IAsyncEnumerable<T> enumerable)
{
if (enumerable is MaterializingAsyncEnumerable<T>)
Expand Down
59 changes: 59 additions & 0 deletions Source/aweXpect/Results/HasItemObjectResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using aweXpect.Core;
using aweXpect.Equivalency;
using aweXpect.Options;

namespace aweXpect.Results;

/// <summary>
/// The result for verifying that a collection has a specific item at a given index.
/// </summary>
/// <remarks>
/// <seealso cref="HasItemResult{TCollection}" />
/// </remarks>
public class HasItemObjectResult<TCollection, TItem>(
ExpectationBuilder expectationBuilder,
IThat<TCollection> collection,
CollectionIndexOptions collectionIndexOptions,
ObjectEqualityOptions<TItem> options)
: HasItemObjectResult<TCollection, TItem,
HasItemObjectResult<TCollection, TItem>>(
expectationBuilder,
collection,
collectionIndexOptions,
options);


/// <summary>
/// The result for verifying that a collection has a specific item at a given index.
/// </summary>
/// <remarks>
/// <seealso cref="HasItemResult{TCollection}" />
/// </remarks>
public class HasItemObjectResult<TCollection, TItem, TSelf>(
ExpectationBuilder expectationBuilder,
IThat<TCollection> collection,
CollectionIndexOptions collectionIndexOptions,
ObjectEqualityOptions<TItem> options)
: HasItemResult<TCollection>(expectationBuilder, collection, collectionIndexOptions)
where TSelf : HasItemObjectResult<TCollection, TItem, TSelf>
{
/// <summary>
/// Use equivalency to compare objects.
/// </summary>
public TSelf Equivalent(Func<EquivalencyOptions, EquivalencyOptions>? optionsCallback = null)
{
options.Equivalent(EquivalencyOptionsExtensions.FromCallback(optionsCallback));
return (TSelf)this;
}

/// <summary>
/// Uses the provided <paramref name="comparer" /> for comparing <see langword="object" />s.
/// </summary>
public TSelf Using(IEqualityComparer<object> comparer)
{
options.Using(comparer);
return (TSelf)this;
}
}
6 changes: 5 additions & 1 deletion Source/aweXpect/That/Collections/CollectionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -101,14 +103,16 @@ internal static ExpectationBuilder AddCollectionContext(this ExpectationBuilder
}

#if NET8_0_OR_GREATER
internal static ExpectationBuilder AddCollectionContext<TItem>(this ExpectationBuilder expectationBuilder,
internal static async Task<ExpectationBuilder> AddCollectionContext<TItem>(this ExpectationBuilder expectationBuilder,
IMaterializedEnumerable<TItem>? value, bool isIncomplete = false)
{
if (value is null)
{
return expectationBuilder;
}

await value.MaterializeItems(Customize.aweXpect.Formatting().MaximumNumberOfCollectionItems.Get());

return expectationBuilder.UpdateContexts(contexts
=>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ public async Task<ConstraintResult> IsMetBy(IAsyncEnumerable<TItem>? actual, IEv
if (_index + _offset < 0)
{
Outcome = Outcome.Failure;
_expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable<TItem>);
await _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable<TItem>);
return this;
}

Expand All @@ -276,8 +276,7 @@ public async Task<ConstraintResult> IsMetBy(IAsyncEnumerable<TItem>? actual, IEv
{
_firstMismatchItem = item;
_foundMismatch = true;
_expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable<TItem>,
true);
await _expectationBuilder.AddCollectionContext(materializedEnumerable as IMaterializedEnumerable<TItem>);
Outcome = Outcome.Failure;
return this;
}
Expand Down
Loading
Loading