From bf315c4011c70f0d84b6150ee70a898f364a20a4 Mon Sep 17 00:00:00 2001 From: stevenaw Date: Tue, 29 Dec 2020 21:04:31 -0500 Subject: [PATCH] IStructuralEquatable takes priority over IEnumerable using NUnit Equality. --- .../Comparers/EquatablesComparer.cs | 19 ---- .../Comparers/StructuralComparer.cs | 93 +++++++++++++++++++ .../Constraints/NUnitEqualityComparer.cs | 3 + .../framework/nunit.framework.csproj | 4 + .../Constraints/CollectionEqualsTests.cs | 36 +++++++ 5 files changed, 136 insertions(+), 19 deletions(-) create mode 100644 src/NUnitFramework/framework/Constraints/Comparers/StructuralComparer.cs diff --git a/src/NUnitFramework/framework/Constraints/Comparers/EquatablesComparer.cs b/src/NUnitFramework/framework/Constraints/Comparers/EquatablesComparer.cs index 01fe405ce9..39dbe68761 100644 --- a/src/NUnitFramework/framework/Constraints/Comparers/EquatablesComparer.cs +++ b/src/NUnitFramework/framework/Constraints/Comparers/EquatablesComparer.cs @@ -22,10 +22,7 @@ // *********************************************************************** using System; -using System.Collections; -using System.Collections.Generic; using System.Reflection; -using NUnit.Compatibility; namespace NUnit.Framework.Constraints.Comparers { @@ -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); @@ -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); diff --git a/src/NUnitFramework/framework/Constraints/Comparers/StructuralComparer.cs b/src/NUnitFramework/framework/Constraints/Comparers/StructuralComparer.cs new file mode 100644 index 0000000000..290f6a0d07 --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/Comparers/StructuralComparer.cs @@ -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 +{ + /// + /// Comparator for two types related by . + /// + 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 is structurally equatable to int[] + // but int[] is NOT structurally equatable to ImmutableArray + 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 diff --git a/src/NUnitFramework/framework/Constraints/NUnitEqualityComparer.cs b/src/NUnitFramework/framework/Constraints/NUnitEqualityComparer.cs index efc24ce9a7..e9d998da34 100644 --- a/src/NUnitFramework/framework/Constraints/NUnitEqualityComparer.cs +++ b/src/NUnitFramework/framework/Constraints/NUnitEqualityComparer.cs @@ -83,6 +83,9 @@ public NUnitEqualityComparer() new TimeSpanToleranceComparer(), new TupleComparer(this), new ValueTupleComparer(this), +#if !NET35 + new StructuralComparer(this), +#endif new EquatablesComparer(this), enumerablesComparer }; diff --git a/src/NUnitFramework/framework/nunit.framework.csproj b/src/NUnitFramework/framework/nunit.framework.csproj index 4d028a0282..4926ad1257 100644 --- a/src/NUnitFramework/framework/nunit.framework.csproj +++ b/src/NUnitFramework/framework/nunit.framework.csproj @@ -11,6 +11,10 @@ + + + + diff --git a/src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs b/src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs index c65edb1d77..415efd42a0 100644 --- a/src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs +++ b/src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs @@ -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(1); + var floatingTypes = ImmutableArray.Create(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(1); + var floatingTypes = ImmutableArray.Create(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(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) {