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