Skip to content

Commit 7cd42e3

Browse files
Merge pull request nunit#4608 from manfred-brands/issues/4572_PropertiesComparer
Issues/4572 properties comparer
2 parents 0295aac + ef78149 commit 7cd42e3

File tree

10 files changed

+200
-21
lines changed

10 files changed

+200
-21
lines changed

src/NUnitFramework/framework/Constraints/AnyOfConstraint.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,19 @@ public AnyOfConstraint Using<T>(Func<T, T, bool> comparer)
130130
return this;
131131
}
132132

133+
/// <summary>
134+
/// Enables comparing of instance properties.
135+
/// </summary>
136+
/// <remarks>
137+
/// This allows comparing classes that don't implement <see cref="IEquatable{T}"/>
138+
/// without having to compare each property separately in own code.
139+
/// </remarks>
140+
public AnyOfConstraint UsingPropertiesComparer()
141+
{
142+
_comparer.CompareProperties = true;
143+
return this;
144+
}
145+
133146
#endregion
134147
}
135148
}

src/NUnitFramework/framework/Constraints/CollectionItemsEqualConstraint.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ internal CollectionItemsEqualConstraint Using(EqualityAdapter adapter)
127127
return this;
128128
}
129129

130+
/// <summary>
131+
/// Enables comparing of instance properties.
132+
/// </summary>
133+
/// <remarks>
134+
/// This allows comparing classes that don't implement <see cref="IEquatable{T}"/>
135+
/// without having to compare each property separately in own code.
136+
/// </remarks>
137+
public CollectionItemsEqualConstraint UsingPropertiesComparer()
138+
{
139+
_comparer.CompareProperties = true;
140+
return this;
141+
}
142+
130143
#endregion
131144

132145
/// <summary>

src/NUnitFramework/framework/Constraints/Comparers/EqualMethodResult.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ internal enum EqualMethodResult
2323
ComparedEqual,
2424

2525
/// <summary>
26-
/// Method is appropriate and the items are consisdered different.
26+
/// Method is appropriate and the items are considered different.
2727
/// </summary>
28-
ComparedNotEqual
28+
ComparedNotEqual,
29+
30+
/// <summary>
31+
/// Method is appropriate but the class has cyclic references.
32+
/// </summary>
33+
ComparisonPending
2934
}
3035
}

src/NUnitFramework/framework/Constraints/Comparers/PropertiesComparer.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
22

33
using System;
4+
using System.Linq;
45
using System.Reflection;
56

67
namespace NUnit.Framework.Constraints.Comparers
@@ -29,9 +30,10 @@ public static EqualMethodResult Equal(object x, object y, ref Tolerance toleranc
2930
}
3031

3132
PropertyInfo[] properties = xType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
32-
if (properties.Length == 0 || properties.Length >= 32)
33+
if (properties.Length == 0 || properties.Length >= 32 || properties.Any(p => p.GetIndexParameters().Length > 0))
3334
{
3435
// We can't compare if there are no (or too many) properties.
36+
// We also can't deal with indexer properties as we don't know the range of valid values.
3537
return EqualMethodResult.TypesNotSupported;
3638
}
3739

src/NUnitFramework/framework/Constraints/EqualConstraint.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,6 @@ public EqualConstraint Ticks
268268
return this;
269269
}
270270
}
271-
272271
/// <summary>
273272
/// Flag the constraint to use the supplied IComparer object.
274273
/// </summary>
@@ -346,6 +345,19 @@ public EqualConstraint Using<TCollectionType, TMemberType>(Func<TCollectionType,
346345
return this;
347346
}
348347

348+
/// <summary>
349+
/// Enables comparing of instance properties.
350+
/// </summary>
351+
/// <remarks>
352+
/// This allows comparing classes that don't implement <see cref="IEquatable{T}"/>
353+
/// without having to compare each property separately in own code.
354+
/// </remarks>
355+
public EqualConstraint UsingPropertiesComparer()
356+
{
357+
_comparer.CompareProperties = true;
358+
return this;
359+
}
360+
349361
#endregion
350362

351363
#region Public Methods

src/NUnitFramework/framework/Constraints/NUnitEqualityComparer.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ public sealed class NUnitEqualityComparer
5151
EquatablesComparer.Equal,
5252
EnumerablesComparer.Equal,
5353
EqualsComparer.Equal,
54-
PropertiesComparer.Equal,
5554
};
5655

5756
/// <summary>
@@ -65,6 +64,12 @@ public sealed class NUnitEqualityComparer
6564
/// </summary>
6665
private bool _compareAsCollection;
6766

67+
/// <summary>
68+
/// If true, when a class does not implement <see cref="IEquatable{T}"/>
69+
/// it will be compared property by property.
70+
/// </summary>
71+
private bool _compareProperties;
72+
6873
/// <summary>
6974
/// Comparison objects used in comparisons for some constraints.
7075
/// </summary>
@@ -96,6 +101,16 @@ public bool IgnoreCase
96101
set => _caseInsensitive = value;
97102
}
98103

104+
/// <summary>
105+
/// Gets and sets a flag indicating whether an instance properties
106+
/// should be compared when determining equality.
107+
/// </summary>
108+
public bool CompareProperties
109+
{
110+
get => _compareProperties;
111+
set => _compareProperties = value;
112+
}
113+
99114
/// <summary>
100115
/// Gets and sets a flag indicating that arrays should be
101116
/// compared as collections, without regard to their shape.
@@ -133,6 +148,7 @@ public bool CompareAsCollection
133148
#endregion
134149

135150
#region Public Methods
151+
136152
/// <summary>
137153
/// Compares two objects for equality within a tolerance.
138154
/// </summary>
@@ -148,6 +164,7 @@ public bool AreEqual(object? x, object? y, ref Tolerance tolerance)
148164
throw new NotSupportedException($"Specified Tolerance not supported for instances of type '{GetType(x)}' and '{GetType(y)}'");
149165
case EqualMethodResult.ComparedEqual:
150166
return true;
167+
case EqualMethodResult.ComparisonPending:
151168
case EqualMethodResult.ComparedNotEqual:
152169
default:
153170
return false;
@@ -170,7 +187,7 @@ internal EqualMethodResult AreEqual(object? x, object? y, ref Tolerance toleranc
170187
return EqualMethodResult.ComparedEqual;
171188

172189
if (state.DidCompare(x, y))
173-
return EqualMethodResult.ComparedNotEqual;
190+
return EqualMethodResult.ComparisonPending;
174191

175192
EqualityAdapter? externalComparer = GetExternalComparer(x, y);
176193

@@ -194,6 +211,11 @@ internal EqualMethodResult AreEqual(object? x, object? y, ref Tolerance toleranc
194211
return result;
195212
}
196213

214+
if (_compareProperties)
215+
{
216+
return PropertiesComparer.Equal(x, y, ref tolerance, state, this);
217+
}
218+
197219
if (tolerance.HasVariance)
198220
return EqualMethodResult.ToleranceNotSupported;
199221

src/NUnitFramework/framework/Constraints/SomeItemsConstraint.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@ public SomeItemsConstraint Using<T>(IEqualityComparer<T> comparer)
127127
return this;
128128
}
129129

130+
/// <summary>
131+
/// Enables comparing of instance properties.
132+
/// </summary>
133+
/// <remarks>
134+
/// This allows comparing classes that don't implement <see cref="IEquatable{T}"/>
135+
/// without having to compare each property separately in own code.
136+
/// </remarks>
137+
public SomeItemsConstraint UsingPropertiesComparer()
138+
{
139+
CheckPrecondition(nameof(UsingPropertiesComparer));
140+
_equalConstraint.UsingPropertiesComparer();
141+
return this;
142+
}
143+
130144
[MemberNotNull(nameof(_equalConstraint))]
131145
private void CheckPrecondition(string argument)
132146
{

src/NUnitFramework/tests/Assertions/AssertThatTests.cs

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -377,13 +377,13 @@ public void AssertThatEqualsWithClassWithSomeToleranceAwareMembers()
377377

378378
Assert.Multiple(() =>
379379
{
380-
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.1, "1.1", zero), Is.EqualTo(instance));
381-
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.2, "1.1", zero), Is.Not.EqualTo(instance));
382-
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.2, "1.1", zero), Is.EqualTo(instance).Within(0.1));
383-
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.1, "1.1", null), Is.Not.EqualTo(instance));
384-
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.1, "2.2", zero), Is.Not.EqualTo(instance));
385-
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 2.2, "1.1", zero), Is.Not.EqualTo(instance));
386-
Assert.That(new ClassWithSomeToleranceAwareMembers(2, 1.1, "1.1", zero), Is.Not.EqualTo(instance));
380+
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.1, "1.1", zero), Is.EqualTo(instance).UsingPropertiesComparer());
381+
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.2, "1.1", zero), Is.Not.EqualTo(instance).UsingPropertiesComparer());
382+
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.2, "1.1", zero), Is.EqualTo(instance).Within(0.1).UsingPropertiesComparer());
383+
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.1, "1.1", null), Is.Not.EqualTo(instance).UsingPropertiesComparer());
384+
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 1.1, "2.2", zero), Is.Not.EqualTo(instance).UsingPropertiesComparer());
385+
Assert.That(new ClassWithSomeToleranceAwareMembers(1, 2.2, "1.1", zero), Is.Not.EqualTo(instance).UsingPropertiesComparer());
386+
Assert.That(new ClassWithSomeToleranceAwareMembers(2, 1.1, "1.1", zero), Is.Not.EqualTo(instance).UsingPropertiesComparer());
387387
});
388388
}
389389

@@ -415,12 +415,12 @@ public void AssertThatEqualsWithStructWithSomeToleranceAwareMembers()
415415

416416
Assert.Multiple(() =>
417417
{
418-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.One), Is.EqualTo(instance));
419-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.2, "1.1", SomeEnum.One), Is.Not.EqualTo(instance));
420-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.2, "1.1", SomeEnum.One), Is.EqualTo(instance).Within(0.1));
421-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.Two), Is.Not.EqualTo(instance).Within(0.1));
422-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 2.2, "1.1", SomeEnum.One), Is.Not.EqualTo(instance));
423-
Assert.That(new StructWithSomeToleranceAwareMembers(2, 1.1, "1.1", SomeEnum.One), Is.Not.EqualTo(instance));
418+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
419+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.2, "1.1", SomeEnum.One), Is.Not.EqualTo(instance).UsingPropertiesComparer());
420+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.2, "1.1", SomeEnum.One), Is.EqualTo(instance).Within(0.1).UsingPropertiesComparer());
421+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.Two), Is.Not.EqualTo(instance).Within(0.1).UsingPropertiesComparer());
422+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 2.2, "1.1", SomeEnum.One), Is.Not.EqualTo(instance).UsingPropertiesComparer());
423+
Assert.That(new StructWithSomeToleranceAwareMembers(2, 1.1, "1.1", SomeEnum.One), Is.Not.EqualTo(instance).UsingPropertiesComparer());
424424
});
425425
}
426426

@@ -526,5 +526,64 @@ public override string ToString()
526526
return $"{ValueA} {ValueB} '{ValueC}' [{Chained}]";
527527
}
528528
}
529+
530+
[Test]
531+
public void AssertWithRecursiveClass()
532+
{
533+
LinkedList list1 = new(1, new(2, new(3)));
534+
LinkedList list2 = new(1, new(2, new(3)));
535+
536+
Assert.That(list1, Is.Not.EqualTo(list2));
537+
Assert.That(list1, Is.EqualTo(list2).UsingPropertiesComparer());
538+
}
539+
540+
[Test]
541+
public void AssertWithCyclicRecursiveClass()
542+
{
543+
LinkedList list1 = new(1);
544+
LinkedList list2 = new(1);
545+
546+
list1.Next = list1;
547+
list2.Next = list2;
548+
549+
Assert.That(list1, Is.Not.EqualTo(list2)); // Reference comparison
550+
Assert.That(list1, Is.EqualTo(list2).UsingPropertiesComparer());
551+
}
552+
553+
private sealed class LinkedList
554+
{
555+
public LinkedList(int value, LinkedList? next = null)
556+
{
557+
Value = value;
558+
Next = next;
559+
}
560+
561+
public int Value { get; }
562+
563+
public LinkedList? Next { get; set; }
564+
}
565+
566+
[Test]
567+
public void EqualMemberWithIndexer()
568+
{
569+
var members = new Members("Hello", "World", "NUnit");
570+
var copy = new Members("Hello", "World", "NUnit");
571+
572+
Assert.That(members[1], Is.EqualTo("World"));
573+
Assert.That(copy, Is.Not.EqualTo(members));
574+
Assert.That(() => Assert.That(copy, Is.EqualTo(members).UsingPropertiesComparer()), Throws.InstanceOf<NotSupportedException>());
575+
}
576+
577+
private sealed class Members
578+
{
579+
private readonly string[] _members;
580+
581+
public Members(params string[] members)
582+
{
583+
_members = members;
584+
}
585+
586+
public string this[int index] => _members[index];
587+
}
529588
}
530589
}

src/NUnitFramework/tests/Constraints/AnyOfConstraintTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,23 @@ public void MissingMember()
6363
{
6464
Assert.That(42, Is.Not.AnyOf(0, -1, 100));
6565
}
66+
67+
[Test]
68+
public void ValidMemberUsingPropertiesComparer()
69+
{
70+
Assert.That(new XY(5, 12), Is.AnyOf(new XY(3, 4), new XY(5, 12)).UsingPropertiesComparer());
71+
}
72+
73+
private sealed class XY
74+
{
75+
public XY(int x, int y)
76+
{
77+
X = x;
78+
Y = y;
79+
}
80+
81+
public int X { get; }
82+
public int Y { get; }
83+
}
6684
}
6785
}

src/NUnitFramework/tests/Constraints/DictionaryContainsValueConstraintTests.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ public void FailsWhenValueIsMissing()
2929
}
3030

3131
[Test]
32-
public void SucceedsWhenValueIsPresentUsingContainKey()
32+
public void SucceedsWhenValueIsPresentUsingContainValue()
3333
{
3434
var dictionary = new Dictionary<string, string> { { "Hello", "World" }, { "Hola", "Mundo" } };
3535
Assert.That(dictionary, Does.ContainValue("Mundo"));
3636
}
3737

3838
[Test]
39-
public void SucceedsWhenValueIsNotPresentUsingContainKey()
39+
public void SucceedsWhenValueIsNotPresentUsingContainValue()
4040
{
4141
var dictionary = new Dictionary<string, string> { { "Hello", "World" }, { "Hola", "Mundo" } };
4242
Assert.That(dictionary, Does.Not.ContainValue("NotValue"));
@@ -77,5 +77,26 @@ public void UsingIsHonored()
7777
Assert.That(dictionary,
7878
new DictionaryContainsValueConstraint("UNIVERSE").Using<string>((x, y) => StringUtil.Compare(x, y, true)));
7979
}
80+
81+
[Test]
82+
public void UsingPropertiesComparerIsHonored()
83+
{
84+
var dictionary = new Dictionary<string, XY> { { "5", new(3, 4) }, { "13", new(5, 12) } };
85+
var value = new XY(5, 12);
86+
Assert.That(dictionary, Does.Not.ContainValue(value));
87+
Assert.That(dictionary, Does.ContainValue(value).UsingPropertiesComparer());
88+
}
89+
90+
private sealed class XY
91+
{
92+
public XY(double x, double y)
93+
{
94+
X = x;
95+
Y = y;
96+
}
97+
98+
public double X { get; }
99+
public double Y { get; }
100+
}
80101
}
81102
}

0 commit comments

Comments
 (0)