-
-
Notifications
You must be signed in to change notification settings - Fork 540
/
GenericDictionaryEquivalencyStep.cs
292 lines (247 loc) · 12.2 KB
/
GenericDictionaryEquivalencyStep.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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using FluentAssertions.Common;
using FluentAssertions.Execution;
namespace FluentAssertions.Equivalency
{
/// <remarks>
/// I think (but did not try) this would have been easier using 'dynamic' but that is
/// precluded by some of the PCL targets.
/// </remarks>
public class GenericDictionaryEquivalencyStep : IEquivalencyStep
{
public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAssertionOptions config)
{
Type expectationType = config.GetExpectationType(context);
return context.Expectation != null && GetIDictionaryInterfaces(expectationType).Any();
}
public bool Handle(IEquivalencyValidationContext context, IEquivalencyValidator parent,
IEquivalencyAssertionOptions config)
{
if (PreconditionsAreMet(context, config))
{
AssertDictionaryEquivalence(context, parent, config);
}
return true;
}
private static Type[] GetIDictionaryInterfaces(Type type)
{
return Common.TypeExtensions.GetClosedGenericInterfaces(
type,
typeof(IDictionary<,>));
}
private static bool PreconditionsAreMet(IEquivalencyValidationContext context, IEquivalencyAssertionOptions config)
{
Type expectationType = config.GetExpectationType(context);
return AssertImplementsOnlyOneDictionaryInterface(context.Expectation)
&& AssertExpectationIsNotNull(context.Subject, context.Expectation)
&& AssertIsCompatiblyTypedDictionary(expectationType, context.Subject)
&& AssertSameLength(context.Subject, expectationType, context.Expectation);
}
private static bool AssertExpectationIsNotNull(object subject, object expectation)
{
return AssertionScope.Current
.ForCondition(!(expectation is null))
.FailWith("Expected {context:Subject} to be {0}, but found {1}.", null, subject);
}
private static bool AssertImplementsOnlyOneDictionaryInterface(object expectation)
{
Type[] interfaces = GetIDictionaryInterfaces(expectation.GetType());
bool multipleInterfaces = interfaces.Length > 1;
if (!multipleInterfaces)
{
return true;
}
AssertionScope.Current.FailWith(
string.Format(
"{{context:Expectation}} implements multiple dictionary types. "
+ "It is not known which type should be use for equivalence.{0}"
+ "The following IDictionary interfaces are implemented: {1}",
Environment.NewLine,
string.Join(", ", (IEnumerable<Type>)interfaces)));
return false;
}
private static bool AssertIsCompatiblyTypedDictionary(Type expectedType, object subject)
{
Type expectedDictionaryType = GetIDictionaryInterface(expectedType);
Type expectedKeyType = GetDictionaryKeyType(expectedDictionaryType);
Type subjectType = subject.GetType();
Type[] subjectDictionaryInterfaces = GetIDictionaryInterfaces(subjectType);
if (!subjectDictionaryInterfaces.Any())
{
AssertionScope.Current.FailWith(
"Expected {context:subject} to be a {0}, but found a {1}.", expectedDictionaryType, subjectType);
return false;
}
Type[] suitableDictionaryInterfaces = subjectDictionaryInterfaces.Where(
@interface => GetDictionaryKeyType(@interface).IsAssignableFrom(expectedKeyType)).ToArray();
if (suitableDictionaryInterfaces.Length > 1)
{
// SMELL: Code could be written to handle this better, but is it really worth the effort?
AssertionScope.Current.FailWith(
"The subject implements multiple IDictionary interfaces. ");
return false;
}
if (!suitableDictionaryInterfaces.Any())
{
AssertionScope.Current.FailWith(
string.Format(
"The {{context:subject}} dictionary has keys of type {0}; "
+ "however, the expectation is not keyed with any compatible types.{1}"
+ "The subject implements: {2}",
expectedKeyType,
Environment.NewLine,
string.Join(",", (IEnumerable<Type>)subjectDictionaryInterfaces)));
return false;
}
return true;
}
private static Type GetDictionaryKeyType(Type expectedType)
{
return expectedType.GetGenericArguments()[0];
}
private static bool AssertSameLength(object subject, Type expectationType, object expectation)
{
string methodName =
ExpressionExtensions.GetMethodName(() => AssertSameLength<object, object, object, object>(null, null));
Type subjectType = subject.GetType();
Type[] subjectTypeArguments = GetDictionaryTypeArguments(subjectType);
Type[] expectationTypeArguments = GetDictionaryTypeArguments(expectationType);
Type[] typeArguments = subjectTypeArguments.Concat(expectationTypeArguments).ToArray();
MethodCallExpression assertSameLength = Expression.Call(
typeof(GenericDictionaryEquivalencyStep),
methodName,
typeArguments,
Expression.Constant(subject, GetIDictionaryInterface(subjectType)),
Expression.Constant(expectation, GetIDictionaryInterface(expectationType)));
return (bool)Expression.Lambda(assertSameLength).Compile().DynamicInvoke();
}
private static Type[] GetDictionaryTypeArguments(Type type)
{
Type dictionaryType = GetIDictionaryInterface(type);
return dictionaryType.GetGenericArguments();
}
private static Type GetIDictionaryInterface(Type expectedType)
{
return GetIDictionaryInterfaces(expectedType).Single();
}
private static bool AssertSameLength<TSubjectKey, TSubjectValue, TExpectedKey, TExpectedValue>(
IDictionary<TSubjectKey, TSubjectValue> subject, IDictionary<TExpectedKey, TExpectedValue> expectation)
where TExpectedKey : TSubjectKey
// Type constraint of TExpectedKey is asymetric in regards to TSubjectKey
// but it is valid. This constraint is implicitly enforced by the
// AssertIsCompatiblyTypedDictionary method which is called before
// the AssertSameLength method.
{
if (expectation.Count == subject.Count)
{
return true;
}
KeyDifference<TSubjectKey, TExpectedKey> keyDifference = CalculateKeyDifference(subject, expectation);
bool hasMissingKeys = keyDifference.MissingKeys.Count > 0;
bool hasAdditionalKeys = keyDifference.AdditionalKeys.Any();
return Execute.Assertion
.WithExpectation("Expected {context:subject} to be a dictionary with {0} item(s), ", expectation.Count)
.ForCondition(!hasMissingKeys || hasAdditionalKeys)
.FailWith("but it misses key(s) {0}", keyDifference.MissingKeys)
.Then
.ForCondition(hasMissingKeys || !hasAdditionalKeys)
.FailWith("but has additional key(s) {0}", keyDifference.AdditionalKeys)
.Then
.ForCondition(!hasMissingKeys || !hasAdditionalKeys)
.FailWith("but it misses key(s) {0} and has additional key(s) {1}", keyDifference.MissingKeys, keyDifference.AdditionalKeys)
.Then
.ClearExpectation();
}
private static KeyDifference<TSubjectKey, TExpectedKey> CalculateKeyDifference<TSubjectKey, TSubjectValue, TExpectedKey,
TExpectedValue>(IDictionary<TSubjectKey, TSubjectValue> subject,
IDictionary<TExpectedKey, TExpectedValue> expectation)
where TExpectedKey : TSubjectKey
{
var missingKeys = new List<TExpectedKey>();
var presentKeys = new HashSet<TSubjectKey>();
foreach (TExpectedKey expectationKey in expectation.Keys)
{
if (subject.ContainsKey(expectationKey))
{
presentKeys.Add(expectationKey);
}
else
{
missingKeys.Add(expectationKey);
}
}
var additionalKeys = new List<TSubjectKey>();
foreach (TSubjectKey subjectKey in subject.Keys)
{
if (!presentKeys.Contains(subjectKey))
{
additionalKeys.Add(subjectKey);
}
}
return new KeyDifference<TSubjectKey, TExpectedKey>(missingKeys, additionalKeys);
}
private static void AssertDictionaryEquivalence(IEquivalencyValidationContext context,
IEquivalencyValidator parent, IEquivalencyAssertionOptions config)
{
Type expectationType = config.GetExpectationType(context);
string methodName =
ExpressionExtensions.GetMethodName(
() => AssertDictionaryEquivalence<object, object, object, object>(null, null, null, null, null));
Type subjectType = context.Subject.GetType();
Type[] subjectTypeArguments = GetDictionaryTypeArguments(subjectType);
Type[] expectationTypeArguments = GetDictionaryTypeArguments(expectationType);
Type[] typeArguments = subjectTypeArguments.Concat(expectationTypeArguments).ToArray();
MethodCallExpression assertDictionaryEquivalence = Expression.Call(
typeof(GenericDictionaryEquivalencyStep),
methodName,
typeArguments,
Expression.Constant(context),
Expression.Constant(parent),
Expression.Constant(config),
Expression.Constant(context.Subject, GetIDictionaryInterface(subjectType)),
Expression.Constant(context.Expectation, GetIDictionaryInterface(expectationType)));
Expression.Lambda(assertDictionaryEquivalence).Compile().DynamicInvoke();
}
private static void AssertDictionaryEquivalence<TSubjectKey, TSubjectValue, TExpectedKey, TExpectedValue>(
EquivalencyValidationContext context,
IEquivalencyValidator parent,
IEquivalencyAssertionOptions config,
IDictionary<TSubjectKey, TSubjectValue> subject,
IDictionary<TExpectedKey, TExpectedValue> expectation)
where TExpectedKey : TSubjectKey
{
foreach (TExpectedKey key in expectation.Keys)
{
if (subject.TryGetValue(key, out TSubjectValue subjectValue))
{
if (config.IsRecursive)
{
parent.AssertEqualityUsing(context.CreateForDictionaryItem(key, subjectValue, expectation[key]));
}
else
{
subjectValue.Should().Be(expectation[key], context.Because, context.BecauseArgs);
}
}
else
{
AssertionScope.Current.FailWith("Expected {context:subject} to contain key {0}.", key);
}
}
}
private class KeyDifference<TSubjectKey, TExpectedKey>
{
public KeyDifference(List<TExpectedKey> missingKeys, List<TSubjectKey> additionalKeys)
{
MissingKeys = missingKeys;
AdditionalKeys = additionalKeys;
}
public List<TExpectedKey> MissingKeys { get; }
public List<TSubjectKey> AdditionalKeys { get; }
}
}
}