Skip to content

Commit

Permalink
IStructuralEquatable takes priority over IEnumerable using NUnit Equa…
Browse files Browse the repository at this point in the history
…lity.
  • Loading branch information
stevenaw committed Dec 31, 2020
1 parent 58e63ad commit bf315c4
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 19 deletions.
Expand Up @@ -22,10 +22,7 @@
// ***********************************************************************

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using NUnit.Compatibility;

namespace NUnit.Framework.Constraints.Comparers
{
Expand All @@ -49,10 +46,6 @@ internal EquatablesComparer(NUnitEqualityComparer equalityComparer)
Type xType = x.GetType();
Type yType = y.GetType();

if (x is IEnumerable && DoesUseStructuralEquality(xType)
&& y is IEnumerable && DoesUseStructuralEquality(yType))
return null;

MethodInfo equals = FirstImplementsIEquatableOfSecond(xType, yType);
if (equals != null)
return InvokeFirstIEquatableEqualsSecond(x, y, equals);
Expand All @@ -64,18 +57,6 @@ internal EquatablesComparer(NUnitEqualityComparer equalityComparer)
return null;
}

private static bool DoesUseStructuralEquality(Type type)
{
if (!type.IsValueType)
return false;

foreach(var @interface in type.GetInterfaces())
if (@interface.FullName == "System.Collections.IStructuralComparable" || @interface.FullName == "System.Collections.IStructuralEquatable")
return true;

return false;
}

private static MethodInfo FirstImplementsIEquatableOfSecond(Type first, Type second)
{
var mostDerived = default(EquatableMethodImpl);
Expand Down
@@ -0,0 +1,93 @@
// ***********************************************************************
// Copyright (c) 2020 Charlie Poole, Rob Prouse
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ***********************************************************************

#if !NET35
using System.Collections;

namespace NUnit.Framework.Constraints.Comparers
{
/// <summary>
/// Comparator for two types related by <see cref="IStructuralEquatable"/>.
/// </summary>
class StructuralComparer : IChainComparer
{
private readonly NUnitEqualityComparer _equalityComparer;

internal StructuralComparer(NUnitEqualityComparer equalityComparer)
{
_equalityComparer = equalityComparer;
}

public bool? Equal(object x, object y, ref Tolerance tolerance, ComparisonState state)
{
if (_equalityComparer.CompareAsCollection && state.TopLevelComparison)
return null;

if (x is IStructuralEquatable xEquatable && y is IStructuralEquatable yEquatable)
{
// We can't pass tolerance as a ref so we pass by value and reassign later
var equalityComparison = new NUnitEqualityComparison(_equalityComparer, tolerance);

// Check both directions in case they are different implementations, and only one is aware of the other.
// Like how ImmutableArray<int> is structurally equatable to int[]
// but int[] is NOT structurally equatable to ImmutableArray<int>
var xResult = xEquatable.Equals(y, equalityComparison);
var yResult = yEquatable.Equals(x, equalityComparison);

// Keep all the refs up to date
tolerance = equalityComparison.Tolerance;

return xResult || yResult;
}

return null;
}

private sealed class NUnitEqualityComparison : IEqualityComparer
{
private readonly NUnitEqualityComparer _comparer;

private Tolerance _tolerance;
public Tolerance Tolerance { get { return _tolerance; } }

public NUnitEqualityComparison(NUnitEqualityComparer comparer, Tolerance tolerance)
{
_comparer = comparer;
_tolerance = tolerance;
}

public new bool Equals(object x, object y)
{
return _comparer.AreEqual(x, y, ref _tolerance);
}

public int GetHashCode(object obj)
{
// TODO: Better hashcode generation, likely based on the corresponding comparer to ensure types which can be
// compared with each other end up in the same bucket
return 0;
}
}
}
}
#endif
Expand Up @@ -83,6 +83,9 @@ public NUnitEqualityComparer()
new TimeSpanToleranceComparer(),
new TupleComparer(this),
new ValueTupleComparer(this),
#if !NET35
new StructuralComparer(this),
#endif
new EquatablesComparer(this),
enumerablesComparer
};
Expand Down
4 changes: 4 additions & 0 deletions src/NUnitFramework/framework/nunit.framework.csproj
Expand Up @@ -11,6 +11,10 @@
<PackageReference Include="jnm2.ReferenceAssemblies.net35" Version="1.0.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net35' and '$(TargetFramework)' != 'net40'">
<PackageReference Include="System.Runtime" version="4.3.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
</ItemGroup>
Expand Down
36 changes: 36 additions & 0 deletions src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs
Expand Up @@ -109,6 +109,42 @@ public void HonorsIgnoreCase(IEnumerable expected, IEnumerable actual)
};

#if !(NET35 || NET40)
[Test]
[DefaultFloatingPointTolerance(0.5)]
public void StructuralComparerOnSameCollection_RespectsAndSetsToleranceByRef()
{
var integerTypes = ImmutableArray.Create<int>(1);
var floatingTypes = ImmutableArray.Create<double>(1.1);

var equalsConstraint = Is.EqualTo(floatingTypes);
var originalTolerance = equalsConstraint.Tolerance;

Assert.That(integerTypes, equalsConstraint);

Assert.That(equalsConstraint.Tolerance, Is.Not.EqualTo(originalTolerance));
Assert.That(equalsConstraint.Tolerance.Mode, Is.Not.EqualTo(originalTolerance.Mode));
}

[Test]
public void StructuralComparerOnSameCollection_OfDifferentUnderlyingType_UsesNUnitComparer()
{
var integerTypes = ImmutableArray.Create<int>(1);
var floatingTypes = ImmutableArray.Create<double>(1.1);

Assert.That(integerTypes, Is.Not.EqualTo(floatingTypes));
Assert.That(integerTypes, Is.EqualTo(floatingTypes).Within(0.5));
}

[Test]
public void StructuralComparerOnDifferentCollection_OfDifferentUnderlyingType_UsesNUnitComparer()
{
var integerTypes = ImmutableArray.Create<int>(1);
var floatingTypes = new double[] { 1.1 };

Assert.That(integerTypes, Is.Not.EqualTo(floatingTypes));
Assert.That(integerTypes, Is.EqualTo(floatingTypes).Within(0.5));
}

[TestCaseSource(nameof(GetImmutableCollectionsData))]
public void ImmutableCollectionsEquals(object x, object y)
{
Expand Down

0 comments on commit bf315c4

Please sign in to comment.