OLD | NEW |
| (Empty) |
1 // Copyright 2014 Google Inc. All Rights Reserved. | |
2 // | |
3 // Licensed under the Apache License, Version 2.0 (the "License"); | |
4 // you may not use this file except in compliance with the License. | |
5 // You may obtain a copy of the License at | |
6 // | |
7 // http://www.apache.org/licenses/LICENSE-2.0 | |
8 // | |
9 // Unless required by applicable law or agreed to in writing, software | |
10 // distributed under the License is distributed on an "AS IS" BASIS, | |
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 // See the License for the specific language governing permissions and | |
13 // limitations under the License. | |
14 | |
15 part of quiver.testing.equality; | |
16 | |
17 /** | |
18 * Matcher for == and hashCode methods of a class. | |
19 * | |
20 * To use, invoke areEqualityGroups with a list of equality groups where each | |
21 * group contains objects that are supposed to be equal to each other, and | |
22 * objects of different groups are expected to be unequal. For example: | |
23 * | |
24 * expect({ | |
25 * 'hello': ["hello", "h" + "ello"], | |
26 * 'world': ["world", "wor" + "ld"], | |
27 * 'three': [2, 1 + 1] | |
28 * }, areEqualityGroups); | |
29 * | |
30 * This tests that: | |
31 * | |
32 * * comparing each object against itself returns true | |
33 * * comparing each object against an instance of an incompatible class | |
34 * returns false | |
35 * * comparing each pair of objects within the same equality group returns | |
36 * true | |
37 * * comparing each pair of objects from different equality groups returns | |
38 * false | |
39 * * the hash codes of any two equal objects are equal | |
40 * * equals implementation is idempotent | |
41 * | |
42 * The format of the Map passed to expect is such that the map keys are used in | |
43 * error messages to identify the group described by the map value. | |
44 * | |
45 * When a test fails, the error message labels the objects involved in | |
46 * the failed comparison as follows: | |
47 * | |
48 * "`[group x, item j]`" refers to the ith item in the xth equality group, | |
49 * where both equality groups and the items within equality groups are | |
50 * numbered starting from 1. When either a constructor argument or an | |
51 * equal object is provided, that becomes group 1. | |
52 * | |
53 */ | |
54 const Matcher areEqualityGroups = const _EqualityGroupMatcher(); | |
55 | |
56 const _repetitions = 3; | |
57 | |
58 class _EqualityGroupMatcher extends Matcher { | |
59 static const failureReason = 'failureReason'; | |
60 const _EqualityGroupMatcher(); | |
61 | |
62 @override | |
63 Description describe(Description description) => | |
64 description.add('to be equality groups'); | |
65 | |
66 @override | |
67 bool matches(Map<String, List> item, Map matchState) { | |
68 try { | |
69 _verifyEqualityGroups(item, matchState); | |
70 return true; | |
71 } on MatchError catch (e) { | |
72 matchState[failureReason] = e.toString(); | |
73 return false; | |
74 } | |
75 } | |
76 | |
77 Description describeMismatch(item, Description mismatchDescription, | |
78 Map matchState, bool verbose) => | |
79 mismatchDescription.add(" ${matchState[failureReason]}"); | |
80 | |
81 void _verifyEqualityGroups(Map<String, List> equalityGroups, Map matchState) { | |
82 if (equalityGroups == null) { | |
83 throw new MatchError('Equality Group must not be null'); | |
84 } | |
85 var equalityGroupsCopy = {}; | |
86 equalityGroups.forEach((String groupName, List group) { | |
87 if (groupName == null) { | |
88 throw new MatchError('Group name must not be null'); | |
89 } | |
90 if (group == null) { | |
91 throw new MatchError('Group must not be null'); | |
92 } | |
93 equalityGroupsCopy[groupName] = new List.from(group); | |
94 }); | |
95 | |
96 // Run the test multiple times to ensure deterministic equals | |
97 for (var run in range(_repetitions)) { | |
98 _checkBasicIdentity(equalityGroupsCopy, matchState); | |
99 _checkGroupBasedEquality(equalityGroupsCopy); | |
100 } | |
101 } | |
102 | |
103 void _checkBasicIdentity(Map<String, List> equalityGroups, Map matchState) { | |
104 var flattened = equalityGroups.values.expand((group) => group); | |
105 for (var item in flattened) { | |
106 if (item == _NotAnInstance.equalToNothing) { | |
107 throw new MatchError( | |
108 "$item must not be equal to an arbitrary object of another class"); | |
109 } | |
110 | |
111 if (item != item) { | |
112 throw new MatchError("$item must be equal to itself"); | |
113 } | |
114 | |
115 if (item.hashCode != item.hashCode) { | |
116 throw new MatchError("the implementation of hashCode of $item must " | |
117 "be idempotent"); | |
118 } | |
119 } | |
120 } | |
121 | |
122 void _checkGroupBasedEquality(Map<String, List> equalityGroups) { | |
123 equalityGroups.forEach((String groupName, List group) { | |
124 var groupLength = group.length; | |
125 for (var itemNumber = 0; itemNumber < groupLength; itemNumber++) { | |
126 _checkEqualToOtherGroup( | |
127 equalityGroups, groupLength, itemNumber, groupName); | |
128 _checkUnequalToOtherGroups(equalityGroups, groupName, itemNumber); | |
129 } | |
130 }); | |
131 } | |
132 | |
133 void _checkUnequalToOtherGroups( | |
134 Map<String, List> equalityGroups, String groupName, int itemNumber) { | |
135 equalityGroups.forEach((String unrelatedGroupName, List unrelatedGroup) { | |
136 if (groupName != unrelatedGroupName) { | |
137 for (var unrelatedItemNumber = 0; | |
138 unrelatedItemNumber < unrelatedGroup.length; | |
139 unrelatedItemNumber++) { | |
140 _expectUnrelated(equalityGroups, groupName, itemNumber, | |
141 unrelatedGroupName, unrelatedItemNumber); | |
142 } | |
143 } | |
144 }); | |
145 } | |
146 | |
147 void _checkEqualToOtherGroup(Map<String, List> equalityGroups, | |
148 int groupLength, int itemNumber, String groupName) { | |
149 for (var relatedItemNumber = 0; | |
150 relatedItemNumber < groupLength; | |
151 relatedItemNumber++) { | |
152 if (itemNumber != relatedItemNumber) { | |
153 _expectRelated( | |
154 equalityGroups, groupName, itemNumber, relatedItemNumber); | |
155 } | |
156 } | |
157 } | |
158 | |
159 void _expectRelated(Map<String, List> equalityGroups, String groupName, | |
160 int itemNumber, int relatedItemNumber) { | |
161 var itemInfo = _createItem(equalityGroups, groupName, itemNumber); | |
162 var relatedInfo = _createItem(equalityGroups, groupName, relatedItemNumber); | |
163 | |
164 if (itemInfo.value != relatedInfo.value) { | |
165 throw new MatchError("$itemInfo must be equal to $relatedInfo"); | |
166 } | |
167 | |
168 if (itemInfo.value.hashCode != relatedInfo.value.hashCode) { | |
169 throw new MatchError( | |
170 "the hashCode (${itemInfo.value.hashCode}) of $itemInfo must " | |
171 "be equal to the hashCode (${relatedInfo.value.hashCode}) of " | |
172 "$relatedInfo}"); | |
173 } | |
174 } | |
175 | |
176 void _expectUnrelated(Map<String, List> equalityGroups, String groupName, | |
177 int itemNumber, String unrelatedGroupName, int unrelatedItemNumber) { | |
178 var itemInfo = _createItem(equalityGroups, groupName, itemNumber); | |
179 var unrelatedInfo = | |
180 _createItem(equalityGroups, unrelatedGroupName, unrelatedItemNumber); | |
181 | |
182 if (itemInfo.value == unrelatedInfo.value) { | |
183 throw new MatchError("$itemInfo must not be equal to " "$unrelatedInfo)"); | |
184 } | |
185 } | |
186 | |
187 _Item _createItem( | |
188 Map<String, List> equalityGroups, String groupName, int itemNumber) => | |
189 new _Item(equalityGroups[groupName][itemNumber], groupName, itemNumber); | |
190 } | |
191 | |
192 class _NotAnInstance { | |
193 static const equalToNothing = const _NotAnInstance._(); | |
194 const _NotAnInstance._(); | |
195 } | |
196 | |
197 class _Item { | |
198 final Object value; | |
199 final String groupName; | |
200 final int itemNumber; | |
201 | |
202 _Item(this.value, this.groupName, this.itemNumber); | |
203 | |
204 @override | |
205 String toString() => "$value [group '$groupName', item ${itemNumber + 1}]"; | |
206 } | |
207 | |
208 class MatchError extends Error { | |
209 final message; | |
210 | |
211 /// The [message] describes the match error. | |
212 MatchError([this.message]); | |
213 | |
214 String toString() => message; | |
215 } | |
OLD | NEW |