LCOV - code coverage report
Current view: top level - matcher-0.12.1+4/lib/src - core_matchers.dart (source / functions) Hit Total Coverage
Test: coverage.lcov Lines: 28 219 12.8 %
Date: 2017-10-10 20:17:03 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright (c) 2012, 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 'package:stack_trace/stack_trace.dart';
       6             : 
       7             : import 'description.dart';
       8             : import 'interfaces.dart';
       9             : import 'util.dart';
      10             : 
      11             : /// Returns a matcher that matches the isEmpty property.
      12             : const Matcher isEmpty = const _Empty();
      13             : 
      14             : class _Empty extends Matcher {
      15           1 :   const _Empty();
      16             : 
      17           1 :   bool matches(item, Map matchState) => item.isEmpty;
      18             : 
      19           0 :   Description describe(Description description) => description.add('empty');
      20             : }
      21             : 
      22             : /// Returns a matcher that matches the isNotEmpty property.
      23             : const Matcher isNotEmpty = const _NotEmpty();
      24             : 
      25             : class _NotEmpty extends Matcher {
      26           0 :   const _NotEmpty();
      27             : 
      28           0 :   bool matches(item, Map matchState) => item.isNotEmpty;
      29             : 
      30           0 :   Description describe(Description description) => description.add('non-empty');
      31             : }
      32             : 
      33             : /// A matcher that matches any null value.
      34             : const Matcher isNull = const _IsNull();
      35             : 
      36             : /// A matcher that matches any non-null value.
      37             : const Matcher isNotNull = const _IsNotNull();
      38             : 
      39             : class _IsNull extends Matcher {
      40           0 :   const _IsNull();
      41             :   bool matches(item, Map matchState) => item == null;
      42           0 :   Description describe(Description description) => description.add('null');
      43             : }
      44             : 
      45             : class _IsNotNull extends Matcher {
      46           2 :   const _IsNotNull();
      47             :   bool matches(item, Map matchState) => item != null;
      48           0 :   Description describe(Description description) => description.add('not null');
      49             : }
      50             : 
      51             : /// A matcher that matches the Boolean value true.
      52             : const Matcher isTrue = const _IsTrue();
      53             : 
      54             : /// A matcher that matches anything except the Boolean value true.
      55             : const Matcher isFalse = const _IsFalse();
      56             : 
      57             : class _IsTrue extends Matcher {
      58           1 :   const _IsTrue();
      59           1 :   bool matches(item, Map matchState) => item == true;
      60           0 :   Description describe(Description description) => description.add('true');
      61             : }
      62             : 
      63             : class _IsFalse extends Matcher {
      64           0 :   const _IsFalse();
      65           0 :   bool matches(item, Map matchState) => item == false;
      66           0 :   Description describe(Description description) => description.add('false');
      67             : }
      68             : 
      69             : /// A matcher that matches the numeric value NaN.
      70             : const Matcher isNaN = const _IsNaN();
      71             : 
      72             : /// A matcher that matches any non-NaN value.
      73             : const Matcher isNotNaN = const _IsNotNaN();
      74             : 
      75             : class _IsNaN extends Matcher {
      76           0 :   const _IsNaN();
      77           0 :   bool matches(item, Map matchState) => double.NAN.compareTo(item) == 0;
      78           0 :   Description describe(Description description) => description.add('NaN');
      79             : }
      80             : 
      81             : class _IsNotNaN extends Matcher {
      82           0 :   const _IsNotNaN();
      83           0 :   bool matches(item, Map matchState) => double.NAN.compareTo(item) != 0;
      84           0 :   Description describe(Description description) => description.add('not NaN');
      85             : }
      86             : 
      87             : /// Returns a matches that matches if the value is the same instance
      88             : /// as [expected], using [identical].
      89           0 : Matcher same(expected) => new _IsSameAs(expected);
      90             : 
      91             : class _IsSameAs extends Matcher {
      92             :   final _expected;
      93           0 :   const _IsSameAs(this._expected);
      94           0 :   bool matches(item, Map matchState) => identical(item, _expected);
      95             :   // If all types were hashable we could show a hash here.
      96             :   Description describe(Description description) =>
      97           0 :       description.add('same instance as ').addDescriptionOf(_expected);
      98             : }
      99             : 
     100             : /// Returns a matcher that matches if the value is structurally equal to
     101             : /// [expected].
     102             : ///
     103             : /// If [expected] is a [Matcher], then it matches using that. Otherwise it tests
     104             : /// for equality using `==` on the expected value.
     105             : ///
     106             : /// For [Iterable]s and [Map]s, this will recursively match the elements. To
     107             : /// handle cyclic structures a recursion depth [limit] can be provided. The
     108             : /// default limit is 100. [Set]s will be compared order-independently.
     109           4 : Matcher equals(expected, [int limit = 100]) => expected is String
     110           0 :     ? new _StringEqualsMatcher(expected)
     111           4 :     : new _DeepMatcher(expected, limit);
     112             : 
     113             : typedef _RecursiveMatcher = List<String> Function(
     114             :     dynamic, dynamic, String, int);
     115             : 
     116             : class _DeepMatcher extends Matcher {
     117             :   final _expected;
     118             :   final int _limit;
     119             : 
     120           4 :   _DeepMatcher(this._expected, [int limit = 1000]) : this._limit = limit;
     121             : 
     122             :   // Returns a pair (reason, location)
     123             :   List<String> _compareIterables(Iterable expected, Object actual,
     124             :       _RecursiveMatcher matcher, int depth, String location) {
     125           0 :     if (actual is Iterable) {
     126           0 :       var expectedIterator = expected.iterator;
     127           0 :       var actualIterator = actual.iterator;
     128           0 :       for (var index = 0;; index++) {
     129             :         // Advance in lockstep.
     130           0 :         var expectedNext = expectedIterator.moveNext();
     131           0 :         var actualNext = actualIterator.moveNext();
     132             : 
     133             :         // If we reached the end of both, we succeeded.
     134             :         if (!expectedNext && !actualNext) return null;
     135             : 
     136             :         // Fail if their lengths are different.
     137           0 :         var newLocation = '$location[$index]';
     138           0 :         if (!expectedNext) return ['longer than expected', newLocation];
     139           0 :         if (!actualNext) return ['shorter than expected', newLocation];
     140             : 
     141             :         // Match the elements.
     142           0 :         var rp = matcher(expectedIterator.current, actualIterator.current,
     143             :             newLocation, depth);
     144             :         if (rp != null) return rp;
     145             :       }
     146             :     } else {
     147           0 :       return ['is not Iterable', location];
     148             :     }
     149             :   }
     150             : 
     151             :   List<String> _compareSets(Set expected, Object actual,
     152             :       _RecursiveMatcher matcher, int depth, String location) {
     153           0 :     if (actual is Iterable) {
     154           0 :       Set other = actual.toSet();
     155             : 
     156           0 :       for (var expectedElement in expected) {
     157           0 :         if (other.every((actualElement) =>
     158           0 :             matcher(expectedElement, actualElement, location, depth) != null)) {
     159           0 :           return ['does not contain $expectedElement', location];
     160             :         }
     161             :       }
     162             : 
     163           0 :       if (other.length > expected.length) {
     164           0 :         return ['larger than expected', location];
     165           0 :       } else if (other.length < expected.length) {
     166           0 :         return ['smaller than expected', location];
     167             :       } else {
     168             :         return null;
     169             :       }
     170             :     } else {
     171           0 :       return ['is not Iterable', location];
     172             :     }
     173             :   }
     174             : 
     175             :   List<String> _recursiveMatch(
     176             :       Object expected, Object actual, String location, int depth) {
     177             :     // If the expected value is a matcher, try to match it.
     178           4 :     if (expected is Matcher) {
     179           1 :       var matchState = {};
     180           1 :       if (expected.matches(actual, matchState)) return null;
     181             : 
     182           0 :       var description = new StringDescription();
     183           0 :       expected.describe(description);
     184           0 :       return ['does not match $description', location];
     185             :     } else {
     186             :       // Otherwise, test for equality.
     187             :       try {
     188           4 :         if (expected == actual) return null;
     189             :       } catch (e) {
     190             :         // TODO(gram): Add a test for this case.
     191           0 :         return ['== threw "$e"', location];
     192             :       }
     193             :     }
     194             : 
     195           2 :     if (depth > _limit) return ['recursion depth limit exceeded', location];
     196             : 
     197             :     // If _limit is 1 we can only recurse one level into object.
     198           1 :     if (depth == 0 || _limit > 1) {
     199           1 :       if (expected is Set) {
     200           0 :         return _compareSets(
     201           0 :             expected, actual, _recursiveMatch, depth + 1, location);
     202           1 :       } else if (expected is Iterable) {
     203           0 :         return _compareIterables(
     204           0 :             expected, actual, _recursiveMatch, depth + 1, location);
     205           1 :       } else if (expected is Map) {
     206           0 :         if (actual is! Map) return ['expected a map', location];
     207           0 :         var map = (actual as Map);
     208             :         var err =
     209           0 :             (expected.length == map.length) ? '' : 'has different length and ';
     210           0 :         for (var key in expected.keys) {
     211           0 :           if (!map.containsKey(key)) {
     212           0 :             return ["${err}is missing map key '$key'", location];
     213             :           }
     214             :         }
     215             : 
     216           0 :         for (var key in map.keys) {
     217           0 :           if (!expected.containsKey(key)) {
     218           0 :             return ["${err}has extra map key '$key'", location];
     219             :           }
     220             :         }
     221             : 
     222           0 :         for (var key in expected.keys) {
     223           0 :           var rp = _recursiveMatch(
     224           0 :               expected[key], map[key], "$location['$key']", depth + 1);
     225             :           if (rp != null) return rp;
     226             :         }
     227             : 
     228             :         return null;
     229             :       }
     230             :     }
     231             : 
     232           1 :     var description = new StringDescription();
     233             : 
     234             :     // If we have recursed, show the expected value too; if not, expect() will
     235             :     // show it for us.
     236           1 :     if (depth > 0) {
     237             :       description
     238           0 :           .add('was ')
     239           0 :           .addDescriptionOf(actual)
     240           0 :           .add(' instead of ')
     241           0 :           .addDescriptionOf(expected);
     242           0 :       return [description.toString(), location];
     243             :     }
     244             : 
     245             :     // We're not adding any value to the actual value.
     246           1 :     return ["", location];
     247             :   }
     248             : 
     249             :   String _match(expected, actual, Map matchState) {
     250           4 :     var rp = _recursiveMatch(expected, actual, '', 0);
     251             :     if (rp == null) return null;
     252             :     String reason;
     253           3 :     if (rp[0].length > 0) {
     254           0 :       if (rp[1].length > 0) {
     255           0 :         reason = "${rp[0]} at location ${rp[1]}";
     256             :       } else {
     257           0 :         reason = rp[0];
     258             :       }
     259             :     } else {
     260             :       reason = '';
     261             :     }
     262             :     // Cache the failure reason in the matchState.
     263           2 :     addStateInfo(matchState, {'reason': reason});
     264             :     return reason;
     265             :   }
     266             : 
     267             :   bool matches(item, Map matchState) =>
     268           8 :       _match(_expected, item, matchState) == null;
     269             : 
     270             :   Description describe(Description description) =>
     271           0 :       description.addDescriptionOf(_expected);
     272             : 
     273             :   Description describeMismatch(
     274             :       item, Description mismatchDescription, Map matchState, bool verbose) {
     275           0 :     var reason = matchState['reason'] ?? '';
     276             :     // If we didn't get a good reason, that would normally be a
     277             :     // simple 'is <value>' message. We only add that if the mismatch
     278             :     // description is non empty (so we are supplementing the mismatch
     279             :     // description).
     280           0 :     if (reason.length == 0 && mismatchDescription.length > 0) {
     281           0 :       mismatchDescription.add('is ').addDescriptionOf(item);
     282             :     } else {
     283           0 :       mismatchDescription.add(reason);
     284             :     }
     285             :     return mismatchDescription;
     286             :   }
     287             : }
     288             : 
     289             : /// A special equality matcher for strings.
     290             : class _StringEqualsMatcher extends Matcher {
     291             :   final String _value;
     292             : 
     293           0 :   _StringEqualsMatcher(this._value);
     294             : 
     295             :   bool get showActualValue => true;
     296             : 
     297           0 :   bool matches(item, Map matchState) => _value == item;
     298             : 
     299             :   Description describe(Description description) =>
     300           0 :       description.addDescriptionOf(_value);
     301             : 
     302             :   Description describeMismatch(
     303             :       item, Description mismatchDescription, Map matchState, bool verbose) {
     304           0 :     if (item is! String) {
     305           0 :       return mismatchDescription.addDescriptionOf(item).add('is not a string');
     306             :     } else {
     307           0 :       var buff = new StringBuffer();
     308           0 :       buff.write('is different.');
     309           0 :       var escapedItem = escape(item);
     310           0 :       var escapedValue = escape(_value);
     311           0 :       int minLength = escapedItem.length < escapedValue.length
     312           0 :           ? escapedItem.length
     313           0 :           : escapedValue.length;
     314             :       var start = 0;
     315           0 :       for (; start < minLength; start++) {
     316           0 :         if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) {
     317             :           break;
     318             :         }
     319             :       }
     320           0 :       if (start == minLength) {
     321           0 :         if (escapedValue.length < escapedItem.length) {
     322           0 :           buff.write(' Both strings start the same, but the actual value also'
     323             :               ' has the following trailing characters: ');
     324           0 :           _writeTrailing(buff, escapedItem, escapedValue.length);
     325             :         } else {
     326           0 :           buff.write(' Both strings start the same, but the actual value is'
     327             :               ' missing the following trailing characters: ');
     328           0 :           _writeTrailing(buff, escapedValue, escapedItem.length);
     329             :         }
     330             :       } else {
     331           0 :         buff.write('\nExpected: ');
     332           0 :         _writeLeading(buff, escapedValue, start);
     333           0 :         _writeTrailing(buff, escapedValue, start);
     334           0 :         buff.write('\n  Actual: ');
     335           0 :         _writeLeading(buff, escapedItem, start);
     336           0 :         _writeTrailing(buff, escapedItem, start);
     337           0 :         buff.write('\n          ');
     338           0 :         for (int i = (start > 10 ? 14 : start); i > 0; i--) buff.write(' ');
     339           0 :         buff.write('^\n Differ at offset $start');
     340             :       }
     341             : 
     342           0 :       return mismatchDescription.add(buff.toString());
     343             :     }
     344             :   }
     345             : 
     346             :   static void _writeLeading(StringBuffer buff, String s, int start) {
     347           0 :     if (start > 10) {
     348           0 :       buff.write('... ');
     349           0 :       buff.write(s.substring(start - 10, start));
     350             :     } else {
     351           0 :       buff.write(s.substring(0, start));
     352             :     }
     353             :   }
     354             : 
     355             :   static void _writeTrailing(StringBuffer buff, String s, int start) {
     356           0 :     if (start + 10 > s.length) {
     357           0 :       buff.write(s.substring(start));
     358             :     } else {
     359           0 :       buff.write(s.substring(start, start + 10));
     360           0 :       buff.write(' ...');
     361             :     }
     362             :   }
     363             : }
     364             : 
     365             : /// A matcher that matches any value.
     366             : const Matcher anything = const _IsAnything();
     367             : 
     368             : class _IsAnything extends Matcher {
     369           0 :   const _IsAnything();
     370             :   bool matches(item, Map matchState) => true;
     371           0 :   Description describe(Description description) => description.add('anything');
     372             : }
     373             : 
     374             : /// Returns a matcher that matches if an object is an instance
     375             : /// of [T] (or a subtype).
     376             : ///
     377             : /// As types are not first class objects in Dart we can only
     378             : /// approximate this test by using a generic wrapper class.
     379             : ///
     380             : /// For example, to test whether 'bar' is an instance of type
     381             : /// 'Foo', we would write:
     382             : ///
     383             : ///     expect(bar, new isInstanceOf<Foo>());
     384             : class isInstanceOf<T> extends Matcher {
     385           1 :   const isInstanceOf();
     386             : 
     387           0 :   bool matches(obj, Map matchState) => obj is T;
     388             : 
     389             :   Description describe(Description description) =>
     390           0 :       description.add('an instance of $T');
     391             : }
     392             : 
     393             : /// A matcher that matches a function call against no exception.
     394             : ///
     395             : /// The function will be called once. Any exceptions will be silently swallowed.
     396             : /// The value passed to expect() should be a reference to the function.
     397             : /// Note that the function cannot take arguments; to handle this
     398             : /// a wrapper will have to be created.
     399             : const Matcher returnsNormally = const _ReturnsNormally();
     400             : 
     401             : class _ReturnsNormally extends Matcher {
     402           0 :   const _ReturnsNormally();
     403             : 
     404             :   bool matches(f, Map matchState) {
     405             :     try {
     406           0 :       f();
     407             :       return true;
     408             :     } catch (e, s) {
     409           0 :       addStateInfo(matchState, {'exception': e, 'stack': s});
     410             :       return false;
     411             :     }
     412             :   }
     413             : 
     414             :   Description describe(Description description) =>
     415           0 :       description.add("return normally");
     416             : 
     417             :   Description describeMismatch(
     418             :       item, Description mismatchDescription, Map matchState, bool verbose) {
     419           0 :     mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']);
     420             :     if (verbose) {
     421           0 :       mismatchDescription.add(' at ').add(matchState['stack'].toString());
     422             :     }
     423             :     return mismatchDescription;
     424             :   }
     425             : }
     426             : 
     427             : /*
     428             :  * Matchers for different exception types. Ideally we should just be able to
     429             :  * use something like:
     430             :  *
     431             :  * final Matcher throwsException =
     432             :  *     const _Throws(const isInstanceOf<Exception>());
     433             :  *
     434             :  * Unfortunately instanceOf is not working with dart2js.
     435             :  *
     436             :  * Alternatively, if static functions could be used in const expressions,
     437             :  * we could use:
     438             :  *
     439             :  * bool _isException(x) => x is Exception;
     440             :  * final Matcher isException = const _Predicate(_isException, "Exception");
     441             :  * final Matcher throwsException = const _Throws(isException);
     442             :  *
     443             :  * But currently using static functions in const expressions is not supported.
     444             :  * For now the only solution for all platforms seems to be separate classes
     445             :  * for each exception type.
     446             :  */
     447             : 
     448             : abstract class TypeMatcher extends Matcher {
     449             :   final String _name;
     450           0 :   const TypeMatcher(this._name);
     451           0 :   Description describe(Description description) => description.add(_name);
     452             : }
     453             : 
     454             : /// A matcher for Map types.
     455             : const Matcher isMap = const _IsMap();
     456             : 
     457             : class _IsMap extends TypeMatcher {
     458           0 :   const _IsMap() : super("Map");
     459           0 :   bool matches(item, Map matchState) => item is Map;
     460             : }
     461             : 
     462             : /// A matcher for List types.
     463             : const Matcher isList = const _IsList();
     464             : 
     465             : class _IsList extends TypeMatcher {
     466           0 :   const _IsList() : super("List");
     467           0 :   bool matches(item, Map matchState) => item is List;
     468             : }
     469             : 
     470             : /// Returns a matcher that matches if an object has a length property
     471             : /// that matches [matcher].
     472           0 : Matcher hasLength(matcher) => new _HasLength(wrapMatcher(matcher));
     473             : 
     474             : class _HasLength extends Matcher {
     475             :   final Matcher _matcher;
     476           0 :   const _HasLength([Matcher matcher = null]) : this._matcher = matcher;
     477             : 
     478             :   bool matches(item, Map matchState) {
     479             :     try {
     480             :       // This is harmless code that will throw if no length property
     481             :       // but subtle enough that an optimizer shouldn't strip it out.
     482           0 :       if (item.length * item.length >= 0) {
     483           0 :         return _matcher.matches(item.length, matchState);
     484             :       }
     485             :     } catch (e) {}
     486             :     return false;
     487             :   }
     488             : 
     489             :   Description describe(Description description) =>
     490           0 :       description.add('an object with length of ').addDescriptionOf(_matcher);
     491             : 
     492             :   Description describeMismatch(
     493             :       item, Description mismatchDescription, Map matchState, bool verbose) {
     494             :     try {
     495             :       // We want to generate a different description if there is no length
     496             :       // property; we use the same trick as in matches().
     497           0 :       if (item.length * item.length >= 0) {
     498             :         return mismatchDescription
     499           0 :             .add('has length of ')
     500           0 :             .addDescriptionOf(item.length);
     501             :       }
     502             :     } catch (e) {}
     503           0 :     return mismatchDescription.add('has no length property');
     504             :   }
     505             : }
     506             : 
     507             : /// Returns a matcher that matches if the match argument contains the expected
     508             : /// value.
     509             : ///
     510             : /// For [String]s this means substring matching;
     511             : /// for [Map]s it means the map has the key, and for [Iterable]s
     512             : /// it means the iterable has a matching element. In the case of iterables,
     513             : /// [expected] can itself be a matcher.
     514           0 : Matcher contains(expected) => new _Contains(expected);
     515             : 
     516             : class _Contains extends Matcher {
     517             :   final _expected;
     518             : 
     519           0 :   const _Contains(this._expected);
     520             : 
     521             :   bool matches(item, Map matchState) {
     522           0 :     if (item is String) {
     523           0 :       return item.indexOf(_expected) >= 0;
     524           0 :     } else if (item is Iterable) {
     525           0 :       if (_expected is Matcher) {
     526           0 :         return item.any((e) => _expected.matches(e, matchState));
     527             :       } else {
     528           0 :         return item.contains(_expected);
     529             :       }
     530           0 :     } else if (item is Map) {
     531           0 :       return item.containsKey(_expected);
     532             :     }
     533             :     return false;
     534             :   }
     535             : 
     536             :   Description describe(Description description) =>
     537           0 :       description.add('contains ').addDescriptionOf(_expected);
     538             : 
     539             :   Description describeMismatch(
     540             :       item, Description mismatchDescription, Map matchState, bool verbose) {
     541           0 :     if (item is String || item is Iterable || item is Map) {
     542             :       return super
     543           0 :           .describeMismatch(item, mismatchDescription, matchState, verbose);
     544             :     } else {
     545           0 :       return mismatchDescription.add('is not a string, map or iterable');
     546             :     }
     547             :   }
     548             : }
     549             : 
     550             : /// Returns a matcher that matches if the match argument is in
     551             : /// the expected value. This is the converse of [contains].
     552           0 : Matcher isIn(expected) => new _In(expected);
     553             : 
     554             : class _In extends Matcher {
     555             :   final _expected;
     556             : 
     557           0 :   const _In(this._expected);
     558             : 
     559             :   bool matches(item, Map matchState) {
     560           0 :     if (_expected is String) {
     561           0 :       return _expected.indexOf(item) >= 0;
     562           0 :     } else if (_expected is Iterable) {
     563           0 :       return _expected.any((e) => e == item);
     564           0 :     } else if (_expected is Map) {
     565           0 :       return _expected.containsKey(item);
     566             :     }
     567             :     return false;
     568             :   }
     569             : 
     570             :   Description describe(Description description) =>
     571           0 :       description.add('is in ').addDescriptionOf(_expected);
     572             : }
     573             : 
     574             : /// Returns a matcher that uses an arbitrary function that returns
     575             : /// true or false for the actual value.
     576             : ///
     577             : /// For example:
     578             : ///
     579             : ///     expect(v, predicate((x) => ((x % 2) == 0), "is even"))
     580             : Matcher predicate<T>(bool f(T value),
     581             :         [String description = 'satisfies function']) =>
     582           2 :     new _Predicate(f, description);
     583             : 
     584             : typedef bool _PredicateFunction<T>(T value);
     585             : 
     586             : class _Predicate<T> extends Matcher {
     587             :   final _PredicateFunction<T> _matcher;
     588             :   final String _description;
     589             : 
     590           2 :   _Predicate(this._matcher, this._description);
     591             : 
     592           6 :   bool matches(item, Map matchState) => _matcher(item as T);
     593             : 
     594             :   Description describe(Description description) =>
     595           0 :       description.add(_description);
     596             : }
     597             : 
     598             : /// A useful utility class for implementing other matchers through inheritance.
     599             : /// Derived classes should call the base constructor with a feature name and
     600             : /// description, and an instance matcher, and should implement the
     601             : /// [featureValueOf] abstract method.
     602             : ///
     603             : /// The feature description will typically describe the item and the feature,
     604             : /// while the feature name will just name the feature. For example, we may
     605             : /// have a Widget class where each Widget has a price; we could make a
     606             : /// [CustomMatcher] that can make assertions about prices with:
     607             : ///
     608             : /// ```dart
     609             : /// class HasPrice extends CustomMatcher {
     610             : ///   HasPrice(matcher) : super("Widget with price that is", "price", matcher);
     611             : ///   featureValueOf(actual) => actual.price;
     612             : /// }
     613             : /// ```
     614             : ///
     615             : /// and then use this for example like:
     616             : ///
     617             : /// ```dart
     618             : /// expect(inventoryItem, new HasPrice(greaterThan(0)));
     619             : /// ```
     620             : class CustomMatcher extends Matcher {
     621             :   final String _featureDescription;
     622             :   final String _featureName;
     623             :   final Matcher _matcher;
     624             : 
     625             :   CustomMatcher(this._featureDescription, this._featureName, matcher)
     626           0 :       : this._matcher = wrapMatcher(matcher);
     627             : 
     628             :   /// Override this to extract the interesting feature.
     629             :   featureValueOf(actual) => actual;
     630             : 
     631             :   bool matches(item, Map matchState) {
     632             :     try {
     633           0 :       var f = featureValueOf(item);
     634           0 :       if (_matcher.matches(f, matchState)) return true;
     635           0 :       addStateInfo(matchState, {'custom.feature': f});
     636             :     } catch (exception, stack) {
     637           0 :       addStateInfo(matchState, {
     638           0 :         'custom.exception': exception.toString(),
     639           0 :         'custom.stack': new Chain.forTrace(stack)
     640           0 :             .foldFrames(
     641             :                 (frame) =>
     642           0 :                     frame.package == 'test' ||
     643           0 :                     frame.package == 'stream_channel' ||
     644           0 :                     frame.package == 'matcher',
     645             :                 terse: true)
     646           0 :             .toString()
     647             :       });
     648             :     }
     649             :     return false;
     650             :   }
     651             : 
     652             :   Description describe(Description description) =>
     653           0 :       description.add(_featureDescription).add(' ').addDescriptionOf(_matcher);
     654             : 
     655             :   Description describeMismatch(
     656             :       item, Description mismatchDescription, Map matchState, bool verbose) {
     657           0 :     if (matchState['custom.exception'] != null) {
     658             :       mismatchDescription
     659           0 :           .add('threw ')
     660           0 :           .addDescriptionOf(matchState['custom.exception'])
     661           0 :           .add('\n')
     662           0 :           .add(matchState['custom.stack'].toString());
     663             :       return mismatchDescription;
     664             :     }
     665             : 
     666             :     mismatchDescription
     667           0 :         .add('has ')
     668           0 :         .add(_featureName)
     669           0 :         .add(' with value ')
     670           0 :         .addDescriptionOf(matchState['custom.feature']);
     671           0 :     var innerDescription = new StringDescription();
     672             : 
     673           0 :     _matcher.describeMismatch(matchState['custom.feature'], innerDescription,
     674           0 :         matchState['state'], verbose);
     675             : 
     676           0 :     if (innerDescription.length > 0) {
     677           0 :       mismatchDescription.add(' which ').add(innerDescription.toString());
     678             :     }
     679             :     return mismatchDescription;
     680             :   }
     681             : }

Generated by: LCOV version 1.13