| 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 |