Skip to content

Conversation

@thomhurst
Copy link
Owner

@thomhurst thomhurst commented Jan 3, 2026

Summary

This PR introduces a new adapter-based architecture for collection assertions, enabling first-class support for types beyond IEnumerable<T>:

  • Memory and ReadOnlyMemory - Zero-allocation assertions for memory spans
  • ISet, IReadOnlySet, HashSet - Set-specific operations (subset, superset, overlaps)
  • IDictionary<TKey, TValue> - Full dictionary interface support with type-preserving chains
  • IList - Index-based assertions with mutable list support
  • IReadOnlyList - Index-based assertions for read-only lists
  • IAsyncEnumerable - Async stream assertions with automatic materialization

Architecture Highlights

Component Purpose
Trait Interfaces (Abstractions/) IItemSequence, ICountable, IContainsCheck, ISetOperations, IIndexable
Struct Adapters (Adapters/) Zero-allocation wrappers for each collection type
CollectionChecks (Collections/) Single source of truth for all assertion logic

New Collection Types Supported

Memory/ReadOnlyMemory:

await Assert.That(memory).IsNotEmpty();
await Assert.That(memory).HasCount(5);
await Assert.That(memory).Contains(42);

Set-specific:

await Assert.That(set).IsSubsetOf(superset);
await Assert.That(set).Overlaps(otherSet);
await Assert.That(set).SetEquals(expected);

Dictionary (IDictionary<TKey, TValue>):

await Assert.That(dict).ContainsKey("key");
await Assert.That(dict).ContainsValue(100);
await Assert.That(dict).AllKeys(k => k.StartsWith("prefix_"));
await Assert.That(dict).ContainsKeyWithValue("key1", expectedValue);

IList (index-based):

await Assert.That(list).HasItemAt(0, "first");
await Assert.That(list).ItemAt(1).IsEqualTo("second");
await Assert.That(list).FirstItem().IsNotNull();
await Assert.That(list).LastItem().IsEqualTo("last");

IReadOnlyList (index-based):

await Assert.That(readOnlyList).HasItemAt(0, "first");
await Assert.That(readOnlyList).ItemAt(1).IsEqualTo("second");
await Assert.That(readOnlyList).FirstItem().IsNotNull();
await Assert.That(readOnlyList).LastItem().IsEqualTo("last");

IAsyncEnumerable:

await Assert.That(asyncStream).IsNotEmpty();
await Assert.That(asyncStream).HasCount(5);
await Assert.That(asyncStream).Contains(item);
await Assert.That(asyncStream).All(x => x > 0);

Overload Resolution Priority

Type Priority Reason
Array 5 Most specific (inherits from IList + IReadOnlyList)
IList 4 More specific (mutable + indexed)
IReadOnlyList 3 Index-based read-only access
ISet 2 Set-specific operations
IEnumerable 1 Fallback for all enumerable types

Benefits

  • Zero-allocation struct adapters for performance-critical scenarios
  • Centralized logic in CollectionChecks for maintainability
  • Type-preserving chains - And/Or maintain specific collection type
  • Extensible pattern for adding custom collection type support
  • Index-based assertions for IList and IReadOnlyList
  • Async stream support with automatic materialization for IAsyncEnumerable

Test plan

  • All existing collection assertion tests pass
  • MemoryAssertionTests.cs - 23 tests covering Memory assertions
  • SetAssertionTests.cs - 23 tests covering Set assertions
  • DictionaryCollectionTests.cs - 31 tests including IDictionary methods
  • ListAssertionTests.cs - 30 tests covering IList index-based assertions
  • ReadOnlyListAssertionTests.cs - 27 tests covering IReadOnlyList assertions
  • AsyncEnumerableAssertionTests.cs - 19 tests covering async stream assertions
  • ArrayAssertionTests.cs - 11 tests verifying Array inherits list behaviors
  • PublicAPI snapshot tests updated

Closes #4224

🤖 Generated with Claude Code

This PR introduces a new adapter-based architecture for collection assertions,
enabling support for types beyond IEnumerable<T>:

## New Collection Types Supported
- Memory<T> and ReadOnlyMemory<T> - first-class assertion support
- ISet<T>, IReadOnlySet<T>, HashSet<T> - with set-specific operations

## Architecture Changes
- **Trait Interfaces** (Abstractions/): IItemSequence, ICountable, IContainsCheck,
  ICollectionAdapter, IDictionaryAdapter, ISetAdapter, ISetOperations
- **Struct Adapters** (Adapters/): Zero-allocation adapters for each collection type
- **CollectionChecks** (Collections/): Single source of truth for all assertion logic

## New Assertions
### Memory/ReadOnlyMemory:
- IsEmpty, IsNotEmpty, HasCount, Contains, All, Any, HasDistinctItems, etc.

### Set-specific:
- IsSubsetOf, IsSupersetOf, IsProperSubsetOf, IsProperSupersetOf
- Overlaps, DoesNotOverlap, SetEquals

### Dictionary (new methods):
- ContainsValue, DoesNotContainValue, ContainsKeyWithValue
- AllKeys, AllValues, AnyKey, AnyValue

## Benefits
- Zero-allocation struct adapters for performance
- Centralized assertion logic in CollectionChecks
- Type-preserving And/Or chaining
- Extensible pattern for custom collection types

Closes #4224

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@thomhurst
Copy link
Owner Author

Summary

Adds Memory, ReadOnlyMemory, and Set-specific assertions with an adapter-based architecture that provides zero-allocation wrappers for different collection types.

Critical Issues

None found ✅

Suggestions

1. Memory ToArray() allocation in AsEnumerable (minor performance concern)

Location: TUnit.Assertions/Adapters/MemoryAdapter.cs

Both MemoryAdapter and ReadOnlyMemoryAdapter implement AsEnumerable() using .ToArray():

public IEnumerable<TItem> AsEnumerable() => _source.ToArray();

Impact: This allocates on the heap when AsEnumerable() is called. However, reviewing the usage patterns in CollectionChecks.cs, most operations use the optimized paths (Count, IsEmpty, Contains via IContainsCheck, indexed access via IIndexable) which avoid enumeration entirely.

Why this might be acceptable:

  • AsEnumerable() appears to be a fallback for assertions that don't have optimized adapter paths
  • The allocation only happens when actually needed (e.g., complex LINQ operations in assertion predicates)
  • The struct adapters themselves are zero-allocation

Potential optimization (optional):
Consider using MemoryMarshal.ToEnumerable() if available in your target frameworks, or document that AsEnumerable() is intentionally an allocation fallback and users should prefer the optimized assertion methods.

2. Consider documenting the adapter pattern for extensibility

The adapter-based architecture is well-designed and extensible. Consider adding a brief doc comment or example showing how users could add custom collection type support by implementing the trait interfaces. This would make the extensibility promise in the PR description more actionable.

TUnit Rules Compliance

AOT Compatible - Uses only GetType() for error messages and typeof for descriptions (both AOT-safe)
No VSTest - No Microsoft.VisualStudio.TestPlatform references
Performance-conscious - Struct adapters, zero-allocation where possible
Modern C# - Uses ReadOnlySpan, Memory, collection expressions
No blocking on async - No .Result, .Wait(), or .GetAwaiter().GetResult()
Snapshot tests - N/A (assertions only, no source generator changes)
Test coverage - 63 new tests covering Memory, Set, and Dictionary assertions

Verdict

APPROVE - No critical issues. The minor allocation in Memory.AsEnumerable() is acceptable as it's a fallback path and the optimized paths avoid it entirely.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an adapter-based architecture for collection assertions, enabling first-class support for Memory, ReadOnlyMemory, and set types (ISet, IReadOnlySet, HashSet), along with enhanced dictionary assertions.

Key Changes

  • New trait-based adapter pattern for unified collection handling
  • Zero-allocation struct adapters for performance
  • Centralized assertion logic in CollectionChecks class
  • Memory/ReadOnlyMemory support with 23 new tests
  • Set-specific operations (subset, superset, overlaps) with 23 new tests
  • Enhanced dictionary methods (ContainsValue, AllKeys, AnyValue) with 31 total tests

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
ICollectionAdapter.cs Defines trait interfaces (IItemSequence, ICountable, IContainsCheck, ISetOperations) and composite adapter interfaces
MemoryAdapter.cs Struct adapters for Memory and ReadOnlyMemory with indexed access and optimized Contains
SetAdapter.cs Struct adapters for ISet and IReadOnlySet with set operation delegates
EnumerableAdapter.cs Generic adapter for IEnumerable with optimizations for known collection types
DictionaryAdapter.cs Struct adapters for IDictionary and IReadOnlyDictionary with key-value operations
CollectionChecks.cs Centralized assertion logic for all collection types (single source of truth)
MemoryAssertionBase.cs Base class providing collection-like assertions for Memory types
MemoryAssertion.cs Entry points for Memory and ReadOnlyMemory assertions
SetAssertionBase.cs Base class with set-specific methods (IsSubsetOf, Overlaps, etc.)
SetAssertion.cs Entry points for ISet, IReadOnlySet, and HashSet
DictionaryAssertionBase.cs Enhanced with ContainsValue, AllKeys/Values, AnyKey/Value methods
Assert.cs New overloads with OverloadResolutionPriority for Memory and Set types
CollectionAssertions.cs Refactored to delegate to CollectionChecks
DictionaryAssertions.cs New assertion classes for enhanced dictionary methods
SetAssertions.cs Assertion classes for all set operations
MemoryAssertions.cs Assertion classes for Memory-specific operations including count comparisons
Test files Comprehensive test coverage for all new functionality

Comment on lines +290 to +301
var seen = new HashSet<TItem>(comparer);
var index = 0;

foreach (var item in adapter.AsEnumerable())
{
if (!seen.Add(item))
{
return AssertionResult.Failed($"duplicate item found at index {index}: {item}");
}
index++;
}

Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method CheckHasDistinctItems in this updated implementation does not provide detailed error information about which items are duplicates or how many duplicates exist. The original implementation collected all duplicates and reported the total count along with the duplicate items themselves. The new implementation only reports the first duplicate found, which provides less helpful diagnostic information for test failures.

Suggested change
var seen = new HashSet<TItem>(comparer);
var index = 0;
foreach (var item in adapter.AsEnumerable())
{
if (!seen.Add(item))
{
return AssertionResult.Failed($"duplicate item found at index {index}: {item}");
}
index++;
}
var counts = new Dictionary<TItem, int>(comparer);
foreach (var item in adapter.AsEnumerable())
{
if (counts.TryGetValue(item, out var currentCount))
{
counts[item] = currentCount + 1;
}
else
{
counts[item] = 1;
}
}
var duplicateItems = new List<TItem>();
foreach (var kvp in counts)
{
if (kvp.Value > 1)
{
duplicateItems.Add(kvp.Key);
}
}
if (duplicateItems.Count > 0)
{
var duplicatesPreview = string.Join(", ", duplicateItems);
return AssertionResult.Failed(
$"collection contains {duplicateItems.Count} duplicate item(s): [{duplicatesPreview}]");
}

Copilot uses AI. Check for mistakes.
}
}

return AssertionResult.Failed($"the item was not found in the collection");
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "the item was not found in the collection" is inconsistent with error messages from other similar assertions. For example, CheckIsNotEmpty returns "it was empty" which is more conversational. Consider making error messages consistent across all collection checks, either using "it" or explicit references to "the collection".

Copilot uses AI. Check for mistakes.
public int Length => _source.Length;

/// <inheritdoc />
public IEnumerable<TItem> AsEnumerable() => _source.ToArray();
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AsEnumerable() method allocates a new array for every call on Memory types. This creates unnecessary allocations in hot paths when the same memory needs to be enumerated multiple times (e.g., in chained assertions or when error messages need to display items). Consider caching or using MemoryMarshal.ToEnumerable if available, or document this allocation cost clearly.

Copilot uses AI. Check for mistakes.

/// <inheritdoc />
public bool IsSupersetOf(IEnumerable<TItem> other)
=> _source?.IsSupersetOf(other) ?? !other.Any(); // Empty set is only superset of empty
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the SetAdapter, when checking IsSupersetOf with a null source, the implementation returns !other.Any(). However, this eagerly evaluates the entire other collection just to check if it's empty. Consider using other is ICollection c ? c.Count == 0 : !other.Any() for better performance when other is a collection type.

Suggested change
=> _source?.IsSupersetOf(other) ?? !other.Any(); // Empty set is only superset of empty
=> _source?.IsSupersetOf(other) ??
(other is System.Collections.ICollection c ? c.Count == 0 : !other.Any()); // Empty set is only superset of empty

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +99
foreach (var element in _source)
{
if (comparer.Equals(element, item))
{
return true;
}
}
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +205 to +211
foreach (var item in adapter.AsEnumerable())
{
if (predicate(item))
{
return AssertionResult.Passed;
}
}
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +421 to +427
foreach (var key in adapter.Keys)
{
if (!predicate(key))
{
return AssertionResult.Failed($"key [{key}] does not satisfy the predicate");
}
}
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +439 to +445
foreach (var value in adapter.Values)
{
if (!predicate(value))
{
return AssertionResult.Failed($"value [{value}] does not satisfy the predicate");
}
}
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +457 to +463
foreach (var key in adapter.Keys)
{
if (predicate(key))
{
return AssertionResult.Passed;
}
}
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +475 to +481
foreach (var value in adapter.Values)
{
if (predicate(value))
{
return AssertionResult.Passed;
}
}
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Updates the TUnit.Assertions public API snapshots to include:
- New Abstractions namespace (ICollectionAdapter, ISetAdapter, etc.)
- New Adapters namespace (EnumerableAdapter, MemoryAdapter, etc.)
- New Collections namespace (CollectionChecks)
- New Memory/Set assertion classes and methods
- New dictionary assertion methods

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@thomhurst thomhurst linked an issue Jan 3, 2026 that may be closed by this pull request
1 task
@thomhurst
Copy link
Owner Author

Summary

Adds adapter-based architecture for collection assertions with Memory, ReadOnlyMemory, and Set-specific assertion support.

Critical Issues

None found ✅

Suggestions

Performance: GetPreview allocation for Memory types (minor)

In CollectionChecks.GetPreview() calling adapter.AsEnumerable() on Memory adapters triggers .ToArray() allocation even when only previewing first N items. This is only called in error paths (not hot paths), so impact is minimal. Optional optimization: Check if adapter implements IIndexable and use indexed access to avoid the full array copy.

Architecture: Excellent use of struct adapters

✅ Zero-allocation struct adapters (MemoryAdapter, SetAdapter, DictionaryAdapter)
✅ Trait-based interfaces (ICountable, IContainsCheck, ISetOperations) for clean separation
✅ Centralized assertion logic in CollectionChecks
✅ Proper null handling with sensible defaults (e.g., empty set is subset of anything)

Testing: Comprehensive coverage

✅ 23 tests for Memory assertions
✅ 23 tests for Set assertions
✅ 31 tests for Dictionary including new methods
✅ Proper conditional compilation for platform-specific types

TUnit Rule Compliance

✅ Rule 1 (Dual-Mode): N/A - Only affects assertion library, not test discovery/engine
✅ Rule 2 (Snapshot Testing): All 4 .verified.txt files properly updated (no .received.txt committed)
✅ Rule 3 (No VSTest): No Microsoft.VisualStudio.TestPlatform usage detected
✅ Rule 4 (Performance): Struct adapters minimize allocations, Contains() uses Span for Memory types, minor GetPreview allocation in error paths only (acceptable)
✅ Rule 5 (AOT Compatible): No reflection usage requiring annotations

Verdict

✅ APPROVE - Excellent architectural implementation with comprehensive testing and proper TUnit compliance. The GetPreview optimization is optional and does not block merge.

Adds first-class assertion support for mutable dictionaries (IDictionary<TKey, TValue>)
in addition to the existing IReadOnlyDictionary support.

## New Types
- MutableDictionaryAssertionBase<TDictionary, TKey, TValue>
- MutableDictionaryAssertion<TKey, TValue>
- MutableDictionaryAndContinuation / MutableDictionaryOrContinuation
- 9 assertion classes: ContainsKey, DoesNotContainKey, ContainsValue, etc.

## New Assert.That Overload
- Assert.That(IDictionary<TKey, TValue>) now returns MutableDictionaryAssertion

## Tests
- Added 13 tests for IDictionary assertions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@thomhurst
Copy link
Owner Author

Summary

Adds first-class assertion support for Memory, ReadOnlyMemory, and Set types via a new adapter-based architecture.

Critical Issues

None found ✅

Suggestions

1. Consider Memory.Span optimizations (Performance)

For Memory and ReadOnlyMemory, you could potentially optimize Contains/Any/All checks by using .Span directly instead of enumerating, which would be even more allocation-free:

// In MemoryAdapter.cs
public bool Contains(TItem item, IEqualityComparer<TItem>? comparer = null)
{
    return comparer == null 
        ? _memory.Span.Contains(item)  // Use Span.Contains directly
        : AsEnumerable().Contains(item, comparer);
}

This would avoid the enumeration overhead for the default comparer case. Similar optimization could apply to All, Any, and other operations.

2. NET5_0_OR_GREATER conditional compilation

The MemoryAssertionTests.cs is wrapped in #if NET5_0_OR_GREATER but Memory was introduced in .NET Standard 2.1 / .NET Core 2.1. Consider whether this should be:

  • #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER if you want broader support
  • Keep as-is if TUnit targets only .NET 5+ for Memory assertions

This is more a question about your target framework strategy than a bug.

TUnit Rules Compliance ✅

  • Dual-mode: N/A (assertions only, no source-gen/engine changes)
  • Snapshot testing: Correctly updated .verified.txt files for all frameworks, no .received.txt committed
  • No VSTest: Pure assertion code, no test platform dependencies
  • Performance First: Struct adapters used for zero allocation
  • AOT Compatible: No reflection usage, uses AOT-friendly generics and Memory types
  • Modern C#: Proper async/await, no blocking, modern syntax
  • No blocking on async: All async methods properly awaited

Verdict

APPROVE - Well-designed architecture that aligns perfectly with TUnit's principles. The adapter pattern is clean and extensible. Suggestions above are optional optimizations only.

@thomhurst thomhurst changed the title feat: add Memory<T>, ReadOnlyMemory<T>, and Set assertions support feat: add collection assertions for Memory, Set, Dictionary, List, ReadOnlyList, and AsyncEnumerable types Jan 3, 2026
…on support

This commit extends the collection assertion architecture with:

IList<T> Support:
- ListAdapter<TItem> - zero-allocation struct adapter
- Index-based methods: HasItemAt, ItemAt, FirstItem, LastItem
- Type-preserving And/Or continuations
- 30 tests in ListAssertionTests.cs

IReadOnlyList<T> Support:
- ReadOnlyListAdapter<TItem> - zero-allocation struct adapter
- Same index-based methods as IList<T>
- Separate overload with priority 3 (IList has priority 4)
- 27 tests in ReadOnlyListAssertionTests.cs

IAsyncEnumerable<T> Support:
- Automatic materialization to List<T> for assertion evaluation
- Full collection methods: IsEmpty, IsNotEmpty, HasCount, Contains, All, Any
- Type-preserving And/Or continuations
- 19 tests in AsyncEnumerableAssertionTests.cs

Overload Resolution Priority:
- Array: 5 (most specific)
- IList<T>: 4 (mutable + indexed)
- IReadOnlyList<T>: 3 (read-only indexed)
- ISet<T>: 2 (set operations)
- IEnumerable<T>: 1 (fallback)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@thomhurst
Copy link
Owner Author

Summary

Adds adapter-based architecture for collection assertions with first-class support for Memory, ReadOnlyMemory, ISet, IDictionary<TKey, TValue>, IList, IReadOnlyList, and IAsyncEnumerable.

Critical Issues

None found ✅

Previous Review Status

Three previous reviews by @thomhurst (PR author) have already approved this PR with no critical issues:

  • All noted the excellent architectural design with struct adapters
  • Minor performance suggestion about Memory.AsEnumerable() allocation (acceptable as it's in error/fallback paths)
  • Optional optimization suggestions for Span usage (not blocking)

All previous reviewers approved the PR.

TUnit Rules Compliance

Rule 1 (Dual-Mode): N/A - Changes only affect TUnit.Assertions, not core engine or source generator
Rule 2 (Snapshot Testing): All 4 .verified.txt files properly updated, no .received.txt committed
Rule 3 (No VSTest): No Microsoft.VisualStudio.TestPlatform usage
Rule 4 (Performance First):

  • Zero-allocation struct adapters (MemoryAdapter, SetAdapter, DictionaryAdapter, etc.)
  • Memory.Span operations for Contains checks
  • Minor allocation in AsEnumerable() is acceptable (error/fallback path only)
    Rule 5 (AOT Compatible):
  • No reflection usage requiring [DynamicallyAccessedMembers]
  • Only uses GetType() for error messages (AOT-safe)
  • Uses modern Memory, Span, and generic constraints (all AOT-safe)
    Modern C#: Proper async/await, no blocking, modern syntax
    Test Coverage: 153+ new tests across 6 test files

Architecture Highlights

Excellent design with:

  • Trait-based interfaces (ICountable, IContainsCheck, ISetOperations, IIndexable)
  • Struct adapters for zero allocations
  • Centralized logic in CollectionChecks
  • Type-preserving chains (And/Or maintain specific collection type)
  • Proper overload resolution priority (Array > IList > IReadOnlyList > ISet > IEnumerable)

Verdict

APPROVE - Excellent implementation that perfectly aligns with TUnit's critical rules and design principles. The adapter pattern is well-designed, extensible, and performance-conscious.

@thomhurst thomhurst merged commit 7967e1c into main Jan 4, 2026
12 of 13 checks passed
@thomhurst thomhurst deleted the feature/assertion-intellisense-v2 branch January 4, 2026 00:08
This was referenced Jan 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Support ReadOnlyMemory assertions [Feature]: Support IDictionary assertions

2 participants