diff --git a/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs new file mode 100644 index 0000000000..10dca85cc3 --- /dev/null +++ b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs @@ -0,0 +1,188 @@ +namespace TUnit.Assertions.Tests.Bugs; + +/// +/// Regression tests for GitHub issue #5706: +/// ItemAt(...).Satisfies(...) should preserve specialised assertion sources for +/// collection-like item values instead of exposing only IAssertionSource<TItem>. +/// +public class Issue5706Tests +{ + [Test] + public async Task List_ItemAt_Satisfies_Preserves_String_Item_Source() + { + IList items = new List { "alpha" }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Contains("pha")); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Collection_Item_Source() + { + IList> items = new List> { new[] { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_IList_Item_Source() + { + IList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_IReadOnlyList_Item_Source() + { + IList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Array_Item_Source() + { + IList items = new List { new[] { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_ConcreteList_Item_Source() + { + IList> items = new List> { new() { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Dictionary_Item_Source() + { + IList> items = new List> + { + new Dictionary { ["answer"] = 42 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Concrete_Dictionary_Item_Source() + { + IList> items = new List> + { + new() { ["answer"] = 42 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Set_Item_Source() + { + IList> items = new List> + { + new HashSet { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_HashSet_Item_Source() + { + IList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_String_Item_Source() + { + IReadOnlyList items = new List { "alpha" }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Contains("pha")); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Collection_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_IList_Item_Source() + { + IReadOnlyList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_IReadOnlyList_Item_Source() + { + IReadOnlyList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Array_Item_Source() + { + IReadOnlyList items = new List { new[] { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Dictionary_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { ["answer"] = 42 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_ReadOnly_Dictionary_Item_Source() + { + IReadOnlyList> items = new List> + { + new Dictionary { ["answer"] = 42 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Set_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_ReadOnly_Set_Item_Source() + { + IReadOnlyList> items = new List> + { + new HashSet { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } +} diff --git a/TUnit.Assertions.Tests/Bugs/Issue5778Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5778Tests.cs new file mode 100644 index 0000000000..64f789bba0 --- /dev/null +++ b/TUnit.Assertions.Tests/Bugs/Issue5778Tests.cs @@ -0,0 +1,188 @@ +namespace TUnit.Assertions.Tests.Bugs; + +/// +/// Regression tests mirroring for the LastItem path. +/// LastItem(...).Satisfies(...) should preserve specialised assertion sources for +/// collection-like item values instead of exposing only IAssertionSource<TItem>. +/// +public class Issue5778Tests +{ + [Test] + public async Task List_LastItem_Satisfies_Preserves_String_Item_Source() + { + IList items = new List { "alpha" }; + + await Assert.That(items).LastItem().Satisfies(item => item.Contains("pha")); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Collection_Item_Source() + { + IList> items = new List> { new[] { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_IList_Item_Source() + { + IList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_IReadOnlyList_Item_Source() + { + IList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Array_Item_Source() + { + IList items = new List { new[] { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_ConcreteList_Item_Source() + { + IList> items = new List> { new() { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Dictionary_Item_Source() + { + IList> items = new List> + { + new Dictionary { ["answer"] = 42 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Concrete_Dictionary_Item_Source() + { + IList> items = new List> + { + new() { ["answer"] = 42 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Set_Item_Source() + { + IList> items = new List> + { + new HashSet { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_HashSet_Item_Source() + { + IList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_String_Item_Source() + { + IReadOnlyList items = new List { "alpha" }; + + await Assert.That(items).LastItem().Satisfies(item => item.Contains("pha")); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_Collection_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_IList_Item_Source() + { + IReadOnlyList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_IReadOnlyList_Item_Source() + { + IReadOnlyList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_Array_Item_Source() + { + IReadOnlyList items = new List { new[] { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_Dictionary_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { ["answer"] = 42 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_ReadOnly_Dictionary_Item_Source() + { + IReadOnlyList> items = new List> + { + new Dictionary { ["answer"] = 42 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_Set_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_ReadOnly_Set_Item_Source() + { + IReadOnlyList> items = new List> + { + new HashSet { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } +} diff --git a/TUnit.Assertions/Conditions/ListAssertions.cs b/TUnit.Assertions/Conditions/ListAssertions.cs index 904de2ad88..4ca5e25cc4 100644 --- a/TUnit.Assertions/Conditions/ListAssertions.cs +++ b/TUnit.Assertions/Conditions/ListAssertions.cs @@ -62,6 +62,9 @@ protected override Task CheckAsync(EvaluationMetadata me /// This enables patterns like: Assert.That(list).ItemAt(0).IsEqualTo(expected) /// public class ListItemAtSource : IAssertionSource +#if !NETSTANDARD2_0 + , IItemSatisfiesSource> +#endif where TList : IList { private readonly AssertionContext _listContext; @@ -126,6 +129,33 @@ public ListItemAtNullAssertion IsNotNull() public ListItemAtSatisfiesAssertion Satisfies( Func, Assertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) + { + return CreateSatisfiesAssertion( + (item, index) => assertion(new ValueAssertion(item, $"item[{index}]")), + expression); + } + +#if !NETSTANDARD2_0 + /// + /// Asserts that the item at the index satisfies the given assertion expressed against + /// a specialised assertion source . The source is constructed + /// per-item via its static factory. + /// Specify explicitly or via a typed lambda parameter. + /// + public ListItemAtSatisfiesAssertion Satisfies( + Func assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TSource : IAssertionSourceFor + { + return CreateSatisfiesAssertion( + (item, index) => assertion(TSource.Create(item, $"item[{index}]")), + expression); + } +#endif + + internal ListItemAtSatisfiesAssertion CreateSatisfiesAssertion( + Func assertion, + string? expression) { _listContext.ExpressionBuilder.Append($".Satisfies({expression})"); return new ListItemAtSatisfiesAssertion(_listContext, _index, assertion); @@ -179,6 +209,9 @@ public IsNotAssignableFromAssertion IsNotAssignableFrom public class ListLastItemSource : IAssertionSource +#if !NETSTANDARD2_0 + , IItemSatisfiesSource> +#endif where TList : IList { private readonly AssertionContext _listContext; @@ -241,6 +274,33 @@ public ListLastItemNullAssertion IsNotNull() public ListLastItemSatisfiesAssertion Satisfies( Func, Assertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) + { + return CreateSatisfiesAssertion( + item => assertion(new ValueAssertion(item, "lastItem")), + expression); + } + +#if !NETSTANDARD2_0 + /// + /// Asserts that the last item satisfies the given assertion expressed against + /// a specialised assertion source . The source is constructed + /// from the last item via its static factory. + /// Specify explicitly or via a typed lambda parameter. + /// + public ListLastItemSatisfiesAssertion Satisfies( + Func assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TSource : IAssertionSourceFor + { + return CreateSatisfiesAssertion( + item => assertion(TSource.Create(item, "lastItem")), + expression); + } +#endif + + internal ListLastItemSatisfiesAssertion CreateSatisfiesAssertion( + Func assertion, + string? expression) { _listContext.ExpressionBuilder.Append($".Satisfies({expression})"); return new ListLastItemSatisfiesAssertion(_listContext, assertion); @@ -420,12 +480,30 @@ public class ListItemAtSatisfiesAssertion : ListAssertionBase { private readonly int _index; - private readonly Func, Assertion?> _assertion; + private readonly Func _assertion; + /// + /// Backward-compatible constructor preserved for direct consumers from before the + /// internal refactor to a raw-item delegate. Wraps the supplied factory in a + /// — the unspecialised path. Internal callers should + /// use the entry point + /// to get specialised assertion sources. + /// public ListItemAtSatisfiesAssertion( AssertionContext context, int index, Func, Assertion?> assertion) + : this( + context, + index, + (item, itemIndex) => assertion(new ValueAssertion(item, $"item[{itemIndex}]"))) + { + } + + internal ListItemAtSatisfiesAssertion( + AssertionContext context, + int index, + Func assertion) : base(context) { _index = index; @@ -451,8 +529,7 @@ protected override async Task CheckAsync(EvaluationMetadata(actualItem, $"item[{_index}]"); - var resultingAssertion = _assertion(itemSource); + var resultingAssertion = _assertion(actualItem, _index); if (resultingAssertion != null) { @@ -595,11 +672,25 @@ protected override string GetExpectation() => public class ListLastItemSatisfiesAssertion : ListAssertionBase where TList : IList { - private readonly Func, Assertion?> _assertion; + private readonly Func _assertion; + /// + /// Backward-compatible constructor preserved for direct consumers from before the + /// internal refactor to a raw-item delegate. Wraps the supplied factory in a + /// — the unspecialised path. Internal callers should + /// use the entry point + /// to get specialised assertion sources. + /// public ListLastItemSatisfiesAssertion( AssertionContext context, Func, Assertion?> assertion) + : this(context, item => assertion(new ValueAssertion(item, "lastItem"))) + { + } + + internal ListLastItemSatisfiesAssertion( + AssertionContext context, + Func assertion) : base(context) { _assertion = assertion; @@ -623,8 +714,7 @@ protected override async Task CheckAsync(EvaluationMetadata(lastItem, "lastItem"); - var resultingAssertion = _assertion(itemSource); + var resultingAssertion = _assertion(lastItem); if (resultingAssertion != null) { diff --git a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs index 5d91d94373..f098cedeee 100644 --- a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs +++ b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs @@ -64,6 +64,9 @@ protected override Task CheckAsync(EvaluationMetadata me /// This enables patterns like: Assert.That(list).ItemAt(0).IsEqualTo(expected) /// public class ReadOnlyListItemAtSource : IAssertionSource +#if !NETSTANDARD2_0 + , IItemSatisfiesSource> +#endif where TList : IReadOnlyList { private readonly AssertionContext _listContext; @@ -128,6 +131,33 @@ public ReadOnlyListItemAtNullAssertion IsNotNull() public ReadOnlyListItemAtSatisfiesAssertion Satisfies( Func, Assertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) + { + return CreateSatisfiesAssertion( + (item, index) => assertion(new ValueAssertion(item, $"item[{index}]")), + expression); + } + +#if !NETSTANDARD2_0 + /// + /// Asserts that the item at the index satisfies the given assertion expressed against + /// a specialised assertion source . The source is constructed + /// per-item via its static factory. + /// Specify explicitly or via a typed lambda parameter. + /// + public ReadOnlyListItemAtSatisfiesAssertion Satisfies( + Func assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TSource : IAssertionSourceFor + { + return CreateSatisfiesAssertion( + (item, index) => assertion(TSource.Create(item, $"item[{index}]")), + expression); + } +#endif + + internal ReadOnlyListItemAtSatisfiesAssertion CreateSatisfiesAssertion( + Func assertion, + string? expression) { _listContext.ExpressionBuilder.Append($".Satisfies({expression})"); return new ReadOnlyListItemAtSatisfiesAssertion(_listContext, _index, assertion); @@ -181,6 +211,9 @@ public IsNotAssignableFromAssertion IsNotAssignableFrom public class ReadOnlyListLastItemSource : IAssertionSource +#if !NETSTANDARD2_0 + , IItemSatisfiesSource> +#endif where TList : IReadOnlyList { private readonly AssertionContext _listContext; @@ -243,6 +276,33 @@ public ReadOnlyListLastItemNullAssertion IsNotNull() public ReadOnlyListLastItemSatisfiesAssertion Satisfies( Func, Assertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) + { + return CreateSatisfiesAssertion( + item => assertion(new ValueAssertion(item, "lastItem")), + expression); + } + +#if !NETSTANDARD2_0 + /// + /// Asserts that the last item satisfies the given assertion expressed against + /// a specialised assertion source . The source is constructed + /// from the last item via its static factory. + /// Specify explicitly or via a typed lambda parameter. + /// + public ReadOnlyListLastItemSatisfiesAssertion Satisfies( + Func assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TSource : IAssertionSourceFor + { + return CreateSatisfiesAssertion( + item => assertion(TSource.Create(item, "lastItem")), + expression); + } +#endif + + internal ReadOnlyListLastItemSatisfiesAssertion CreateSatisfiesAssertion( + Func assertion, + string? expression) { _listContext.ExpressionBuilder.Append($".Satisfies({expression})"); return new ReadOnlyListLastItemSatisfiesAssertion(_listContext, assertion); @@ -422,12 +482,30 @@ public class ReadOnlyListItemAtSatisfiesAssertion : ReadOnlyListAs where TList : IReadOnlyList { private readonly int _index; - private readonly Func, Assertion?> _assertion; + private readonly Func _assertion; + /// + /// Backward-compatible constructor preserved for direct consumers from before the + /// internal refactor to a raw-item delegate. Wraps the supplied factory in a + /// — the unspecialised path. Internal callers should + /// use the entry + /// point to get specialised assertion sources. + /// public ReadOnlyListItemAtSatisfiesAssertion( AssertionContext context, int index, Func, Assertion?> assertion) + : this( + context, + index, + (item, itemIndex) => assertion(new ValueAssertion(item, $"item[{itemIndex}]"))) + { + } + + internal ReadOnlyListItemAtSatisfiesAssertion( + AssertionContext context, + int index, + Func assertion) : base(context) { _index = index; @@ -453,8 +531,7 @@ protected override async Task CheckAsync(EvaluationMetadata(actualItem, $"item[{_index}]"); - var resultingAssertion = _assertion(itemSource); + var resultingAssertion = _assertion(actualItem, _index); if (resultingAssertion != null) { @@ -597,11 +674,25 @@ protected override string GetExpectation() => public class ReadOnlyListLastItemSatisfiesAssertion : ReadOnlyListAssertionBase where TList : IReadOnlyList { - private readonly Func, Assertion?> _assertion; + private readonly Func _assertion; + /// + /// Backward-compatible constructor preserved for direct consumers from before the + /// internal refactor to a raw-item delegate. Wraps the supplied factory in a + /// — the unspecialised path. Internal callers should + /// use the + /// entry point to get specialised assertion sources. + /// public ReadOnlyListLastItemSatisfiesAssertion( AssertionContext context, Func, Assertion?> assertion) + : this(context, item => assertion(new ValueAssertion(item, "lastItem"))) + { + } + + internal ReadOnlyListLastItemSatisfiesAssertion( + AssertionContext context, + Func assertion) : base(context) { _assertion = assertion; @@ -625,8 +716,7 @@ protected override async Task CheckAsync(EvaluationMetadata(lastItem, "lastItem"); - var resultingAssertion = _assertion(itemSource); + var resultingAssertion = _assertion(lastItem); if (resultingAssertion != null) { diff --git a/TUnit.Assertions/Core/IAssertionSourceFor.cs b/TUnit.Assertions/Core/IAssertionSourceFor.cs new file mode 100644 index 0000000000..fff3664693 --- /dev/null +++ b/TUnit.Assertions/Core/IAssertionSourceFor.cs @@ -0,0 +1,34 @@ +#if !NETSTANDARD2_0 +namespace TUnit.Assertions.Core; + +/// +/// Marks an assertion source as constructible from a raw value, enabling generic +/// dispatch through Satisfies<TSource> on item-at and similar entry points. +/// Implementations expose a static factory used to materialise the specialised source +/// per-item without per-shape overload enumeration. +/// +/// +/// Intentionally does not extend : a single +/// implementing class can implement multiple parameterisations of this interface +/// (e.g. against both an interface item type and a matching concrete type) without +/// triggering conflicting Context property requirements. +/// +/// The value type the source wraps. +/// The implementing source type (CRTP). +public interface IAssertionSourceFor + where TSelf : IAssertionSourceFor +{ + /// + /// Constructs the specialised assertion source wrapping with the + /// given . + /// + /// + /// Implementations may receive a when the + /// caller selects from a collection that allows nulls. Whether the resulting assertion + /// surfaces a graceful failure or throws on a subsequent operation is the implementer's + /// responsibility — the contract is "construct a source that callers can run assertions + /// against", not "validate non-null up front". + /// + static abstract TSelf Create(TItem item, string label); +} +#endif diff --git a/TUnit.Assertions/Core/IItemSatisfiesSource.cs b/TUnit.Assertions/Core/IItemSatisfiesSource.cs new file mode 100644 index 0000000000..464851281b --- /dev/null +++ b/TUnit.Assertions/Core/IItemSatisfiesSource.cs @@ -0,0 +1,24 @@ +#if !NETSTANDARD2_0 +namespace TUnit.Assertions.Core; + +/// +/// Common shape for item-selector sources (ItemAt, LastItem, etc.) that expose a +/// generic Satisfies<TSource> entry. Lets a single set of per-shape +/// extension overloads dispatch across all selector variants by binding to this +/// interface rather than each concrete source type. +/// +/// +/// Extends so that extension methods targeting +/// this interface bind in preference to those targeting +/// directly — without that, the predicate-style Satisfies(Func<TItem, bool>) +/// extension can win overload resolution for items whose specialised assertion methods +/// return bool (e.g. IDictionary.ContainsKey). +/// +/// The selected item's type. +/// The selector-specific assertion result type. +public interface IItemSatisfiesSource : IAssertionSource +{ + TResult Satisfies(Func assertion, string? expression = null) + where TSource : IAssertionSourceFor; +} +#endif diff --git a/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs new file mode 100644 index 0000000000..3bb52670fc --- /dev/null +++ b/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs @@ -0,0 +1,99 @@ +#if !NETSTANDARD2_0 +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Sources; + +namespace TUnit.Assertions.Extensions; + +/// +/// Specialised Satisfies overloads when the selected item (from ItemAt, +/// LastItem, etc.) is itself a collection-like value. Each overload binds the +/// matching implementation and dispatches +/// through , +/// so a single set of per-shape overloads serves every item-selector source type. +/// +/// +/// Both interface-shaped (e.g. IList<T>) and concrete-shaped +/// (e.g. List<T>) overloads are required because C# overload +/// resolution performs exact type matching on the source's TItem: +/// IList<List<int>> binds the List<TInner> +/// overload, never the IList<TInner> one. +/// +/// When introducing a new +/// implementation, add the matching overload below; otherwise users must +/// spell out the type argument explicitly: .Satisfies<MyAssertion<T>>(...). +/// +public static class CollectionItemSatisfiesExtensions +{ + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TKey : notnull + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TKey : notnull + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + +#if NET5_0_OR_GREATER + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); +#endif + + public static TResult Satisfies( + this IItemSatisfiesSource source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TKey : notnull + => source.Satisfies>(assertion, expression); +} +#endif diff --git a/TUnit.Assertions/Sources/ArrayAssertion.cs b/TUnit.Assertions/Sources/ArrayAssertion.cs index 6efe0fc411..040a6f066e 100644 --- a/TUnit.Assertions/Sources/ArrayAssertion.cs +++ b/TUnit.Assertions/Sources/ArrayAssertion.cs @@ -10,6 +10,9 @@ namespace TUnit.Assertions.Sources; /// enabling generated assertions that target concrete array types (e.g., string[]). /// public class ArrayAssertion : CollectionAssertionBase +#if !NETSTANDARD2_0 + , IAssertionSourceFor> +#endif { public ArrayAssertion(TItem[]? value, string? expression) : base(new AssertionContext(value!, CreateExpressionBuilder(expression))) @@ -21,6 +24,10 @@ internal ArrayAssertion(AssertionContext context) { } +#if !NETSTANDARD2_0 + public static ArrayAssertion Create(TItem[] item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); diff --git a/TUnit.Assertions/Sources/CollectionAssertion.cs b/TUnit.Assertions/Sources/CollectionAssertion.cs index 9d91a042fe..8bddae8fa5 100644 --- a/TUnit.Assertions/Sources/CollectionAssertion.cs +++ b/TUnit.Assertions/Sources/CollectionAssertion.cs @@ -11,6 +11,9 @@ namespace TUnit.Assertions.Sources; /// that persist through And/Or continuations. /// public class CollectionAssertion : CollectionAssertionBase, TItem> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, CollectionAssertion> +#endif { public CollectionAssertion(IEnumerable value, string? expression) : base(new AssertionContext>(value, CreateExpressionBuilder(expression))) @@ -22,6 +25,10 @@ internal CollectionAssertion(AssertionContext> context) { } +#if !NETSTANDARD2_0 + public static CollectionAssertion Create(IEnumerable item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); diff --git a/TUnit.Assertions/Sources/DictionaryAssertion.cs b/TUnit.Assertions/Sources/DictionaryAssertion.cs index d08ef3fcb5..74c7bb6d5a 100644 --- a/TUnit.Assertions/Sources/DictionaryAssertion.cs +++ b/TUnit.Assertions/Sources/DictionaryAssertion.cs @@ -11,6 +11,9 @@ namespace TUnit.Assertions.Sources; /// plus collection methods (Contains, IsEmpty, All, etc.) since dictionaries are collections of KeyValuePair items. /// public class DictionaryAssertion : DictionaryAssertionBase, TKey, TValue> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, DictionaryAssertion> +#endif where TKey : notnull { public DictionaryAssertion(IReadOnlyDictionary? value, string? expression) @@ -18,6 +21,10 @@ public DictionaryAssertion(IReadOnlyDictionary? value, string? exp { } +#if !NETSTANDARD2_0 + public static DictionaryAssertion Create(IReadOnlyDictionary item, string label) => new(item, label); +#endif + private static AssertionContext> CreateContext( IReadOnlyDictionary? value, string? expression) diff --git a/TUnit.Assertions/Sources/ListAssertion.cs b/TUnit.Assertions/Sources/ListAssertion.cs index 0dc4c587ea..cd34cf370e 100644 --- a/TUnit.Assertions/Sources/ListAssertion.cs +++ b/TUnit.Assertions/Sources/ListAssertion.cs @@ -10,6 +10,10 @@ namespace TUnit.Assertions.Sources; /// in addition to all standard collection methods. /// public class ListAssertion : ListAssertionBase, TItem> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, ListAssertion> + , IAssertionSourceFor, ListAssertion> +#endif { public ListAssertion(IList? value, string? expression) : base(CreateContext(value, expression)) @@ -21,6 +25,11 @@ internal ListAssertion(AssertionContext> context) { } +#if !NETSTANDARD2_0 + public static ListAssertion Create(IList item, string label) => new(item, label); + public static ListAssertion Create(List item, string label) => new(item, label); +#endif + private static AssertionContext> CreateContext( IList? value, string? expression) diff --git a/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs b/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs index 2d0afd5ae0..3401c197de 100644 --- a/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs +++ b/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs @@ -11,6 +11,10 @@ namespace TUnit.Assertions.Sources; /// plus collection methods (Contains, IsEmpty, All, etc.) since dictionaries are collections of KeyValuePair items. /// public class MutableDictionaryAssertion : MutableDictionaryAssertionBase, TKey, TValue> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, MutableDictionaryAssertion> + , IAssertionSourceFor, MutableDictionaryAssertion> +#endif where TKey : notnull { public MutableDictionaryAssertion(IDictionary? value, string? expression) @@ -18,6 +22,11 @@ public MutableDictionaryAssertion(IDictionary? value, string? expr { } +#if !NETSTANDARD2_0 + public static MutableDictionaryAssertion Create(IDictionary item, string label) => new(item, label); + public static MutableDictionaryAssertion Create(Dictionary item, string label) => new(item, label); +#endif + private static AssertionContext> CreateContext( IDictionary? value, string? expression) diff --git a/TUnit.Assertions/Sources/ReadOnlyListAssertion.cs b/TUnit.Assertions/Sources/ReadOnlyListAssertion.cs index 367a079b6e..3645f0fe01 100644 --- a/TUnit.Assertions/Sources/ReadOnlyListAssertion.cs +++ b/TUnit.Assertions/Sources/ReadOnlyListAssertion.cs @@ -8,6 +8,9 @@ namespace TUnit.Assertions.Sources; /// /// The type of items in the read-only list public class ReadOnlyListAssertion : ReadOnlyListAssertionBase, TItem> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, ReadOnlyListAssertion> +#endif { public ReadOnlyListAssertion(IReadOnlyList? value, string? expression) : base(CreateContext(value, expression)) @@ -19,6 +22,10 @@ internal ReadOnlyListAssertion(AssertionContext> context) { } +#if !NETSTANDARD2_0 + public static ReadOnlyListAssertion Create(IReadOnlyList item, string label) => new(item, label); +#endif + private static AssertionContext> CreateContext( IReadOnlyList? value, string? expression) diff --git a/TUnit.Assertions/Sources/SetAssertion.cs b/TUnit.Assertions/Sources/SetAssertion.cs index 155b306051..9e9a3012cb 100644 --- a/TUnit.Assertions/Sources/SetAssertion.cs +++ b/TUnit.Assertions/Sources/SetAssertion.cs @@ -11,6 +11,9 @@ namespace TUnit.Assertions.Sources; /// /// The type of items in the set. public class SetAssertion : SetAssertionBase, TItem>, IAssertionSource> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, SetAssertion> +#endif { /// /// Creates a new SetAssertion for the given set. @@ -34,6 +37,10 @@ private protected SetAssertion(AssertionContext> context) /// AssertionContext> IAssertionSource>.Context => Context; +#if !NETSTANDARD2_0 + public static SetAssertion Create(ISet item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); @@ -49,6 +56,9 @@ private static StringBuilder CreateExpressionBuilder(string? expression) /// /// The type of items in the set. public class ReadOnlySetAssertion : SetAssertionBase, TItem>, IAssertionSource> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, ReadOnlySetAssertion> +#endif { /// /// Creates a new ReadOnlySetAssertion for the given read-only set. @@ -72,6 +82,10 @@ private protected ReadOnlySetAssertion(AssertionContext> con /// AssertionContext> IAssertionSource>.Context => Context; +#if !NETSTANDARD2_0 + public static ReadOnlySetAssertion Create(IReadOnlySet item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); @@ -87,6 +101,9 @@ private static StringBuilder CreateExpressionBuilder(string? expression) /// /// The type of items in the set. public class HashSetAssertion : SetAssertionBase, TItem>, IAssertionSource> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, HashSetAssertion> +#endif { /// /// Creates a new HashSetAssertion for the given hash set. @@ -110,6 +127,10 @@ private protected HashSetAssertion(AssertionContext> context) /// AssertionContext> IAssertionSource>.Context => Context; +#if !NETSTANDARD2_0 + public static HashSetAssertion Create(HashSet item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 1923ab110f..fc2e65071d 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -1534,7 +1534,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListItemAtSource : ., . + public class ListItemAtSource : ., ., .> where TList : . { public ListItemAtSource(. listContext, int index) { } @@ -1550,6 +1550,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ListLastItemEqualsAssertion : . where TList : . @@ -1572,7 +1574,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListLastItemSource : ., . + public class ListLastItemSource : ., ., .> where TList : . { public ListLastItemSource(. listContext) { } @@ -1588,6 +1590,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.("IsEqualTo", OverloadResolutionPriority=2)] public class LongEqualsAssertion : . @@ -1786,7 +1790,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListItemAtSource : ., . + public class ReadOnlyListItemAtSource : ., ., .> where TList : . { public ReadOnlyListItemAtSource(. listContext, int index) { } @@ -1802,6 +1806,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ReadOnlyListLastItemEqualsAssertion : . where TList : . @@ -1824,7 +1830,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListLastItemSource : ., . + public class ReadOnlyListLastItemSource : ., ., .> where TList : . { public ReadOnlyListLastItemSource(. listContext) { } @@ -1840,6 +1846,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.<.Rune>("IsAscii", CustomName="IsNotAscii", ExpectationMessage="be ASCII", NegateLogic=true)] [.<.Rune>("IsAscii", ExpectationMessage="be ASCII")] @@ -2501,6 +2509,11 @@ namespace .Core . AssertAsync(); } public interface IAssertionSource { } + public interface IAssertionSourceFor + where TSelf : . + { + TSelf Create(TItem item, string label); + } public interface IAssertionSource : . { . Context { get; } @@ -2512,6 +2525,11 @@ namespace .Core . IsTypeOf(); } public interface IDelegateAssertionSource : ., . { } + public interface IItemSatisfiesSource : ., . + { + TResult Satisfies( assertion, string? expression = null) + where TSource : .; + } public class ListAndContinuation : . where TList : . { } public class ListOrContinuation : . @@ -3093,6 +3111,23 @@ namespace .Extensions public static . IsInOrder(this . source) where TCollection : . { } } + public static class CollectionItemSatisfiesExtensions + { + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + } public static class ComparisonAssertionExtensions { [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] @@ -6088,9 +6123,10 @@ namespace .Extensions } namespace .Sources { - public class ArrayAssertion : . + public class ArrayAssertion : ., .> { public ArrayAssertion(TItem[]? value, string? expression) { } + public static . Create(TItem[] item, string label) { } } public class AsyncDelegateAssertion : ., ., .<.>, . { @@ -6221,9 +6257,10 @@ namespace .Sources public . IsOrderedByDescending( keySelector, .? comparer, [.("keySelector")] string? selectorExpression = null, [.("comparer")] string? comparerExpression = null) { } public . IsTypeOf() { } } - public class CollectionAssertion : .<., TItem> + public class CollectionAssertion : .<., TItem>, .<., .> { public CollectionAssertion(. value, string? expression) { } + public static . Create(. item, string label) { } } public class DelegateAssertion : ., ., . { @@ -6259,10 +6296,11 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class DictionaryAssertion : .<., TKey, TValue> + public class DictionaryAssertion : .<., TKey, TValue>, .<., .> where TKey : notnull { public DictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class FuncAssertion : ., ., . { @@ -6287,10 +6325,11 @@ namespace .Sources public . ThrowsExactly() where TException : { } } - public class HashSetAssertion : .<., TItem>, ., .<.> + public class HashSetAssertion : .<., TItem>, ., .<., .>, .<.> { public HashSetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public abstract class ListAssertionBase : . where TList : . @@ -6303,9 +6342,11 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? expression = null) { } public . LastItem() { } } - public class ListAssertion : .<., TItem> + public class ListAssertion : .<., TItem>, .<., .>, .<., .> { public ListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public class MemoryAndContinuation : . { @@ -6370,10 +6411,12 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class MutableDictionaryAssertion : .<., TKey, TValue> + public class MutableDictionaryAssertion : .<., TKey, TValue>, .<., .>, .<., .> where TKey : notnull { public MutableDictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public abstract class ReadOnlyListAssertionBase : . where TList : . @@ -6386,19 +6429,21 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? indexExpression = null) { } public . LastItem() { } } - public class ReadOnlyListAssertion : .<., TItem> + public class ReadOnlyListAssertion : .<., TItem>, .<., .> { public ReadOnlyListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class ReadOnlyMemoryAssertion : .<, TItem>, ., .<> { public ReadOnlyMemoryAssertion( value, string expression) { } protected override . CreateAdapter( value) { } } - public class ReadOnlySetAssertion : .<., TItem>, ., .<.> + public class ReadOnlySetAssertion : .<., TItem>, ., .<., .>, .<.> { public ReadOnlySetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetAndContinuation : . where TSet : . @@ -6421,10 +6466,11 @@ namespace .Sources public . Overlaps(. other, [.("other")] string? expression = null) { } public . SetEquals(. other, [.("other")] string? expression = null) { } } - public class SetAssertion : .<., TItem>, ., .<.> + public class SetAssertion : .<., TItem>, ., .<., .>, .<.> { public SetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetOrContinuation : . where TSet : . diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index c184337bb5..4874df1872 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -1517,7 +1517,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListItemAtSource : ., . + public class ListItemAtSource : ., ., .> where TList : . { public ListItemAtSource(. listContext, int index) { } @@ -1533,6 +1533,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ListLastItemEqualsAssertion : . where TList : . @@ -1555,7 +1557,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListLastItemSource : ., . + public class ListLastItemSource : ., ., .> where TList : . { public ListLastItemSource(. listContext) { } @@ -1571,6 +1573,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.("IsEqualTo", OverloadResolutionPriority=2)] public class LongEqualsAssertion : . @@ -1769,7 +1773,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListItemAtSource : ., . + public class ReadOnlyListItemAtSource : ., ., .> where TList : . { public ReadOnlyListItemAtSource(. listContext, int index) { } @@ -1785,6 +1789,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ReadOnlyListLastItemEqualsAssertion : . where TList : . @@ -1807,7 +1813,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListLastItemSource : ., . + public class ReadOnlyListLastItemSource : ., ., .> where TList : . { public ReadOnlyListLastItemSource(. listContext) { } @@ -1823,6 +1829,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.<.Rune>("IsAscii", CustomName="IsNotAscii", ExpectationMessage="be ASCII", NegateLogic=true)] [.<.Rune>("IsAscii", ExpectationMessage="be ASCII")] @@ -2480,6 +2488,11 @@ namespace .Core . AssertAsync(); } public interface IAssertionSource { } + public interface IAssertionSourceFor + where TSelf : . + { + TSelf Create(TItem item, string label); + } public interface IAssertionSource : . { . Context { get; } @@ -2491,6 +2504,11 @@ namespace .Core . IsTypeOf(); } public interface IDelegateAssertionSource : ., . { } + public interface IItemSatisfiesSource : ., . + { + TResult Satisfies( assertion, string? expression = null) + where TSource : .; + } public class ListAndContinuation : . where TList : . { } public class ListOrContinuation : . @@ -3058,6 +3076,23 @@ namespace .Extensions public static . IsInOrder(this . source) where TCollection : . { } } + public static class CollectionItemSatisfiesExtensions + { + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + } public static class ComparisonAssertionExtensions { [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] @@ -6017,9 +6052,10 @@ namespace .Extensions } namespace .Sources { - public class ArrayAssertion : . + public class ArrayAssertion : ., .> { public ArrayAssertion(TItem[]? value, string? expression) { } + public static . Create(TItem[] item, string label) { } } public class AsyncDelegateAssertion : ., ., .<.>, . { @@ -6149,9 +6185,10 @@ namespace .Sources public . IsOrderedByDescending( keySelector, .? comparer, [.("keySelector")] string? selectorExpression = null, [.("comparer")] string? comparerExpression = null) { } public . IsTypeOf() { } } - public class CollectionAssertion : .<., TItem> + public class CollectionAssertion : .<., TItem>, .<., .> { public CollectionAssertion(. value, string? expression) { } + public static . Create(. item, string label) { } } public class DelegateAssertion : ., ., . { @@ -6187,10 +6224,11 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class DictionaryAssertion : .<., TKey, TValue> + public class DictionaryAssertion : .<., TKey, TValue>, .<., .> where TKey : notnull { public DictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class FuncAssertion : ., ., . { @@ -6215,10 +6253,11 @@ namespace .Sources public . ThrowsExactly() where TException : { } } - public class HashSetAssertion : .<., TItem>, ., .<.> + public class HashSetAssertion : .<., TItem>, ., .<., .>, .<.> { public HashSetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public abstract class ListAssertionBase : . where TList : . @@ -6231,9 +6270,11 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? expression = null) { } public . LastItem() { } } - public class ListAssertion : .<., TItem> + public class ListAssertion : .<., TItem>, .<., .>, .<., .> { public ListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public class MemoryAndContinuation : . { @@ -6298,10 +6339,12 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class MutableDictionaryAssertion : .<., TKey, TValue> + public class MutableDictionaryAssertion : .<., TKey, TValue>, .<., .>, .<., .> where TKey : notnull { public MutableDictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public abstract class ReadOnlyListAssertionBase : . where TList : . @@ -6314,19 +6357,21 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? indexExpression = null) { } public . LastItem() { } } - public class ReadOnlyListAssertion : .<., TItem> + public class ReadOnlyListAssertion : .<., TItem>, .<., .> { public ReadOnlyListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class ReadOnlyMemoryAssertion : .<, TItem>, ., .<> { public ReadOnlyMemoryAssertion( value, string expression) { } protected override . CreateAdapter( value) { } } - public class ReadOnlySetAssertion : .<., TItem>, ., .<.> + public class ReadOnlySetAssertion : .<., TItem>, ., .<., .>, .<.> { public ReadOnlySetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetAndContinuation : . where TSet : . @@ -6349,10 +6394,11 @@ namespace .Sources public . Overlaps(. other, [.("other")] string? expression = null) { } public . SetEquals(. other, [.("other")] string? expression = null) { } } - public class SetAssertion : .<., TItem>, ., .<.> + public class SetAssertion : .<., TItem>, ., .<., .>, .<.> { public SetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetOrContinuation : . where TSet : . diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index c49c22d3f6..54d39689d4 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -1534,7 +1534,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListItemAtSource : ., . + public class ListItemAtSource : ., ., .> where TList : . { public ListItemAtSource(. listContext, int index) { } @@ -1550,6 +1550,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ListLastItemEqualsAssertion : . where TList : . @@ -1572,7 +1574,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListLastItemSource : ., . + public class ListLastItemSource : ., ., .> where TList : . { public ListLastItemSource(. listContext) { } @@ -1588,6 +1590,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.("IsEqualTo", OverloadResolutionPriority=2)] public class LongEqualsAssertion : . @@ -1786,7 +1790,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListItemAtSource : ., . + public class ReadOnlyListItemAtSource : ., ., .> where TList : . { public ReadOnlyListItemAtSource(. listContext, int index) { } @@ -1802,6 +1806,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ReadOnlyListLastItemEqualsAssertion : . where TList : . @@ -1824,7 +1830,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListLastItemSource : ., . + public class ReadOnlyListLastItemSource : ., ., .> where TList : . { public ReadOnlyListLastItemSource(. listContext) { } @@ -1840,6 +1846,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.<.Rune>("IsAscii", CustomName="IsNotAscii", ExpectationMessage="be ASCII", NegateLogic=true)] [.<.Rune>("IsAscii", ExpectationMessage="be ASCII")] @@ -2501,6 +2509,11 @@ namespace .Core . AssertAsync(); } public interface IAssertionSource { } + public interface IAssertionSourceFor + where TSelf : . + { + TSelf Create(TItem item, string label); + } public interface IAssertionSource : . { . Context { get; } @@ -2512,6 +2525,11 @@ namespace .Core . IsTypeOf(); } public interface IDelegateAssertionSource : ., . { } + public interface IItemSatisfiesSource : ., . + { + TResult Satisfies( assertion, string? expression = null) + where TSource : .; + } public class ListAndContinuation : . where TList : . { } public class ListOrContinuation : . @@ -3093,6 +3111,23 @@ namespace .Extensions public static . IsInOrder(this . source) where TCollection : . { } } + public static class CollectionItemSatisfiesExtensions + { + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + } public static class ComparisonAssertionExtensions { [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] @@ -6088,9 +6123,10 @@ namespace .Extensions } namespace .Sources { - public class ArrayAssertion : . + public class ArrayAssertion : ., .> { public ArrayAssertion(TItem[]? value, string? expression) { } + public static . Create(TItem[] item, string label) { } } public class AsyncDelegateAssertion : ., ., .<.>, . { @@ -6221,9 +6257,10 @@ namespace .Sources public . IsOrderedByDescending( keySelector, .? comparer, [.("keySelector")] string? selectorExpression = null, [.("comparer")] string? comparerExpression = null) { } public . IsTypeOf() { } } - public class CollectionAssertion : .<., TItem> + public class CollectionAssertion : .<., TItem>, .<., .> { public CollectionAssertion(. value, string? expression) { } + public static . Create(. item, string label) { } } public class DelegateAssertion : ., ., . { @@ -6259,10 +6296,11 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class DictionaryAssertion : .<., TKey, TValue> + public class DictionaryAssertion : .<., TKey, TValue>, .<., .> where TKey : notnull { public DictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class FuncAssertion : ., ., . { @@ -6287,10 +6325,11 @@ namespace .Sources public . ThrowsExactly() where TException : { } } - public class HashSetAssertion : .<., TItem>, ., .<.> + public class HashSetAssertion : .<., TItem>, ., .<., .>, .<.> { public HashSetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public abstract class ListAssertionBase : . where TList : . @@ -6303,9 +6342,11 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? expression = null) { } public . LastItem() { } } - public class ListAssertion : .<., TItem> + public class ListAssertion : .<., TItem>, .<., .>, .<., .> { public ListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public class MemoryAndContinuation : . { @@ -6370,10 +6411,12 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class MutableDictionaryAssertion : .<., TKey, TValue> + public class MutableDictionaryAssertion : .<., TKey, TValue>, .<., .>, .<., .> where TKey : notnull { public MutableDictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public abstract class ReadOnlyListAssertionBase : . where TList : . @@ -6386,19 +6429,21 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? indexExpression = null) { } public . LastItem() { } } - public class ReadOnlyListAssertion : .<., TItem> + public class ReadOnlyListAssertion : .<., TItem>, .<., .> { public ReadOnlyListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class ReadOnlyMemoryAssertion : .<, TItem>, ., .<> { public ReadOnlyMemoryAssertion( value, string expression) { } protected override . CreateAdapter( value) { } } - public class ReadOnlySetAssertion : .<., TItem>, ., .<.> + public class ReadOnlySetAssertion : .<., TItem>, ., .<., .>, .<.> { public ReadOnlySetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetAndContinuation : . where TSet : . @@ -6421,10 +6466,11 @@ namespace .Sources public . Overlaps(. other, [.("other")] string? expression = null) { } public . SetEquals(. other, [.("other")] string? expression = null) { } } - public class SetAssertion : .<., TItem>, ., .<.> + public class SetAssertion : .<., TItem>, ., .<., .>, .<.> { public SetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetOrContinuation : . where TSet : .