LCOV - code coverage report
Current view: top level - matcher-0.12.11/lib/src - equals_matcher.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 21 137 15.3 %
Date: 2021-11-28 14:37:50 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
       2             : // for details. All rights reserved. Use of this source code is governed by a
       3             : // BSD-style license that can be found in the LICENSE file.
       4             : 
       5             : import 'feature_matcher.dart';
       6             : import 'interfaces.dart';
       7             : import 'util.dart';
       8             : 
       9             : /// Returns a matcher that matches if the value is structurally equal to
      10             : /// [expected].
      11             : ///
      12             : /// If [expected] is a [Matcher], then it matches using that. Otherwise it tests
      13             : /// for equality using `==` on the expected value.
      14             : ///
      15             : /// For [Iterable]s and [Map]s, this will recursively match the elements. To
      16             : /// handle cyclic structures a recursion depth [limit] can be provided. The
      17             : /// default limit is 100. [Set]s will be compared order-independently.
      18          16 : Matcher equals(Object? expected, [int limit = 100]) => expected is String
      19           7 :     ? _StringEqualsMatcher(expected)
      20           5 :     : _DeepMatcher(expected, limit);
      21             : 
      22             : typedef _RecursiveMatcher = _Mismatch? Function(Object?, Object?, String, int);
      23             : 
      24             : /// A special equality matcher for strings.
      25             : class _StringEqualsMatcher extends FeatureMatcher<String> {
      26             :   final String _value;
      27             : 
      28           7 :   _StringEqualsMatcher(this._value);
      29             : 
      30           7 :   @override
      31          14 :   bool typedMatches(String item, Map matchState) => _value == item;
      32             : 
      33           0 :   @override
      34             :   Description describe(Description description) =>
      35           0 :       description.addDescriptionOf(_value);
      36             : 
      37           0 :   @override
      38             :   Description describeTypedMismatch(String item,
      39             :       Description mismatchDescription, Map matchState, bool verbose) {
      40           0 :     var buff = StringBuffer();
      41           0 :     buff.write('is different.');
      42           0 :     var escapedItem = escape(item);
      43           0 :     var escapedValue = escape(_value);
      44           0 :     var minLength = escapedItem.length < escapedValue.length
      45           0 :         ? escapedItem.length
      46           0 :         : escapedValue.length;
      47             :     var start = 0;
      48           0 :     for (; start < minLength; start++) {
      49           0 :       if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) {
      50             :         break;
      51             :       }
      52             :     }
      53           0 :     if (start == minLength) {
      54           0 :       if (escapedValue.length < escapedItem.length) {
      55           0 :         buff.write(' Both strings start the same, but the actual value also'
      56             :             ' has the following trailing characters: ');
      57           0 :         _writeTrailing(buff, escapedItem, escapedValue.length);
      58             :       } else {
      59           0 :         buff.write(' Both strings start the same, but the actual value is'
      60             :             ' missing the following trailing characters: ');
      61           0 :         _writeTrailing(buff, escapedValue, escapedItem.length);
      62             :       }
      63             :     } else {
      64           0 :       buff.write('\nExpected: ');
      65           0 :       _writeLeading(buff, escapedValue, start);
      66           0 :       _writeTrailing(buff, escapedValue, start);
      67           0 :       buff.write('\n  Actual: ');
      68           0 :       _writeLeading(buff, escapedItem, start);
      69           0 :       _writeTrailing(buff, escapedItem, start);
      70           0 :       buff.write('\n          ');
      71           0 :       for (var i = start > 10 ? 14 : start; i > 0; i--) {
      72           0 :         buff.write(' ');
      73             :       }
      74           0 :       buff.write('^\n Differ at offset $start');
      75             :     }
      76             : 
      77           0 :     return mismatchDescription.add(buff.toString());
      78             :   }
      79             : 
      80           0 :   static void _writeLeading(StringBuffer buff, String s, int start) {
      81           0 :     if (start > 10) {
      82           0 :       buff.write('... ');
      83           0 :       buff.write(s.substring(start - 10, start));
      84             :     } else {
      85           0 :       buff.write(s.substring(0, start));
      86             :     }
      87             :   }
      88             : 
      89           0 :   static void _writeTrailing(StringBuffer buff, String s, int start) {
      90           0 :     if (start + 10 > s.length) {
      91           0 :       buff.write(s.substring(start));
      92             :     } else {
      93           0 :       buff.write(s.substring(start, start + 10));
      94           0 :       buff.write(' ...');
      95             :     }
      96             :   }
      97             : }
      98             : 
      99             : class _DeepMatcher extends Matcher {
     100             :   final Object? _expected;
     101             :   final int _limit;
     102             : 
     103           5 :   _DeepMatcher(this._expected, [int limit = 1000]) : _limit = limit;
     104             : 
     105           0 :   _Mismatch? _compareIterables(Iterable expected, Object? actual,
     106             :       _RecursiveMatcher matcher, int depth, String location) {
     107           0 :     if (actual is Iterable) {
     108           0 :       var expectedIterator = expected.iterator;
     109           0 :       var actualIterator = actual.iterator;
     110           0 :       for (var index = 0;; index++) {
     111             :         // Advance in lockstep.
     112           0 :         var expectedNext = expectedIterator.moveNext();
     113           0 :         var actualNext = actualIterator.moveNext();
     114             : 
     115             :         // If we reached the end of both, we succeeded.
     116             :         if (!expectedNext && !actualNext) return null;
     117             : 
     118             :         // Fail if their lengths are different.
     119           0 :         var newLocation = '$location[$index]';
     120             :         if (!expectedNext) {
     121           0 :           return _Mismatch.simple(newLocation, actual, 'longer than expected');
     122             :         }
     123             :         if (!actualNext) {
     124           0 :           return _Mismatch.simple(newLocation, actual, 'shorter than expected');
     125             :         }
     126             : 
     127             :         // Match the elements.
     128           0 :         var rp = matcher(expectedIterator.current, actualIterator.current,
     129             :             newLocation, depth);
     130             :         if (rp != null) return rp;
     131             :       }
     132             :     } else {
     133           0 :       return _Mismatch.simple(location, actual, 'is not Iterable');
     134             :     }
     135             :   }
     136             : 
     137           0 :   _Mismatch? _compareSets(Set expected, Object? actual,
     138             :       _RecursiveMatcher matcher, int depth, String location) {
     139           0 :     if (actual is Iterable) {
     140           0 :       var other = actual.toSet();
     141             : 
     142           0 :       for (var expectedElement in expected) {
     143           0 :         if (other.every((actualElement) =>
     144             :             matcher(expectedElement, actualElement, location, depth) != null)) {
     145           0 :           return _Mismatch(
     146             :               location,
     147             :               actual,
     148           0 :               (description, verbose) => description
     149           0 :                   .add('does not contain ')
     150           0 :                   .addDescriptionOf(expectedElement));
     151             :         }
     152             :       }
     153             : 
     154           0 :       if (other.length > expected.length) {
     155           0 :         return _Mismatch.simple(location, actual, 'larger than expected');
     156           0 :       } else if (other.length < expected.length) {
     157           0 :         return _Mismatch.simple(location, actual, 'smaller than expected');
     158             :       } else {
     159             :         return null;
     160             :       }
     161             :     } else {
     162           0 :       return _Mismatch.simple(location, actual, 'is not Iterable');
     163             :     }
     164             :   }
     165             : 
     166           5 :   _Mismatch? _recursiveMatch(
     167             :       Object? expected, Object? actual, String location, int depth) {
     168             :     // If the expected value is a matcher, try to match it.
     169           5 :     if (expected is Matcher) {
     170           0 :       var matchState = {};
     171           0 :       if (expected.matches(actual, matchState)) return null;
     172           0 :       return _Mismatch(location, actual, (description, verbose) {
     173           0 :         var oldLength = description.length;
     174           0 :         expected.describeMismatch(actual, description, matchState, verbose);
     175           0 :         if (depth > 0 && description.length == oldLength) {
     176           0 :           description.add('does not match ');
     177           0 :           expected.describe(description);
     178             :         }
     179             :       });
     180             :     } else {
     181             :       // Otherwise, test for equality.
     182             :       try {
     183           5 :         if (expected == actual) return null;
     184             :       } catch (e) {
     185             :         // TODO(gram): Add a test for this case.
     186           0 :         return _Mismatch(
     187             :             location,
     188             :             actual,
     189           0 :             (description, verbose) =>
     190           0 :                 description.add('== threw ').addDescriptionOf(e));
     191             :       }
     192             :     }
     193             : 
     194          10 :     if (depth > _limit) {
     195           0 :       return _Mismatch.simple(
     196             :           location, actual, 'recursion depth limit exceeded');
     197             :     }
     198             : 
     199             :     // If _limit is 1 we can only recurse one level into object.
     200           5 :     if (depth == 0 || _limit > 1) {
     201           5 :       if (expected is Set) {
     202           0 :         return _compareSets(
     203           0 :             expected, actual, _recursiveMatch, depth + 1, location);
     204           5 :       } else if (expected is Iterable) {
     205           0 :         return _compareIterables(
     206           0 :             expected, actual, _recursiveMatch, depth + 1, location);
     207           5 :       } else if (expected is Map) {
     208           0 :         if (actual is! Map) {
     209           0 :           return _Mismatch.simple(location, actual, 'expected a map');
     210             :         }
     211           0 :         var err = (expected.length == actual.length)
     212             :             ? ''
     213             :             : 'has different length and ';
     214           0 :         for (var key in expected.keys) {
     215           0 :           if (!actual.containsKey(key)) {
     216           0 :             return _Mismatch(
     217             :                 location,
     218             :                 actual,
     219           0 :                 (description, verbose) => description
     220           0 :                     .add('${err}is missing map key ')
     221           0 :                     .addDescriptionOf(key));
     222             :           }
     223             :         }
     224             : 
     225           0 :         for (var key in actual.keys) {
     226           0 :           if (!expected.containsKey(key)) {
     227           0 :             return _Mismatch(
     228             :                 location,
     229             :                 actual,
     230           0 :                 (description, verbose) => description
     231           0 :                     .add('${err}has extra map key ')
     232           0 :                     .addDescriptionOf(key));
     233             :           }
     234             :         }
     235             : 
     236           0 :         for (var key in expected.keys) {
     237           0 :           var rp = _recursiveMatch(
     238           0 :               expected[key], actual[key], "$location['$key']", depth + 1);
     239             :           if (rp != null) return rp;
     240             :         }
     241             : 
     242             :         return null;
     243             :       }
     244             :     }
     245             : 
     246             :     // If we have recursed, show the expected value too; if not, expect() will
     247             :     // show it for us.
     248           5 :     if (depth > 0) {
     249           0 :       return _Mismatch(location, actual,
     250           0 :           (description, verbose) => description.addDescriptionOf(expected),
     251             :           instead: true);
     252             :     } else {
     253           5 :       return _Mismatch(location, actual, null);
     254             :     }
     255             :   }
     256             : 
     257           5 :   @override
     258             :   bool matches(Object? actual, Map matchState) {
     259          10 :     var mismatch = _recursiveMatch(_expected, actual, '', 0);
     260             :     if (mismatch == null) return true;
     261          10 :     addStateInfo(matchState, {'mismatch': mismatch});
     262             :     return false;
     263             :   }
     264             : 
     265           0 :   @override
     266             :   Description describe(Description description) =>
     267           0 :       description.addDescriptionOf(_expected);
     268             : 
     269           0 :   @override
     270             :   Description describeMismatch(Object? item, Description mismatchDescription,
     271             :       Map matchState, bool verbose) {
     272           0 :     var mismatch = matchState['mismatch'] as _Mismatch;
     273           0 :     var describeProblem = mismatch.describeProblem;
     274           0 :     if (mismatch.location.isNotEmpty) {
     275             :       mismatchDescription
     276           0 :           .add('at location ')
     277           0 :           .add(mismatch.location)
     278           0 :           .add(' is ')
     279           0 :           .addDescriptionOf(mismatch.actual);
     280             :       if (describeProblem != null) {
     281             :         mismatchDescription
     282           0 :             .add(' ${mismatch.instead ? 'instead of' : 'which'} ');
     283             :         describeProblem(mismatchDescription, verbose);
     284             :       }
     285             :     } else {
     286             :       // If we didn't get a good reason, that would normally be a
     287             :       // simple 'is <value>' message. We only add that if the mismatch
     288             :       // description is non empty (so we are supplementing the mismatch
     289             :       // description).
     290             :       if (describeProblem == null) {
     291           0 :         if (mismatchDescription.length > 0) {
     292           0 :           mismatchDescription.add('is ').addDescriptionOf(item);
     293             :         }
     294             :       } else {
     295             :         describeProblem(mismatchDescription, verbose);
     296             :       }
     297             :     }
     298             :     return mismatchDescription;
     299             :   }
     300             : }
     301             : 
     302             : class _Mismatch {
     303             :   /// A human-readable description of the location within the collection where
     304             :   /// the mismatch occurred.
     305             :   final String location;
     306             : 
     307             :   /// The actual value found at [location].
     308             :   final Object? actual;
     309             : 
     310             :   /// Callback that can create a detailed description of the problem.
     311             :   final void Function(Description, bool verbose)? describeProblem;
     312             : 
     313             :   /// If `true`, [describeProblem] describes the expected value, so when the
     314             :   /// final mismatch description is pieced together, it will be preceded by
     315             :   /// `instead of` (e.g. `at location [2] is <3> instead of <4>`).  If `false`,
     316             :   /// [describeProblem] is a problem description from a sub-matcher, so when the
     317             :   /// final mismatch description is pieced together, it will be preceded by
     318             :   /// `which` (e.g. `at location [2] is <foo> which has length of 3`).
     319             :   final bool instead;
     320             : 
     321           5 :   _Mismatch(this.location, this.actual, this.describeProblem,
     322             :       {this.instead = false});
     323             : 
     324           0 :   _Mismatch.simple(this.location, this.actual, String problem,
     325             :       {this.instead = false})
     326           0 :       : describeProblem = ((description, verbose) => description.add(problem));
     327             : }

Generated by: LCOV version 1.14