| // 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(item, Map matchState) { |
| try { |
| _verifyEqualityGroups(item as Map<String, List>, 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'); |
| } |
| final equalityGroupsCopy = <String, List>{}; |
| 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 i = 0; i < _repetitions; i++) { |
| _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; |
| } |