Skip to content

Commit

Permalink
Merge pull request #3646 from stevenaw/3447-structural-equality
Browse files Browse the repository at this point in the history
IStructuralEquatable support
  • Loading branch information
rprouse committed Jan 2, 2021
2 parents c6d3ff1 + bf315c4 commit a26b9e5
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 2 deletions.
Expand Up @@ -22,9 +22,7 @@
// ***********************************************************************

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

namespace NUnit.Framework.Constraints.Comparers
{
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
72 changes: 72 additions & 0 deletions src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs
Expand Up @@ -24,6 +24,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
#if !(NET35 || NET40)
using System.Collections.Immutable;
#endif
using System.Linq;
using NUnit.Framework.Internal;
using NUnit.TestUtilities.Collections;
Expand Down Expand Up @@ -104,5 +107,74 @@ public void HonorsIgnoreCase(IEnumerable expected, IEnumerable actual)
new object[] {new List<char> {'A', 'B', 'C'}, new List<char> {'a', 'b', 'c'}},
new object[] {new List<string> {"a", "b", "c"}, new List<string> {"A", "B", "C"}},
};

#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)
{
Assert.That(x, Is.EqualTo(y));
}

private static IEnumerable<object> GetImmutableCollectionsData()
{
var data = new[] { 1, 2, 3 };
var immutableDataGenerators = new Func<IEnumerable<int>>[]
{
() => ImmutableArray.Create(data),
() => ImmutableList.Create(data),
() => ImmutableQueue.Create(data),
() => ImmutableStack.Create(data.Reverse().ToArray()),
() => new List<int>(data),
() => data
};

for (var i = 0; i < immutableDataGenerators.Length; i++)
{
for (var j = i; j < immutableDataGenerators.Length; j++)
{
var x = immutableDataGenerators[i]();
var y = immutableDataGenerators[j]();

yield return new object[] { x, y };
}
}
}
#endif
}
}
1 change: 1 addition & 0 deletions src/NUnitFramework/tests/nunit.framework.tests.csproj
Expand Up @@ -44,6 +44,7 @@

<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
Expand Down

0 comments on commit a26b9e5

Please sign in to comment.