| // Copyright 2014 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| part of quiver.testing.equality; |
| |
| /** |
| * Matcher for == and hashCode methods of a class. |
| * |
| * To use, invoke areEqualityGroups with a list of equality groups where each |
| * group contains objects that are supposed to be equal to each other, and |
| * objects of different groups are expected to be unequal. For example: |
| * |
| * expect({ |
| * 'hello': ["hello", "h" + "ello"], |
| * 'world': ["world", "wor" + "ld"], |
| * 'three': [2, 1 + 1] |
| * }, areEqualityGroups); |
| * |
| * This tests that: |
| * |
| * * comparing each object against itself returns true |
| * * comparing each object against an instance of an incompatible class |
| * returns false |
| * * comparing each pair of objects within the same equality group returns |
| * true |
| * * comparing each pair of objects from different equality groups returns |
| * false |
| * * the hash codes of any two equal objects are equal |
| * * equals implementation is idempotent |
| * |
| * The format of the Map passed to expect is such that the map keys are used in |
| * error messages to identify the group described by the map value. |
| * |
| * When a test fails, the error message labels the objects involved in |
| * the failed comparison as follows: |
| * |
| * "`[group x, item j]`" refers to the ith item in the xth equality group, |
| * where both equality groups and the items within equality groups are |
| * numbered starting from 1. When either a constructor argument or an |
| * equal object is provided, that becomes group 1. |
| * |
| */ |
| const Matcher areEqualityGroups = const _EqualityGroupMatcher(); |
| |
| const _repetitions = 3; |
| |
| class _EqualityGroupMatcher extends Matcher { |
| static const failureReason = 'failureReason'; |
| const _EqualityGroupMatcher(); |
| |
| @override |
| Description describe(Description description) => |
| description.add('to be equality groups'); |
| |
| @override |
| bool matches(Map<String, List> item, Map matchState) { |
| try { |
| _verifyEqualityGroups(item, matchState); |
| return true; |
| } on MatchError catch (e) { |
| matchState[failureReason] = e.toString(); |
| return false; |
| } |
| } |
| |
| Description describeMismatch(item, Description mismatchDescription, |
| Map matchState, bool verbose) => |
| mismatchDescription.add(" ${matchState[failureReason]}"); |
| |
| void _verifyEqualityGroups(Map<String, List> equalityGroups, Map matchState) { |
| if (equalityGroups == null) { |
| throw new MatchError('Equality Group must not be null'); |
| } |
| var equalityGroupsCopy = {}; |
| equalityGroups.forEach((String groupName, List group) { |
| if (groupName == null) { |
| throw new MatchError('Group name must not be null'); |
| } |
| if (group == null) { |
| throw new MatchError('Group must not be null'); |
| } |
| equalityGroupsCopy[groupName] = new List.from(group); |
| }); |
| |
| // Run the test multiple times to ensure deterministic equals |
| for (var run in range(_repetitions)) { |
| _checkBasicIdentity(equalityGroupsCopy, matchState); |
| _checkGroupBasedEquality(equalityGroupsCopy); |
| } |
| } |
| |
| void _checkBasicIdentity(Map<String, List> equalityGroups, Map matchState) { |
| var flattened = equalityGroups.values.expand((group) => group); |
| for (var item in flattened) { |
| if (item == _NotAnInstance.equalToNothing) { |
| throw new MatchError( |
| "$item must not be equal to an arbitrary object of another class"); |
| } |
| |
| if (item != item) { |
| throw new MatchError("$item must be equal to itself"); |
| } |
| |
| if (item.hashCode != item.hashCode) { |
| throw new MatchError("the implementation of hashCode of $item must " |
| "be idempotent"); |
| } |
| } |
| } |
| |
| void _checkGroupBasedEquality(Map<String, List> equalityGroups) { |
| equalityGroups.forEach((String groupName, List group) { |
| var groupLength = group.length; |
| for (var itemNumber = 0; itemNumber < groupLength; itemNumber++) { |
| _checkEqualToOtherGroup( |
| equalityGroups, groupLength, itemNumber, groupName); |
| _checkUnequalToOtherGroups(equalityGroups, groupName, itemNumber); |
| } |
| }); |
| } |
| |
| void _checkUnequalToOtherGroups( |
| Map<String, List> equalityGroups, String groupName, int itemNumber) { |
| equalityGroups.forEach((String unrelatedGroupName, List unrelatedGroup) { |
| if (groupName != unrelatedGroupName) { |
| for (var unrelatedItemNumber = 0; |
| unrelatedItemNumber < unrelatedGroup.length; |
| unrelatedItemNumber++) { |
| _expectUnrelated(equalityGroups, groupName, itemNumber, |
| unrelatedGroupName, unrelatedItemNumber); |
| } |
| } |
| }); |
| } |
| |
| void _checkEqualToOtherGroup(Map<String, List> equalityGroups, |
| int groupLength, int itemNumber, String groupName) { |
| for (var relatedItemNumber = 0; |
| relatedItemNumber < groupLength; |
| relatedItemNumber++) { |
| if (itemNumber != relatedItemNumber) { |
| _expectRelated( |
| equalityGroups, groupName, itemNumber, relatedItemNumber); |
| } |
| } |
| } |
| |
| void _expectRelated(Map<String, List> equalityGroups, String groupName, |
| int itemNumber, int relatedItemNumber) { |
| var itemInfo = _createItem(equalityGroups, groupName, itemNumber); |
| var relatedInfo = _createItem(equalityGroups, groupName, relatedItemNumber); |
| |
| if (itemInfo.value != relatedInfo.value) { |
| throw new MatchError("$itemInfo must be equal to $relatedInfo"); |
| } |
| |
| if (itemInfo.value.hashCode != relatedInfo.value.hashCode) { |
| throw new MatchError( |
| "the hashCode (${itemInfo.value.hashCode}) of $itemInfo must " |
| "be equal to the hashCode (${relatedInfo.value.hashCode}) of " |
| "$relatedInfo}"); |
| } |
| } |
| |
| void _expectUnrelated(Map<String, List> equalityGroups, String groupName, |
| int itemNumber, String unrelatedGroupName, int unrelatedItemNumber) { |
| var itemInfo = _createItem(equalityGroups, groupName, itemNumber); |
| var unrelatedInfo = |
| _createItem(equalityGroups, unrelatedGroupName, unrelatedItemNumber); |
| |
| if (itemInfo.value == unrelatedInfo.value) { |
| throw new MatchError("$itemInfo must not be equal to " "$unrelatedInfo)"); |
| } |
| } |
| |
| _Item _createItem( |
| Map<String, List> equalityGroups, String groupName, int itemNumber) => |
| new _Item(equalityGroups[groupName][itemNumber], groupName, itemNumber); |
| } |
| |
| class _NotAnInstance { |
| static const equalToNothing = const _NotAnInstance._(); |
| const _NotAnInstance._(); |
| } |
| |
| class _Item { |
| final Object value; |
| final String groupName; |
| final int itemNumber; |
| |
| _Item(this.value, this.groupName, this.itemNumber); |
| |
| @override |
| String toString() => "$value [group '$groupName', item ${itemNumber + 1}]"; |
| } |
| |
| class MatchError extends Error { |
| final message; |
| |
| /// The [message] describes the match error. |
| MatchError([this.message]); |
| |
| String toString() => message; |
| } |