Skip to content

Commit 6b74f1a

Browse files
committed
Add object equivalency with member selection and ordering control
1 parent edb2650 commit 6b74f1a

14 files changed

+421
-1095
lines changed

src/SharpAssert.Runtime/Features/Collections/CollectionExtensions.cs

Lines changed: 0 additions & 118 deletions
This file was deleted.

src/SharpAssert.Runtime/Features/Collections/CollectionOrderingExpectation.cs

Lines changed: 0 additions & 74 deletions
This file was deleted.

src/SharpAssert.Runtime/Features/Collections/CollectionUniquenessExpectation.cs

Lines changed: 0 additions & 65 deletions
This file was deleted.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// ABOUTME: Configuration builder for equivalency assertions.
2+
// ABOUTME: Provides a fluent API for customizing object comparison behavior.
3+
4+
using System.Linq.Expressions;
5+
using KellermanSoftware.CompareNetObjects;
6+
7+
namespace SharpAssert;
8+
9+
public sealed class EquivalencyConfig<T>
10+
{
11+
internal ComparisonConfig ComparisonConfig { get; } = new();
12+
13+
public EquivalencyConfig<T> Excluding(Expression<Func<T, object>> memberSelector)
14+
{
15+
var memberName = GetMemberName(memberSelector);
16+
ComparisonConfig.MembersToIgnore.Add(memberName);
17+
return this;
18+
}
19+
20+
public EquivalencyConfig<T> Including(Expression<Func<T, object>> memberSelector)
21+
{
22+
var memberName = GetMemberName(memberSelector);
23+
ComparisonConfig.MembersToInclude.Add(memberName);
24+
return this;
25+
}
26+
27+
public EquivalencyConfig<T> WithoutStrictOrdering()
28+
{
29+
ComparisonConfig.IgnoreCollectionOrder = true;
30+
return this;
31+
}
32+
33+
static string GetMemberName(Expression<Func<T, object>> expression)
34+
{
35+
if (expression.Body is MemberExpression memberExpr)
36+
return memberExpr.Member.Name;
37+
38+
if (expression.Body is UnaryExpression { Operand: MemberExpression unaryMember })
39+
return unaryMember.Member.Name;
40+
41+
throw new ArgumentException($"Expression '{expression}' does not refer to a property or field.");
42+
}
43+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// ABOUTME: Extension methods for object equivalency assertions.
2+
// ABOUTME: Provides IsEquivalentTo() for deep object comparison with configuration.
3+
4+
namespace SharpAssert;
5+
6+
public static class EquivalencyExtensions
7+
{
8+
public static IsEquivalentToExpectation<T> IsEquivalentTo<T>(this T actual, T expected)
9+
{
10+
return new IsEquivalentToExpectation<T>(actual, expected, new EquivalencyConfig<T>());
11+
}
12+
13+
public static IsEquivalentToExpectation<T> IsEquivalentTo<T>(
14+
this T actual,
15+
T expected,
16+
Func<EquivalencyConfig<T>, EquivalencyConfig<T>> configure)
17+
{
18+
var config = configure(new EquivalencyConfig<T>());
19+
return new IsEquivalentToExpectation<T>(actual, expected, config);
20+
}
21+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// ABOUTME: Expectation for deep object equivalency with configurable comparison rules.
2+
// ABOUTME: Wraps Compare-Net-Objects to provide rich diagnostics on failure.
3+
4+
using KellermanSoftware.CompareNetObjects;
5+
using SharpAssert.Features.Shared;
6+
7+
namespace SharpAssert;
8+
9+
public sealed class IsEquivalentToExpectation<T> : Expectation
10+
{
11+
readonly T actual;
12+
readonly T expected;
13+
readonly EquivalencyConfig<T> config;
14+
15+
internal IsEquivalentToExpectation(T actual, T expected, EquivalencyConfig<T> config)
16+
{
17+
this.actual = actual;
18+
this.expected = expected;
19+
this.config = config;
20+
}
21+
22+
public override EvaluationResult Evaluate(ExpectationContext context)
23+
{
24+
var compareLogic = new CompareLogic(config.ComparisonConfig);
25+
var result = compareLogic.Compare(actual, expected);
26+
27+
if (result.AreEqual)
28+
return ExpectationResults.Pass(context.Expression);
29+
30+
var differences = FormatDifferences(result.Differences);
31+
return ExpectationResults.Fail(context.Expression, differences.ToArray());
32+
}
33+
34+
static List<string> FormatDifferences(IList<Difference> differences)
35+
{
36+
var lines = new List<string>();
37+
38+
if (differences.Count > 0)
39+
{
40+
lines.Add("Object differences:");
41+
foreach (var diff in differences.Take(20))
42+
{
43+
lines.Add($" {diff.PropertyName}: expected {FormatValue(diff.Object2Value)}, got {FormatValue(diff.Object1Value)}");
44+
}
45+
46+
if (differences.Count > 20)
47+
lines.Add($" ... ({differences.Count - 20} more differences)");
48+
}
49+
50+
return lines;
51+
}
52+
53+
static string FormatValue(object? value) => ValueFormatter.Format(value);
54+
}

0 commit comments

Comments
 (0)