Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(453)

Side by Side Diff: pkg/unittest/lib/src/core_matchers.dart

Issue 210413002: pkg/unittest: using matcher and mock from packages (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: releasing v0.10.1 Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/unittest/lib/mock.dart ('k') | pkg/unittest/lib/src/description.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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 part of unittest.matcher;
6
7 /**
8 * Returns a matcher that matches empty strings, maps or iterables
9 * (including collections).
10 */
11 const Matcher isEmpty = const _Empty();
12
13 class _Empty extends Matcher {
14 const _Empty();
15 bool matches(item, Map matchState) {
16 if (item is Map || item is Iterable) {
17 return item.isEmpty;
18 } else if (item is String) {
19 return item.length == 0;
20 } else {
21 return false;
22 }
23 }
24 Description describe(Description description) => description.add('empty');
25 }
26
27 /** A matcher that matches any null value. */
28 const Matcher isNull = const _IsNull();
29
30 /** A matcher that matches any non-null value. */
31 const Matcher isNotNull = const _IsNotNull();
32
33 class _IsNull extends Matcher {
34 const _IsNull();
35 bool matches(item, Map matchState) => item == null;
36 Description describe(Description description) => description.add('null');
37 }
38
39 class _IsNotNull extends Matcher {
40 const _IsNotNull();
41 bool matches(item, Map matchState) => item != null;
42 Description describe(Description description) => description.add('not null');
43 }
44
45 /** A matcher that matches the Boolean value true. */
46 const Matcher isTrue = const _IsTrue();
47
48 /** A matcher that matches anything except the Boolean value true. */
49 const Matcher isFalse = const _IsFalse();
50
51 class _IsTrue extends Matcher {
52 const _IsTrue();
53 bool matches(item, Map matchState) => item == true;
54 Description describe(Description description) => description.add('true');
55 }
56
57 class _IsFalse extends Matcher {
58 const _IsFalse();
59 bool matches(item, Map matchState) => item == false;
60 Description describe(Description description) => description.add('false');
61 }
62
63 /**
64 * Returns a matches that matches if the value is the same instance
65 * as [expected], using [identical].
66 */
67 Matcher same(expected) => new _IsSameAs(expected);
68
69 class _IsSameAs extends Matcher {
70 final _expected;
71 const _IsSameAs(this._expected);
72 bool matches(item, Map matchState) => identical(item, _expected);
73 // If all types were hashable we could show a hash here.
74 Description describe(Description description) =>
75 description.add('same instance as ').addDescriptionOf(_expected);
76 }
77
78 /**
79 * Returns a matcher that matches if the value is structurally equal to
80 * [expected].
81 *
82 * If [expected] is a [Matcher], then it matches using that. Otherwise it tests
83 * for equality using `==` on the expected value.
84 *
85 * For [Iterable]s and [Map]s, this will recursively match the elements. To
86 * handle cyclic structures a recursion depth [limit] can be provided. The
87 * default limit is 100.
88 */
89 Matcher equals(expected, [limit=100]) =>
90 expected is String
91 ? new _StringEqualsMatcher(expected)
92 : new _DeepMatcher(expected, limit);
93
94 class _DeepMatcher extends Matcher {
95 final _expected;
96 final int _limit;
97 var count;
98
99 _DeepMatcher(this._expected, [limit = 1000]): this._limit = limit;
100
101 // Returns a pair (reason, location)
102 List _compareIterables(expected, actual, matcher, depth, location) {
103 if (actual is! Iterable) return ['is not Iterable', location];
104
105 var expectedIterator = expected.iterator;
106 var actualIterator = actual.iterator;
107 for (var index = 0; ; index++) {
108 // Advance in lockstep.
109 var expectedNext = expectedIterator.moveNext();
110 var actualNext = actualIterator.moveNext();
111
112 // If we reached the end of both, we succeeded.
113 if (!expectedNext && !actualNext) return null;
114
115 // Fail if their lengths are different.
116 var newLocation = '${location}[${index}]';
117 if (!expectedNext) return ['longer than expected', newLocation];
118 if (!actualNext) return ['shorter than expected', newLocation];
119
120 // Match the elements.
121 var rp = matcher(expectedIterator.current, actualIterator.current,
122 newLocation, depth);
123 if (rp != null) return rp;
124 }
125 }
126
127 List _recursiveMatch(expected, actual, String location, int depth) {
128 // If the expected value is a matcher, try to match it.
129 if (expected is Matcher) {
130 var matchState = {};
131 if (expected.matches(actual, matchState)) return null;
132
133 var description = new StringDescription();
134 expected.describe(description);
135 return ['does not match $description', location];
136 } else {
137 // Otherwise, test for equality.
138 try {
139 if (expected == actual) return null;
140 } catch (e, s) {
141 // TODO(gram): Add a test for this case.
142 return ['== threw "$e"', location];
143 }
144 }
145
146 if (depth > _limit) return ['recursion depth limit exceeded', location];
147
148 // If _limit is 1 we can only recurse one level into object.
149 bool canRecurse = depth == 0 || _limit > 1;
150
151 if (expected is Iterable && canRecurse) {
152 return _compareIterables(expected, actual, _recursiveMatch, depth + 1,
153 location);
154 }
155
156 if (expected is Map && canRecurse) {
157 if (actual is! Map) return ['expected a map', location];
158
159 var err = (expected.length == actual.length) ? '' :
160 'has different length and ';
161 for (var key in expected.keys) {
162 if (!actual.containsKey(key)) {
163 return ["${err}is missing map key '$key'", location];
164 }
165 }
166
167 for (var key in actual.keys) {
168 if (!expected.containsKey(key)) {
169 return ["${err}has extra map key '$key'", location];
170 }
171 }
172
173 for (var key in expected.keys) {
174 var rp = _recursiveMatch(expected[key], actual[key],
175 "${location}['${key}']", depth + 1);
176 if (rp != null) return rp;
177 }
178
179 return null;
180 }
181
182 var description = new StringDescription();
183
184 // If we have recursed, show the expected value too; if not, expect() will
185 // show it for us.
186 if (depth > 0) {
187 description.add('was ').
188 addDescriptionOf(actual).
189 add(' instead of ').
190 addDescriptionOf(expected);
191 return [description.toString(), location];
192 }
193
194 // We're not adding any value to the actual value.
195 return ["", location];
196 }
197
198 String _match(expected, actual, Map matchState) {
199 var rp = _recursiveMatch(expected, actual, '', 0);
200 if (rp == null) return null;
201 var reason;
202 if (rp[0].length > 0) {
203 if (rp[1].length > 0) {
204 reason = "${rp[0]} at location ${rp[1]}";
205 } else {
206 reason = rp[0];
207 }
208 } else {
209 reason = '';
210 }
211 // Cache the failure reason in the matchState.
212 addStateInfo(matchState, {'reason': reason});
213 return reason;
214 }
215
216 bool matches(item, Map matchState) =>
217 _match(_expected, item, matchState) == null;
218
219 Description describe(Description description) =>
220 description.addDescriptionOf(_expected);
221
222 Description describeMismatch(item, Description mismatchDescription,
223 Map matchState, bool verbose) {
224 var reason = matchState['reason'];
225 // If we didn't get a good reason, that would normally be a
226 // simple 'is <value>' message. We only add that if the mismatch
227 // description is non empty (so we are supplementing the mismatch
228 // description).
229 if (reason.length == 0 && mismatchDescription.length > 0) {
230 mismatchDescription.add('is ').addDescriptionOf(item);
231 } else {
232 mismatchDescription.add(reason);
233 }
234 return mismatchDescription;
235 }
236 }
237
238 /** A special equality matcher for strings. */
239 class _StringEqualsMatcher extends Matcher {
240 final String _value;
241
242 _StringEqualsMatcher(this._value);
243
244 bool get showActualValue => true;
245
246 bool matches(item, Map matchState) => _value == item;
247
248 Description describe(Description description) =>
249 description.addDescriptionOf(_value);
250
251 Description describeMismatch(item, Description mismatchDescription,
252 Map matchState, bool verbose) {
253 if (item is! String) {
254 return mismatchDescription.addDescriptionOf(item).add('is not a string');
255 } else {
256 var buff = new StringBuffer();
257 buff.write('is different.');
258 var escapedItem = _escape(item);
259 var escapedValue = _escape(_value);
260 int minLength = escapedItem.length < escapedValue.length ?
261 escapedItem.length : escapedValue.length;
262 int start;
263 for (start = 0; start < minLength; start++) {
264 if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) {
265 break;
266 }
267 }
268 if (start == minLength) {
269 if (escapedValue.length < escapedItem.length) {
270 buff.write(' Both strings start the same, but the given value also'
271 ' has the following trailing characters: ');
272 _writeTrailing(buff, escapedItem, escapedValue.length);
273 } else {
274 buff.write(' Both strings start the same, but the given value is'
275 ' missing the following trailing characters: ');
276 _writeTrailing(buff, escapedValue, escapedItem.length);
277 }
278 } else {
279 buff.write('\nExpected: ');
280 _writeLeading(buff, escapedValue, start);
281 _writeTrailing(buff, escapedValue, start);
282 buff.write('\n Actual: ');
283 _writeLeading(buff, escapedItem, start);
284 _writeTrailing(buff, escapedItem, start);
285 buff.write('\n ');
286 for (int i = (start > 10 ? 14 : start); i > 0; i--) buff.write(' ');
287 buff.write('^\n Differ at offset $start');
288 }
289
290 return mismatchDescription.replace(buff.toString());
291 }
292 }
293
294 static String _escape(String s) =>
295 s.replaceAll('\n', '\\n').replaceAll('\r', '\\r').replaceAll('\t', '\\t');
296
297 static void _writeLeading(StringBuffer buff, String s, int start) {
298 if (start > 10) {
299 buff.write('... ');
300 buff.write(s.substring(start - 10, start));
301 } else {
302 buff.write(s.substring(0, start));
303 }
304 }
305
306 static void _writeTrailing(StringBuffer buff, String s, int start) {
307 if (start + 10 > s.length) {
308 buff.write(s.substring(start));
309 } else {
310 buff.write(s.substring(start, start + 10));
311 buff.write(' ...');
312 }
313 }
314 }
315
316 /** A matcher that matches any value. */
317 const Matcher anything = const _IsAnything();
318
319 class _IsAnything extends Matcher {
320 const _IsAnything();
321 bool matches(item, Map matchState) => true;
322 Description describe(Description description) => description.add('anything');
323 }
324
325 /**
326 * Returns a matcher that matches if an object is an instance
327 * of [type] (or a subtype).
328 *
329 * As types are not first class objects in Dart we can only
330 * approximate this test by using a generic wrapper class.
331 *
332 * For example, to test whether 'bar' is an instance of type
333 * 'Foo', we would write:
334 *
335 * expect(bar, new isInstanceOf<Foo>());
336 *
337 * To get better error message, supply a name when creating the
338 * Type wrapper; e.g.:
339 *
340 * expect(bar, new isInstanceOf<Foo>('Foo'));
341 *
342 * Note that this does not currently work in dart2js; it will
343 * match any type, and isNot(new isInstanceof<T>()) will always
344 * fail. This is because dart2js currently ignores template type
345 * parameters.
346 */
347 class isInstanceOf<T> extends Matcher {
348 final String _name;
349 const isInstanceOf([name = 'specified type']): this._name = name;
350 bool matches(obj, Map matchState) => obj is T;
351 // The description here is lame :-(
352 Description describe(Description description) =>
353 description.add('an instance of ${_name}');
354 }
355
356 /**
357 * This can be used to match two kinds of objects:
358 *
359 * * A [Function] that throws an exception when called. The function cannot
360 * take any arguments. If you want to test that a function expecting
361 * arguments throws, wrap it in another zero-argument function that calls
362 * the one you want to test.
363 *
364 * * A [Future] that completes with an exception. Note that this creates an
365 * asynchronous expectation. The call to `expect()` that includes this will
366 * return immediately and execution will continue. Later, when the future
367 * completes, the actual expectation will run.
368 */
369 const Matcher throws = const Throws();
370
371 /**
372 * This can be used to match two kinds of objects:
373 *
374 * * A [Function] that throws an exception when called. The function cannot
375 * take any arguments. If you want to test that a function expecting
376 * arguments throws, wrap it in another zero-argument function that calls
377 * the one you want to test.
378 *
379 * * A [Future] that completes with an exception. Note that this creates an
380 * asynchronous expectation. The call to `expect()` that includes this will
381 * return immediately and execution will continue. Later, when the future
382 * completes, the actual expectation will run.
383 *
384 * In both cases, when an exception is thrown, this will test that the exception
385 * object matches [matcher]. If [matcher] is not an instance of [Matcher], it
386 * will implicitly be treated as `equals(matcher)`.
387 */
388 Matcher throwsA(matcher) => new Throws(wrapMatcher(matcher));
389
390 /**
391 * A matcher that matches a function call against no exception.
392 * The function will be called once. Any exceptions will be silently swallowed.
393 * The value passed to expect() should be a reference to the function.
394 * Note that the function cannot take arguments; to handle this
395 * a wrapper will have to be created.
396 */
397 const Matcher returnsNormally = const _ReturnsNormally();
398
399 class Throws extends Matcher {
400 final Matcher _matcher;
401
402 const Throws([Matcher matcher]): this._matcher = matcher;
403
404 bool matches(item, Map matchState) {
405 if (item is! Function && item is! Future) return false;
406 if (item is Future) {
407 var done = wrapAsync((fn) => fn());
408
409 // Queue up an asynchronous expectation that validates when the future
410 // completes.
411 item.then((value) {
412 done(() => fail("Expected future to fail, but succeeded with '$value'.") );
413 }, onError: (error, trace) {
414 done(() {
415 if (_matcher == null) return;
416 var reason;
417 if (trace != null) {
418 var stackTrace = trace.toString();
419 stackTrace = " ${stackTrace.replaceAll("\n", "\n ")}";
420 reason = "Actual exception trace:\n$stackTrace";
421 }
422 expect(error, _matcher, reason: reason);
423 });
424 });
425 // It hasn't failed yet.
426 return true;
427 }
428
429 try {
430 item();
431 return false;
432 } catch (e, s) {
433 if (_matcher == null || _matcher.matches(e, matchState)) {
434 return true;
435 } else {
436 addStateInfo(matchState, {'exception': e, 'stack': s});
437 return false;
438 }
439 }
440 }
441
442 Description describe(Description description) {
443 if (_matcher == null) {
444 return description.add("throws");
445 } else {
446 return description.add('throws ').addDescriptionOf(_matcher);
447 }
448 }
449
450 Description describeMismatch(item, Description mismatchDescription,
451 Map matchState,
452 bool verbose) {
453 if (item is! Function && item is! Future) {
454 return mismatchDescription.add('is not a Function or Future');
455 } else if (_matcher == null || matchState['exception'] == null) {
456 return mismatchDescription.add('did not throw');
457 } else {
458 mismatchDescription. add('threw ').
459 addDescriptionOf(matchState['exception']);
460 if (verbose) {
461 mismatchDescription.add(' at ').add(matchState['stack'].toString());
462 }
463 return mismatchDescription;
464 }
465 }
466 }
467
468 class _ReturnsNormally extends Matcher {
469 const _ReturnsNormally();
470
471 bool matches(f, Map matchState) {
472 try {
473 f();
474 return true;
475 } catch (e, s) {
476 addStateInfo(matchState, {'exception': e, 'stack': s});
477 return false;
478 }
479 }
480
481 Description describe(Description description) =>
482 description.add("return normally");
483
484 Description describeMismatch(item, Description mismatchDescription,
485 Map matchState,
486 bool verbose) {
487 mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']);
488 if (verbose) {
489 mismatchDescription.add(' at ').add(matchState['stack'].toString());
490 }
491 return mismatchDescription;
492 }
493 }
494
495 /*
496 * Matchers for different exception types. Ideally we should just be able to
497 * use something like:
498 *
499 * final Matcher throwsException =
500 * const _Throws(const isInstanceOf<Exception>());
501 *
502 * Unfortunately instanceOf is not working with dart2js.
503 *
504 * Alternatively, if static functions could be used in const expressions,
505 * we could use:
506 *
507 * bool _isException(x) => x is Exception;
508 * final Matcher isException = const _Predicate(_isException, "Exception");
509 * final Matcher throwsException = const _Throws(isException);
510 *
511 * But currently using static functions in const expressions is not supported.
512 * For now the only solution for all platforms seems to be separate classes
513 * for each exception type.
514 */
515
516 abstract class TypeMatcher extends Matcher {
517 final String _name;
518 const TypeMatcher(this._name);
519 Description describe(Description description) => description.add(_name);
520 }
521
522 /** A matcher for FormatExceptions. */
523 const isFormatException = const _FormatException();
524
525 /** A matcher for functions that throw FormatException. */
526 const Matcher throwsFormatException = const Throws(isFormatException);
527
528 class _FormatException extends TypeMatcher {
529 const _FormatException(): super("FormatException");
530 bool matches(item, Map matchState) => item is FormatException;
531 }
532
533 /** A matcher for Exceptions. */
534 const isException = const _Exception();
535
536 /** A matcher for functions that throw Exception. */
537 const Matcher throwsException = const Throws(isException);
538
539 class _Exception extends TypeMatcher {
540 const _Exception(): super("Exception");
541 bool matches(item, Map matchState) => item is Exception;
542 }
543
544 /** A matcher for ArgumentErrors. */
545 const isArgumentError = const _ArgumentError();
546
547 /** A matcher for functions that throw ArgumentError. */
548 const Matcher throwsArgumentError = const Throws(isArgumentError);
549
550 class _ArgumentError extends TypeMatcher {
551 const _ArgumentError(): super("ArgumentError");
552 bool matches(item, Map matchState) => item is ArgumentError;
553 }
554
555 /** A matcher for RangeErrors. */
556 const isRangeError = const _RangeError();
557
558 /** A matcher for functions that throw RangeError. */
559 const Matcher throwsRangeError = const Throws(isRangeError);
560
561 class _RangeError extends TypeMatcher {
562 const _RangeError(): super("RangeError");
563 bool matches(item, Map matchState) => item is RangeError;
564 }
565
566 /** A matcher for NoSuchMethodErrors. */
567 const isNoSuchMethodError = const _NoSuchMethodError();
568
569 /** A matcher for functions that throw NoSuchMethodError. */
570 const Matcher throwsNoSuchMethodError = const Throws(isNoSuchMethodError);
571
572 class _NoSuchMethodError extends TypeMatcher {
573 const _NoSuchMethodError(): super("NoSuchMethodError");
574 bool matches(item, Map matchState) => item is NoSuchMethodError;
575 }
576
577 /** A matcher for UnimplementedErrors. */
578 const isUnimplementedError = const _UnimplementedError();
579
580 /** A matcher for functions that throw Exception. */
581 const Matcher throwsUnimplementedError = const Throws(isUnimplementedError);
582
583 class _UnimplementedError extends TypeMatcher {
584 const _UnimplementedError(): super("UnimplementedError");
585 bool matches(item, Map matchState) => item is UnimplementedError;
586 }
587
588 /** A matcher for UnsupportedError. */
589 const isUnsupportedError = const _UnsupportedError();
590
591 /** A matcher for functions that throw UnsupportedError. */
592 const Matcher throwsUnsupportedError = const Throws(isUnsupportedError);
593
594 class _UnsupportedError extends TypeMatcher {
595 const _UnsupportedError(): super("UnsupportedError");
596 bool matches(item, Map matchState) => item is UnsupportedError;
597 }
598
599 /** A matcher for StateErrors. */
600 const isStateError = const _StateError();
601
602 /** A matcher for functions that throw StateError. */
603 const Matcher throwsStateError = const Throws(isStateError);
604
605 class _StateError extends TypeMatcher {
606 const _StateError(): super("StateError");
607 bool matches(item, Map matchState) => item is StateError;
608 }
609
610 /** A matcher for FallThroughError. */
611 const isFallThroughError = const _FallThroughError();
612
613 /** A matcher for functions that throw FallThroughError. */
614 const Matcher throwsFallThroughError = const Throws(isFallThroughError);
615
616 class _FallThroughError extends TypeMatcher {
617 const _FallThroughError(): super("FallThroughError");
618 bool matches(item, Map matchState) => item is FallThroughError;
619 }
620
621 /** A matcher for NullThrownError. */
622 const isNullThrownError = const _NullThrownError();
623
624 /** A matcher for functions that throw NullThrownError. */
625 const Matcher throwsNullThrownError = const Throws(isNullThrownError);
626
627 class _NullThrownError extends TypeMatcher {
628 const _NullThrownError(): super("NullThrownError");
629 bool matches(item, Map matchState) => item is NullThrownError;
630 }
631
632 /** A matcher for ConcurrentModificationError. */
633 const isConcurrentModificationError = const _ConcurrentModificationError();
634
635 /** A matcher for functions that throw ConcurrentModificationError. */
636 const Matcher throwsConcurrentModificationError =
637 const Throws(isConcurrentModificationError);
638
639 class _ConcurrentModificationError extends TypeMatcher {
640 const _ConcurrentModificationError(): super("ConcurrentModificationError");
641 bool matches(item, Map matchState) => item is ConcurrentModificationError;
642 }
643
644 /** A matcher for AbstractClassInstantiationError. */
645 const isAbstractClassInstantiationError =
646 const _AbstractClassInstantiationError();
647
648 /** A matcher for functions that throw AbstractClassInstantiationError. */
649 const Matcher throwsAbstractClassInstantiationError =
650 const Throws(isAbstractClassInstantiationError);
651
652 class _AbstractClassInstantiationError extends TypeMatcher {
653 const _AbstractClassInstantiationError() :
654 super("AbstractClassInstantiationError");
655 bool matches(item, Map matchState) => item is AbstractClassInstantiationError;
656 }
657
658 /** A matcher for CyclicInitializationError. */
659 const isCyclicInitializationError = const _CyclicInitializationError();
660
661 /** A matcher for functions that throw CyclicInitializationError. */
662 const Matcher throwsCyclicInitializationError =
663 const Throws(isCyclicInitializationError);
664
665 class _CyclicInitializationError extends TypeMatcher {
666 const _CyclicInitializationError(): super("CyclicInitializationError");
667 bool matches(item, Map matchState) => item is CyclicInitializationError;
668 }
669
670 /** A matcher for Map types. */
671 const isMap = const _IsMap();
672
673 class _IsMap extends TypeMatcher {
674 const _IsMap(): super("Map");
675 bool matches(item, Map matchState) => item is Map;
676 }
677
678 /** A matcher for List types. */
679 const isList = const _IsList();
680
681 class _IsList extends TypeMatcher {
682 const _IsList(): super("List");
683 bool matches(item, Map matchState) => item is List;
684 }
685
686 /**
687 * Returns a matcher that matches if an object has a length property
688 * that matches [matcher].
689 */
690 Matcher hasLength(matcher) => new _HasLength(wrapMatcher(matcher));
691
692 class _HasLength extends Matcher {
693 final Matcher _matcher;
694 const _HasLength([Matcher matcher = null]): this._matcher = matcher;
695
696 bool matches(item, Map matchState) {
697 try {
698 // This is harmless code that will throw if no length property
699 // but subtle enough that an optimizer shouldn't strip it out.
700 if (item.length * item.length >= 0) {
701 return _matcher.matches(item.length, matchState);
702 }
703 } catch (e) {
704 return false;
705 }
706 }
707
708 Description describe(Description description) =>
709 description.add('an object with length of ').
710 addDescriptionOf(_matcher);
711
712 Description describeMismatch(item, Description mismatchDescription,
713 Map matchState, bool verbose) {
714 try {
715 // We want to generate a different description if there is no length
716 // property; we use the same trick as in matches().
717 if (item.length * item.length >= 0) {
718 return mismatchDescription.add('has length of ').
719 addDescriptionOf(item.length);
720 }
721 } catch (e) {
722 return mismatchDescription.add('has no length property');
723 }
724 }
725 }
726
727 /**
728 * Returns a matcher that matches if the match argument contains
729 * the expected value. For [String]s this means substring matching;
730 * for [Map]s it means the map has the key, and for [Iterable]s
731 * (including [Iterable]s) it means the iterable has a matching
732 * element. In the case of iterables, [expected] can itself be a
733 * matcher.
734 */
735 Matcher contains(expected) => new _Contains(expected);
736
737 class _Contains extends Matcher {
738
739 final _expected;
740
741 const _Contains(this._expected);
742
743 bool matches(item, Map matchState) {
744 if (item is String) {
745 return item.indexOf(_expected) >= 0;
746 } else if (item is Iterable) {
747 if (_expected is Matcher) {
748 return item.any((e) => _expected.matches(e, matchState));
749 } else {
750 return item.contains(_expected);
751 }
752 } else if (item is Map) {
753 return item.containsKey(_expected);
754 }
755 return false;
756 }
757
758 Description describe(Description description) =>
759 description.add('contains ').addDescriptionOf(_expected);
760
761 Description describeMismatch(item, Description mismatchDescription,
762 Map matchState, bool verbose) {
763 if (item is String || item is Iterable || item is Map) {
764 return super.describeMismatch(item, mismatchDescription, matchState,
765 verbose);
766 } else {
767 return mismatchDescription.add('is not a string, map or iterable');
768 }
769 }
770 }
771
772 /**
773 * Returns a matcher that matches if the match argument is in
774 * the expected value. This is the converse of [contains].
775 */
776 Matcher isIn(expected) => new _In(expected);
777
778 class _In extends Matcher {
779
780 final _expected;
781
782 const _In(this._expected);
783
784 bool matches(item, Map matchState) {
785 if (_expected is String) {
786 return _expected.indexOf(item) >= 0;
787 } else if (_expected is Iterable) {
788 return _expected.any((e) => e == item);
789 } else if (_expected is Map) {
790 return _expected.containsKey(item);
791 }
792 return false;
793 }
794
795 Description describe(Description description) =>
796 description.add('is in ').addDescriptionOf(_expected);
797 }
798
799 /**
800 * Returns a matcher that uses an arbitrary function that returns
801 * true or false for the actual value. For example:
802 *
803 * expect(v, predicate((x) => ((x % 2) == 0), "is even"))
804 */
805 Matcher predicate(Function f, [description = 'satisfies function']) =>
806 new _Predicate(f, description);
807
808 class _Predicate extends Matcher {
809
810 final Function _matcher;
811 final String _description;
812
813 const _Predicate(this._matcher, this._description);
814
815 bool matches(item, Map matchState) => _matcher(item);
816
817 Description describe(Description description) =>
818 description.add(_description);
819 }
820
821 /**
822 * A useful utility class for implementing other matchers through inheritance.
823 * Derived classes should call the base constructor with a feature name and
824 * description, and an instance matcher, and should implement the
825 * [featureValueOf] abstract method.
826 *
827 * The feature description will typically describe the item and the feature,
828 * while the feature name will just name the feature. For example, we may
829 * have a Widget class where each Widget has a price; we could make a
830 * [CustomMatcher] that can make assertions about prices with:
831 *
832 * class HasPrice extends CustomMatcher {
833 * const HasPrice(matcher) :
834 * super("Widget with price that is", "price", matcher);
835 * featureValueOf(actual) => actual.price;
836 * }
837 *
838 * and then use this for example like:
839 *
840 * expect(inventoryItem, new HasPrice(greaterThan(0)));
841 */
842 class CustomMatcher extends Matcher {
843 final String _featureDescription;
844 final String _featureName;
845 final Matcher _matcher;
846
847 CustomMatcher(this._featureDescription, this._featureName, matcher)
848 : this._matcher = wrapMatcher(matcher);
849
850 /** Override this to extract the interesting feature.*/
851 featureValueOf(actual) => actual;
852
853 bool matches(item, Map matchState) {
854 var f = featureValueOf(item);
855 if (_matcher.matches(f, matchState)) return true;
856 addStateInfo(matchState, {'feature': f});
857 return false;
858 }
859
860 Description describe(Description description) =>
861 description.add(_featureDescription).add(' ').addDescriptionOf(_matcher);
862
863 Description describeMismatch(item, Description mismatchDescription,
864 Map matchState, bool verbose) {
865 mismatchDescription.add('has ').add(_featureName).add(' with value ').
866 addDescriptionOf(matchState['feature']);
867 var innerDescription = new StringDescription();
868 _matcher.describeMismatch(matchState['feature'], innerDescription,
869 matchState['state'], verbose);
870 if (innerDescription.length > 0) {
871 mismatchDescription.add(' which ').add(innerDescription.toString());
872 }
873 return mismatchDescription;
874 }
875 }
OLDNEW
« no previous file with comments | « pkg/unittest/lib/mock.dart ('k') | pkg/unittest/lib/src/description.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698