Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 15, 2025

Description

IsEquivalentTo fails for collections containing types implementing IEquatable<T> (e.g., Vector2, Uri, CultureInfo). The structural comparer attempts to reflect over properties requiring parameters (like indexers), causing "Parameter count mismatch" exceptions.

// Previously failed with TargetParameterCountException
var array = new Vector2[] { new(1, 2), new(3, 4) };
var list = new List<Vector2>(array);
await Assert.That(array).IsEquivalentTo(list);

// Now uses Vector2.Equals(Vector2) automatically

Solution: Modified TypeHelper.IsPrimitiveOrWellKnownType to detect IEquatable<T> implementations:

  • Value types implementing IEquatable<T>: Always use their Equals method
  • Sealed reference types implementing IEquatable<T>: Use Equals if not compiler-generated
  • Records: Continue using structural comparison (preserve existing behavior)

Implementation details:

  • Added IsCompilerGeneratedType to detect records via EqualityContract property
  • Uses typeof(IEquatable<>) for type-safe interface detection
  • Properly annotated with [DynamicallyAccessedMembers] for AOT/trimming

Related Issue

(Issue linking handled by system)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Checklist

Required

  • I have read the Contributing Guidelines
  • If this is a new feature, I started a discussion first and received agreement
  • My code follows the project's code style (modern C# syntax, proper naming conventions)
  • I have written tests that prove my fix is effective or my feature works

TUnit-Specific Requirements

  • Dual-Mode Implementation: If this change affects test discovery/execution, I have implemented it in BOTH:
    • Source Generator path (TUnit.Core.SourceGenerator) - N/A (assertions only)
    • Reflection path (TUnit.Engine) - N/A (assertions only)
  • Snapshot Tests: If I changed source generator output or public APIs:
    • I ran TUnit.Core.SourceGenerator.Tests and/or TUnit.PublicAPI tests
    • I reviewed the .received.txt files and accepted them as .verified.txt
    • I committed the updated .verified.txt files
  • Performance: If this change affects hot paths (test discovery, execution, assertions):
    • I minimized allocations and avoided LINQ in hot paths
    • I cached reflection results where appropriate
  • AOT Compatibility: If this change uses reflection:
    • I added appropriate [DynamicallyAccessedMembers] annotations
    • I verified the change works with dotnet publish -p:PublishAot=true

Testing

  • All existing tests pass (dotnet test)
  • I have added tests that cover my changes
  • I have tested both source-generated and reflection modes (if applicable)

Additional Notes

Test coverage:

  • IEquatableCollectionTests.cs: Covers Vector2, Vector3, Uri, CultureInfo, DateTimeOffset, custom types
  • IssueReproductionTest.cs: Exact scenario from issue
  • IEquatableWorkaroundTests.cs: Verifies explicit comparer still works

Verified: 1379 tests pass on .NET 8.0 and 9.0. No breaking changes to records or structural comparison behavior.

Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: IsEquivalentTo does not respect IEquatable<T></issue_title>
<issue_description>### Description

When using
await Assert.That(array).IsEquivalentTo(array2);
on a type which implement IEquatable<T>, TUnit does not use the IEquatable<T> to check item is equal or not.

Expected Behavior

Test should pass.

Actual Behavior

Test failed.

Steps to Reproduce

[Test]
public async Task Test1()
{
    var array = new Vector2[]
    {
        new Vector2(1, 2),
        new Vector2(3, 4),
        new Vector2(5, 6),
    };
    var array2 = new List<Vector2>(array);
    
    await Assert.That(array).IsEquivalentTo(array2);
}

TUnit Version

1.5.60

.NET Version

.NET9.0

Operating System

Windows

IDE / Test Runner

JetBrains Rider

Error Output / Stack Trace

Parameter count mismatch.
   at System.Reflection.MethodBaseInvoker.ThrowTargetParameterCountException()
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.PropertyInfo.GetValue(Object obj)
   at TUnit.Assertions.Conditions.Helpers.ReflectionHelper.GetMemberValue(Object obj, MemberInfo member)
   at TUnit.Assertions.Conditions.Helpers.StructuralEqualityComparer`1.CompareStructurally(Object x, Object y, HashSet`1 visited)
   at TUnit.Assertions.Conditions.Helpers.StructuralEqualityComparer`1.Equals(T x, T y)
   at TUnit.Assertions.Conditions.Helpers.CollectionEquivalencyChecker.CheckUnorderedEquivalenceLinear[TItem](List`1 actualList, List`1 expectedList, IEqualityComparer`1 comparer)
   at TUnit.Assertions.Conditions.Helpers.CollectionEquivalencyChecker.CheckUnorderedEquivalence[TItem](List`1 actualList, List`1 expectedList, IEqualityComparer`1 comparer)
   at TUnit.Assertions.Conditions.Helpers.CollectionEquivalencyChecker.AreEquivalent[TItem](IEnumerable`1 actual, IEnumerable`1 expected, CollectionOrdering ordering, IEqualityComparer`1 comparer)
   at TUnit.Assertions.Conditions.IsEquivalentToAssertion`2.CheckAsync(EvaluationMetadata`1 metadata)
   at TUnit.Assertions.Core.Assertion`1.ExecuteCoreAsync()
   at TUnit.Assertions.Core.Assertion`1.AssertAsync()
   at LightProto.AssemblyLevelTests.AssemblyPriorityTests.Test() in D:\repos\LightProto\tests\LightProto.AssemblyLevelTests\AssemblyPriorityTests.cs:line 62
   at TUnit.Core.TestMetadata`1.<>c__DisplayClass13_0.<<get_CreateExecutableTestFactory>b__2>d.MoveNext()
--- End of stack trace from previous location ---
   at TUnit.Core.ExecutableTest.InvokeTestAsync(Object instance, CancellationToken cancellationToken)
   at TUnit.Engine.TestExecutor.ExecuteTestAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken)
   at TUnit.Engine.TestExecutor.ExecuteAsync(AbstractExecutableTest executableTest, TestInitializer testInitializer, CancellationToken cancellationToken)
   at TUnit.Engine.TestExecutor.ExecuteAsync(AbstractExecutableTest executableTest, TestInitializer testInitializer, CancellationToken cancellationToken)
   at TUnit.Engine.Services.TestExecution.TestCoordinator.<>c__DisplayClass12_0.<<ExecuteTestInternalAsync>b__1>d.MoveNext()
--- End of stack trace from previous location ---
   at TUnit.Engine.Services.TestExecution.TestCoordinator.<>c__DisplayClass12_0.<<ExecuteTestInternalAsync>b__1>d.MoveNext()
--- End of stack trace from previous location ---
   at TUnit.Engine.Helpers.TimeoutHelper.ExecuteWithTimeoutAsync(Func`2 taskFactory, Nullable`1 timeout, CancellationToken cancellationToken, String timeoutMessage)
   at TUnit.Engine.Services.TestExecution.TestCoordinator.<>c__DisplayClass12_0.<<ExecuteTestInternalAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at TUnit.Engine.Services.TestExecution.RetryHelper.ExecuteWithRetry(TestContext testContext, Func`1 action)
   at TUnit.Engine.Services.TestExecution.RetryHelper.ExecuteWithRetry(TestContext testContext, Func`1 action)
   at TUnit.Engine.Services.TestExecution.TestCoordinator.ExecuteTestInternalAsync(AbstractExecutableTest test, CancellationToken cancellationToken)

Additional Context

If I change
await Assert.That(array).IsEquivalentTo(array2);
to
await Assert.That(array).IsEquivalentTo(array2,EqualityComparer<Vector2>.Default);
test will pass.

There are a lot of type (Uri,CultureInfo,Vector3,Matrix3x2,etc...) has this issue, not just Vector2.

Additionally, IComparable<T> should be considered as well.

IDE-Specific Issue?

  • I've confirmed this issue occurs when running via dotnet test or dotnet run, not just in my IDE</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Fix IsEquivalentTo not respecting IEquatable<T> Fix IsEquivalentTo to respect IEquatable<T> implementations Dec 15, 2025
Copilot AI requested a review from thomhurst December 15, 2025 13:48
@thomhurst thomhurst closed this Jan 6, 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.

[Bug]: IsEquivalentTo does not respect IEquatable<T>

2 participants