/
EnumerableEquivalencyValidator.cs
157 lines (134 loc) · 5.96 KB
/
EnumerableEquivalencyValidator.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions.Execution;
namespace FluentAssertions.Equivalency
{
/// <summary>
/// Executes a single equivalency assertion on two collections, optionally recursive and with or without strict ordering.
/// </summary>
internal class EnumerableEquivalencyValidator
{
#region Private Definitions
private readonly IEquivalencyValidator parent;
private readonly IEquivalencyValidationContext context;
#endregion
public EnumerableEquivalencyValidator(IEquivalencyValidator parent, IEquivalencyValidationContext context)
{
this.parent = parent;
this.context = context;
Recursive = false;
}
public bool Recursive { get; set; }
public OrderingRuleCollection OrderingRules { get; set; }
public void Execute<T>(object[] subject, T[] expectation)
{
if (AssertIsNotNull(expectation, subject) && AssertCollectionsHaveSameCount(subject, expectation))
{
if (Recursive)
{
using (context.TraceBlock(path => $"Structurally comparing {subject} and expectation {expectation} at {path}"))
{
AssertElementGraphEquivalency(subject, expectation);
}
}
else
{
using (context.TraceBlock(path => $"Comparing subject {subject} and expectation {expectation} at {path} using simple value equality"))
{
subject.Should().BeEquivalentTo(expectation);
}
}
}
}
private bool AssertIsNotNull(object expectation, object[] subject)
{
return AssertionScope.Current
.ForCondition(!(expectation is null))
.FailWith("Expected {context:subject} to be <null>, but found {0}.", new object[] { subject });
}
private static Continuation AssertCollectionsHaveSameCount<T>(ICollection<object> subject, ICollection<T> expectation)
{
return AssertionScope.Current
.WithExpectation("Expected {context:subject} to be a collection with {0} item(s){reason}", expectation.Count)
.AssertEitherCollectionIsNotEmpty(subject, expectation)
.Then
.AssertCollectionHasEnoughItems(subject, expectation)
.Then
.AssertCollectionHasNotTooManyItems(subject, expectation)
.Then
.ClearExpectation();
}
private void AssertElementGraphEquivalency<T>(object[] subjects, T[] expectations)
{
unmatchedSubjectIndexes = new List<int>(subjects.Length);
unmatchedSubjectIndexes.AddRange(Enumerable.Range(0, subjects.Length));
foreach (int index in Enumerable.Range(0, expectations.Length))
{
T expectation = expectations[index];
if (!OrderingRules.IsOrderingStrictFor(context))
{
using (context.TraceBlock(path => $"Finding the best match of {expectation} within all items in {subjects} at {path}[{index}]"))
{
LooselyMatchAgainst(subjects, expectation, index);
}
}
else
{
using (context.TraceBlock(path => $"Strictly comparing expectation {expectation} at {path} to item with index {index} in {subjects}"))
{
StrictlyMatchAgainst(subjects, expectation, index);
}
}
}
}
private List<int> unmatchedSubjectIndexes;
private void LooselyMatchAgainst<T>(IList<object> subjects, T expectation, int expectationIndex)
{
var results = new AssertionResultSet();
int index = 0;
GetTraceMessage getMessage = path => $"Comparing subject at {path}[{index}] with the expectation at {path}[{expectationIndex}]";
int indexToBeRemoved = -1;
for (var metaIndex = 0; metaIndex < unmatchedSubjectIndexes.Count; metaIndex++)
{
index = unmatchedSubjectIndexes[metaIndex];
object subject = subjects[index];
using (context.TraceBlock(getMessage))
{
string[] failures = TryToMatch(subject, expectation, expectationIndex);
results.AddSet(index, failures);
if (results.ContainsSuccessfulSet())
{
context.TraceSingle(_ => "It's a match");
indexToBeRemoved = metaIndex;
break;
}
else
{
context.TraceSingle(_ => $"Contained {failures.Length} failures");
}
}
}
if (indexToBeRemoved != -1)
{
unmatchedSubjectIndexes.RemoveAt(indexToBeRemoved);
}
foreach (string failure in results.SelectClosestMatchFor(expectationIndex))
{
AssertionScope.Current.AddPreFormattedFailure(failure);
}
}
private string[] TryToMatch<T>(object subject, T expectation, int expectationIndex)
{
using (var scope = new AssertionScope())
{
parent.AssertEqualityUsing(context.CreateForCollectionItem(expectationIndex.ToString(), subject, expectation));
return scope.Discard();
}
}
private void StrictlyMatchAgainst<T>(object[] subjects, T expectation, int expectationIndex)
{
parent.AssertEqualityUsing(context.CreateForCollectionItem(expectationIndex.ToString(), subjects[expectationIndex], expectation));
}
}
}