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

Side by Side Diff: pkg/unittest/lib/mock.dart

Issue 11301046: Restructure pkg/unittest and pkg/webdriver to follow the pub conventions. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 8 years, 1 month 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
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 // TODO(gram) - when we can do package imports from within packages
6 // without Dartdoc choking, this should be moved to a separate package.
Siggi Cherem (dart-lang) 2012/10/31 20:54:02 same question as before
gram 2012/11/01 21:23:19 Done.
7
8 library mock;
9 import 'matcher.dart';
10
11 /**
12 * The error formatter for mocking is a bit different from the default one
13 * for unit testing; instead of the third argument being a 'reason'
14 * it is instead a [signature] describing the method signature filter
15 * that was used to select the logs that were verified.
16 */
17 String _mockingErrorFormatter(actual, Matcher matcher, String signature,
18 MatchState matchState, bool verbose) {
19 var description = new StringDescription();
20 description.add('Expected ${signature} ').addDescriptionOf(matcher).
21 add('\n but: ');
22 matcher.describeMismatch(actual, description, matchState, verbose).add('.');
23 return description.toString();
24 }
25
26 /**
27 * The failure handler for the [expect()] calls that occur in [verify()]
28 * methods in the mock objects. This calls the real failure handler used
29 * by the unit test library after formatting the error message with
30 * the custom formatter.
31 */
32 class _MockFailureHandler implements FailureHandler {
33 FailureHandler proxy;
34 _MockFailureHandler(this.proxy);
35 void fail(String reason) {
36 proxy.fail(reason);
37 }
38 void failMatch(actual, Matcher matcher, String reason,
39 MatchState matchState, bool verbose) {
40 proxy.fail(_mockingErrorFormatter(actual, matcher, reason,
41 matchState, verbose));
42 }
43 }
44
45 _MockFailureHandler _mockFailureHandler = null;
46
47 /** Sentinel value for representing no argument. */
48 class _Sentinel {
49 const _Sentinel();
50 }
51 const _noArg = const _Sentinel();
52
53 /** The ways in which a call to a mock method can be handled. */
54 class Action {
55 /** Do nothing (void method) */
56 static const IGNORE = const Action._('IGNORE');
57
58 /** Return a supplied value. */
59 static const RETURN = const Action._('RETURN');
60
61 /** Throw a supplied value. */
62 static const THROW = const Action._('THROW');
63
64 /** Call a supplied function. */
65 static const PROXY = const Action._('PROXY');
66
67 const Action._(this.name);
68
69 final String name;
70 }
71
72 /**
73 * The behavior of a method call in the mock library is specified
74 * with [Responder]s. A [Responder] has a [value] to throw
75 * or return (depending on the type of [action]),
76 * and can either be one-shot, multi-shot, or infinitely repeating,
77 * depending on the value of [count (1, greater than 1, or 0 respectively).
78 */
79 class Responder {
80 var value;
81 Action action;
82 int count;
83 Responder(this.value, [this.count = 1, this.action = Action.RETURN]);
84 }
85
86 /**
87 * A [CallMatcher] is a special matcher used to match method calls (i.e.
88 * a method name and set of arguments). It is not a [Matcher] like the
89 * unit test [Matcher], but instead represents a method name and a
90 * collection of [Matcher]s, one per argument, that will be applied
91 * to the parameters to decide if the method call is a match.
92 */
93 class CallMatcher {
94 Matcher nameFilter;
95 List<Matcher> argMatchers;
96
97 /**
98 * Constructor for [CallMatcher]. [name] can be null to
99 * match anything, or a literal [String], a predicate [Function],
100 * or a [Matcher]. The various arguments can be scalar values or
101 * [Matcher]s.
102 */
103 CallMatcher([name,
104 arg0 = _noArg,
105 arg1 = _noArg,
106 arg2 = _noArg,
107 arg3 = _noArg,
108 arg4 = _noArg,
109 arg5 = _noArg,
110 arg6 = _noArg,
111 arg7 = _noArg,
112 arg8 = _noArg,
113 arg9 = _noArg]) {
114 if (name == null) {
115 nameFilter = anything;
116 } else {
117 nameFilter = wrapMatcher(name);
118 }
119 argMatchers = new List<Matcher>();
120 if (arg0 === _noArg) return;
121 argMatchers.add(wrapMatcher(arg0));
122 if (arg1 === _noArg) return;
123 argMatchers.add(wrapMatcher(arg1));
124 if (arg2 === _noArg) return;
125 argMatchers.add(wrapMatcher(arg2));
126 if (arg3 === _noArg) return;
127 argMatchers.add(wrapMatcher(arg3));
128 if (arg4 === _noArg) return;
129 argMatchers.add(wrapMatcher(arg4));
130 if (arg5 === _noArg) return;
131 argMatchers.add(wrapMatcher(arg5));
132 if (arg6 === _noArg) return;
133 argMatchers.add(wrapMatcher(arg6));
134 if (arg7 === _noArg) return;
135 argMatchers.add(wrapMatcher(arg7));
136 if (arg8 === _noArg) return;
137 argMatchers.add(wrapMatcher(arg8));
138 if (arg9 === _noArg) return;
139 argMatchers.add(wrapMatcher(arg9));
140 }
141
142 /**
143 * We keep our behavior specifications in a Map, which is keyed
144 * by the [CallMatcher]. To make the keys unique and to get a
145 * descriptive value for the [CallMatcher] we have this override
146 * of [toString()].
147 */
148 String toString() {
149 Description d = new StringDescription();
150 d.addDescriptionOf(nameFilter);
151 // If the nameFilter was a simple string - i.e. just a method name -
152 // strip the quotes to make this more natural in appearance.
153 if (d.toString()[0] == "'") {
154 d.replace(d.toString().substring(1, d.toString().length - 1));
155 }
156 d.add('(');
157 for (var i = 0; i < argMatchers.length; i++) {
158 if (i > 0) d.add(', ');
159 d.addDescriptionOf(argMatchers[i]);
160 }
161 d.add(')');
162 return d.toString();
163 }
164
165 /**
166 * Given a [method] name and list of [arguments], return true
167 * if it matches this [CallMatcher.
168 */
169 bool matches(String method, List arguments) {
170 var matchState = new MatchState();
171 if (!nameFilter.matches(method, matchState)) {
172 return false;
173 }
174 var numArgs = (arguments == null) ? 0 : arguments.length;
175 if (numArgs < argMatchers.length) {
176 throw new Exception("Less arguments than matchers for $method.");
177 }
178 for (var i = 0; i < argMatchers.length; i++) {
179 if (!argMatchers[i].matches(arguments[i], matchState)) {
180 return false;
181 }
182 }
183 return true;
184 }
185 }
186
187 /**
188 * Returns a [CallMatcher] for the specified signature. [method] can be
189 * null to match anything, or a literal [String], a predicate [Function],
190 * or a [Matcher]. The various arguments can be scalar values or [Matcher]s.
191 */
192 CallMatcher callsTo([method,
193 arg0 = _noArg,
194 arg1 = _noArg,
195 arg2 = _noArg,
196 arg3 = _noArg,
197 arg4 = _noArg,
198 arg5 = _noArg,
199 arg6 = _noArg,
200 arg7 = _noArg,
201 arg8 = _noArg,
202 arg9 = _noArg]) {
203 return new CallMatcher(method, arg0, arg1, arg2, arg3, arg4,
204 arg5, arg6, arg7, arg8, arg9);
205 }
206
207 /**
208 * A [Behavior] represents how a [Mock] will respond to one particular
209 * type of method call.
210 */
211 class Behavior {
212 CallMatcher matcher; // The method call matcher.
213 List<Responder> actions; // The values to return/throw or proxies to call.
214 bool logging = true;
215
216 Behavior (this.matcher) {
217 actions = new List<Responder>();
218 }
219
220 /**
221 * Adds a [Responder] that returns a [value] for [count] calls
222 * (1 by default).
223 */
224 Behavior thenReturn(value, [count = 1]) {
225 actions.add(new Responder(value, count, Action.RETURN));
226 return this; // For chaining calls.
227 }
228
229 /** Adds a [Responder] that repeatedly returns a [value]. */
230 Behavior alwaysReturn(value) {
231 return thenReturn(value, 0);
232 }
233
234 /**
235 * Adds a [Responder] that throws [value] [count]
236 * times (1 by default).
237 */
238 Behavior thenThrow(value, [count = 1]) {
239 actions.add(new Responder(value, count, Action.THROW));
240 return this; // For chaining calls.
241 }
242
243 /** Adds a [Responder] that throws [value] endlessly. */
244 Behavior alwaysThrow(value) {
245 return thenThrow(value, 0);
246 }
247
248 /**
249 * [thenCall] creates a proxy Responder, that is called [count]
250 * times (1 by default; 0 is used for unlimited calls, and is
251 * exposed as [alwaysCall]). [value] is the function that will
252 * be called with the same arguments that were passed to the
253 * mock. Proxies can be used to wrap real objects or to define
254 * more complex return/throw behavior. You could even (if you
255 * wanted) use proxies to emulate the behavior of thenReturn;
256 * e.g.:
257 *
258 * m.when(callsTo('foo')).thenReturn(0)
259 *
260 * is equivalent to:
261 *
262 * m.when(callsTo('foo')).thenCall(() => 0)
263 */
264 Behavior thenCall(value, [count = 1]) {
265 actions.add(new Responder(value, count, Action.PROXY));
266 return this; // For chaining calls.
267 }
268
269 /** Creates a repeating proxy call. */
270 Behavior alwaysCall(value) {
271 return thenCall(value, 0);
272 }
273
274 /** Returns true if a method call matches the [Behavior]. */
275 bool matches(String method, List args) => matcher.matches(method, args);
276
277 /** Returns the [matcher]'s representation. */
278 String toString() => matcher.toString();
279 }
280
281 /**
282 * Every call to a [Mock] object method is logged. The logs are
283 * kept in instances of [LogEntry].
284 */
285 class LogEntry {
286 /** The time of the event. */
287 Date time;
288
289 /** The mock object name, if any. */
290 final String mockName;
291
292 /** The method name. */
293 final String methodName;
294
295 /** The parameters. */
296 final List args;
297
298 /** The behavior that resulted. */
299 final Action action;
300
301 /** The value that was returned (if no throw). */
302 final value;
303
304 LogEntry(this.mockName, this.methodName,
305 this.args, this.action, [this.value]) {
306 time = new Date.now();
307 }
308
309 String _pad2(int val) => (val >= 10 ? '$val' : '0$val');
310
311 String toString([Date baseTime]) {
312 Description d = new StringDescription();
313 if (baseTime == null) {
314 // Show absolute time.
315 d.add('${time.hour}:${_pad2(time.minute)}:'
316 '${_pad2(time.second)}.${time.millisecond}> ');
317 } else {
318 // Show relative time.
319 int delta = time.millisecondsSinceEpoch - baseTime.millisecondsSinceEpoch;
320 int secs = delta ~/ 1000;
321 int msecs = delta % 1000;
322 d.add('$secs.$msecs> ');
323 }
324 d.add('${_qualifiedName(mockName, methodName)}(');
325 if (args != null) {
326 for (var i = 0; i < args.length; i++) {
327 if (i != 0) d.add(', ');
328 d.addDescriptionOf(args[i]);
329 }
330 }
331 d.add(') ${action == Action.THROW ? "threw" : "returned"} ');
332 d.addDescriptionOf(value);
333 return d.toString();
334 }
335 }
336
337 /** Utility function for optionally qualified method names */
338 String _qualifiedName(owner, String method) {
339 if (owner == null || owner === anything) {
340 return method;
341 } else if (owner is Matcher) {
342 Description d = new StringDescription();
343 d.addDescriptionOf(owner);
344 d.add('.');
345 d.add(method);
346 return d.toString();
347 } else {
348 return '$owner.$method';
349 }
350 }
351
352 /**
353 * [StepValidator]s are used by [stepwiseValidate] in [LogEntryList], which
354 * iterates through the list and call the [StepValidator] function with the
355 * log [List] and position. The [StepValidator] should return the number of
356 * positions to advance upon success, or zero upon failure. When zero is
357 * returned an error is reported.
358 */
359 typedef int StepValidator(List<LogEntry> logs, int pos);
360
361 /**
362 * We do verification on a list of [LogEntry]s. To allow chaining
363 * of calls to verify, we encapsulate such a list in the [LogEntryList]
364 * class.
365 */
366 class LogEntryList {
367 String filter;
368 List<LogEntry> logs;
369 LogEntryList([this.filter]) {
370 logs = new List<LogEntry>();
371 }
372
373 /** Add a [LogEntry] to the log. */
374 add(LogEntry entry) => logs.add(entry);
375
376 /** Get the first entry, or null if no entries. */
377 get first => (logs == null || logs.length == 0) ? null : logs[0];
378
379 /** Get the last entry, or null if no entries. */
380 get last => (logs == null || logs.length == 0) ? null : logs.last;
381
382 /** Creates a LogEntry predicate function from the argument. */
383 Function _makePredicate(arg) {
384 if (arg == null) {
385 return (e) => true;
386 } else if (arg is CallMatcher) {
387 return (e) => arg.matches(e.methodName, e.args);
388 } else if (arg is Function) {
389 return arg;
390 } else {
391 throw new Exception("Invalid argument to _makePredicate.");
392 }
393 }
394
395 /**
396 * Create a new [LogEntryList] consisting of [LogEntry]s from
397 * this list that match the specified [mockNameFilter] and [logFilter].
398 * [mockNameFilter] can be null, a [String], a predicate [Function],
399 * or a [Matcher]. If [mockNameFilter] is null, this is the same as
400 * [anything].
401 * If [logFilter] is null, all entries in the log will be returned.
402 * Otherwise [logFilter] should be a [CallMatcher] or predicate function
403 * that takes a [LogEntry] and returns a bool.
404 * If [destructive] is true, the log entries are removed from the
405 * original list.
406 */
407 LogEntryList getMatches([mockNameFilter,
408 logFilter,
409 Matcher actionMatcher,
410 bool destructive = false]) {
411 if (mockNameFilter == null) {
412 mockNameFilter = anything;
413 } else {
414 mockNameFilter = wrapMatcher(mockNameFilter);
415 }
416 Function entryFilter = _makePredicate(logFilter);
417 String filterName = _qualifiedName(mockNameFilter, logFilter.toString());
418 LogEntryList rtn = new LogEntryList(filterName);
419 MatchState matchState = new MatchState();
420 for (var i = 0; i < logs.length; i++) {
421 LogEntry entry = logs[i];
422 if (mockNameFilter.matches(entry.mockName, matchState) &&
423 entryFilter(entry)) {
424 if (actionMatcher == null ||
425 actionMatcher.matches(entry, matchState)) {
426 rtn.add(entry);
427 if (destructive) {
428 logs.removeRange(i--, 1);
429 }
430 }
431 }
432 }
433 return rtn;
434 }
435
436 /** Apply a unit test [Matcher] to the [LogEntryList]. */
437 LogEntryList verify(Matcher matcher) {
438 if (_mockFailureHandler == null) {
439 _mockFailureHandler =
440 new _MockFailureHandler(getOrCreateExpectFailureHandler());
441 }
442 expect(logs, matcher, reason:filter, failureHandler: _mockFailureHandler);
443 return this;
444 }
445
446 /**
447 * Iterate through the list and call the [validator] function with the
448 * log [List] and position. The [validator] should return the number of
449 * positions to advance upon success, or zero upon failure. When zero is
450 * returned an error is reported. [reason] can be used to provide a
451 * more descriptive failure message. If a failure occurred false will be
452 * returned (unless the failure handler itself threw an exception);
453 * otherwise true is returned.
454 * The use case here is to perform more complex validations; for example
455 * we may want to assert that the return value from some function is
456 * later used as a parameter to a following function. If we filter the logs
457 * to include just these two functions we can write a simple validator to
458 * do this check.
459 */
460 bool stepwiseValidate(StepValidator validator, [String reason = '']) {
461 if (_mockFailureHandler == null) {
462 _mockFailureHandler =
463 new _MockFailureHandler(getOrCreateExpectFailureHandler());
464 }
465 var i = 0;
466 while (i < logs.length) {
467 var n = validator(logs, i);
468 if (n == 0) {
469 if (reason.length > 0) {
470 reason = ': $reason';
471 }
472 _mockFailureHandler.fail("Stepwise validation failed at $filter "
473 "position $i$reason");
474 return false;
475 } else {
476 i += n;
477 }
478 }
479 return true;
480 }
481
482 /**
483 * Turn the logs into human-readable text. If [baseTime] is specified
484 * then each entry is prefixed with the offset from that time in
485 * milliseconds; otherwise the time of day is used.
486 */
487 String toString([Date baseTime]) {
488 String s = '';
489 for (var e in logs) {
490 s = '$s${e.toString(baseTime)}\n';
491 }
492 return s;
493 }
494
495 /**
496 * Find the first log entry that satisfies [logFilter] and
497 * return its position. A search [start] position can be provided
498 * to allow for repeated searches. [logFilter] can be a [CallMatcher],
499 * or a predicate function that takes a [LogEntry] argument and returns
500 * a bool. If [logFilter] is null, it will match any [LogEntry].
501 * If no entry is found, then [failureReturnValue] is returned.
502 * After each check the position is updated by [skip], so using
503 * [skip] of -1 allows backward searches, using a [skip] of 2 can
504 * be used to check pairs of adjacent entries, and so on.
505 */
506 int findLogEntry(logFilter, [int start = 0, int failureReturnValue = -1,
507 skip = 1]) {
508 logFilter = _makePredicate(logFilter);
509 int pos = start;
510 while (pos >= 0 && pos < logs.length) {
511 if (logFilter(logs[pos])) {
512 return pos;
513 }
514 pos += skip;
515 }
516 return failureReturnValue;
517 }
518
519 /**
520 * Returns log events that happened up to the first one that
521 * satisfies [logFilter]. If [inPlace] is true, then returns
522 * this LogEntryList after removing the from the first satisfier;
523 * onwards otherwise a new list is created. [description]
524 * is used to create a new name for the resulting list.
525 * [defaultPosition] is used as the index of the matching item in
526 * the case that no match is found.
527 */
528 LogEntryList _head(logFilter, bool inPlace,
529 String description, int defaultPosition) {
530 if (filter != null) {
531 description = '$filter $description';
532 }
533 int pos = findLogEntry(logFilter, 0, defaultPosition);
534 if (inPlace) {
535 if (pos < logs.length) {
536 logs.removeRange(pos, logs.length - pos);
537 }
538 filter = description;
539 return this;
540 } else {
541 LogEntryList newList = new LogEntryList(description);
542 for (var i = 0; i < pos; i++) {
543 newList.logs.add(logs[i]);
544 }
545 return newList;
546 }
547 }
548
549 /**
550 * Returns log events that happened from the first one that
551 * satisfies [logFilter]. If [inPlace] is true, then returns
552 * this LogEntryList after removing the entries up to the first
553 * satisfier; otherwise a new list is created. [description]
554 * is used to create a new name for the resulting list.
555 * [defaultPosition] is used as the index of the matching item in
556 * the case that no match is found.
557 */
558 LogEntryList _tail(logFilter, bool inPlace,
559 String description, int defaultPosition) {
560 if (filter != null) {
561 description = '$filter $description';
562 }
563 int pos = findLogEntry(logFilter, 0, defaultPosition);
564 if (inPlace) {
565 if (pos > 0) {
566 logs.removeRange(0, pos);
567 }
568 filter = description;
569 return this;
570 } else {
571 LogEntryList newList = new LogEntryList(description);
572 while (pos < logs.length) {
573 newList.logs.add(logs[pos++]);
574 }
575 return newList;
576 }
577 }
578
579 /**
580 * Returns log events that happened after [when]. If [inPlace]
581 * is true, then it returns this LogEntryList after removing
582 * the entries that happened up to [when]; otherwise a new
583 * list is created.
584 */
585 LogEntryList after(Date when, [bool inPlace = false]) =>
586 _tail((e) => e.time > when, inPlace, 'after $when', logs.length);
587
588 /**
589 * Returns log events that happened from [when] onwards. If
590 * [inPlace] is true, then it returns this LogEntryList after
591 * removing the entries that happened before [when]; otherwise
592 * a new list is created.
593 */
594 LogEntryList from(Date when, [bool inPlace = false]) =>
595 _tail((e) => e.time >= when, inPlace, 'from $when', logs.length);
596
597 /**
598 * Returns log events that happened until [when]. If [inPlace]
599 * is true, then it returns this LogEntryList after removing
600 * the entries that happened after [when]; otherwise a new
601 * list is created.
602 */
603 LogEntryList until(Date when, [bool inPlace = false]) =>
604 _head((e) => e.time > when, inPlace, 'until $when', logs.length);
605
606 /**
607 * Returns log events that happened before [when]. If [inPlace]
608 * is true, then it returns this LogEntryList after removing
609 * the entries that happened from [when] onwards; otherwise a new
610 * list is created.
611 */
612 LogEntryList before(Date when, [bool inPlace = false]) =>
613 _head((e) => e.time >= when, inPlace, 'before $when', logs.length);
614
615 /**
616 * Returns log events that happened after [logEntry]'s time.
617 * If [inPlace] is true, then it returns this LogEntryList after
618 * removing the entries that happened up to [when]; otherwise a new
619 * list is created. If [logEntry] is null the current time is used.
620 */
621 LogEntryList afterEntry(LogEntry logEntry, [bool inPlace = false]) =>
622 after(logEntry == null ? new Date.now() : logEntry.time);
623
624 /**
625 * Returns log events that happened from [logEntry]'s time onwards.
626 * If [inPlace] is true, then it returns this LogEntryList after
627 * removing the entries that happened before [when]; otherwise
628 * a new list is created. If [logEntry] is null the current time is used.
629 */
630 LogEntryList fromEntry(LogEntry logEntry, [bool inPlace = false]) =>
631 from(logEntry == null ? new Date.now() : logEntry.time);
632
633 /**
634 * Returns log events that happened until [logEntry]'s time. If
635 * [inPlace] is true, then it returns this LogEntryList after removing
636 * the entries that happened after [when]; otherwise a new
637 * list is created. If [logEntry] is null the epoch time is used.
638 */
639 LogEntryList untilEntry(LogEntry logEntry, [bool inPlace = false]) =>
640 until(logEntry == null ?
641 new Date.fromMillisecondsSinceEpoch(0) : logEntry.time);
642
643 /**
644 * Returns log events that happened before [logEntry]'s time. If
645 * [inPlace] is true, then it returns this LogEntryList after removing
646 * the entries that happened from [when] onwards; otherwise a new
647 * list is created. If [logEntry] is null the epoch time is used.
648 */
649 LogEntryList beforeEntry(LogEntry logEntry, [bool inPlace = false]) =>
650 before(logEntry == null ?
651 new Date.fromMillisecondsSinceEpoch(0) : logEntry.time);
652
653 /**
654 * Returns log events that happened after the first event in [segment].
655 * If [inPlace] is true, then it returns this LogEntryList after removing
656 * the entries that happened earlier; otherwise a new list is created.
657 */
658 LogEntryList afterFirst(LogEntryList segment, [bool inPlace = false]) =>
659 afterEntry(segment.first, inPlace);
660
661 /**
662 * Returns log events that happened after the last event in [segment].
663 * If [inPlace] is true, then it returns this LogEntryList after removing
664 * the entries that happened earlier; otherwise a new list is created.
665 */
666 LogEntryList afterLast(LogEntryList segment, [bool inPlace = false]) =>
667 afterEntry(segment.last, inPlace);
668
669 /**
670 * Returns log events that happened from the time of the first event in
671 * [segment] onwards. If [inPlace] is true, then it returns this
672 * LogEntryList after removing the earlier entries; otherwise a new list
673 * is created.
674 */
675 LogEntryList fromFirst(LogEntryList segment, [bool inPlace = false]) =>
676 fromEntry(segment.first, inPlace);
677
678 /**
679 * Returns log events that happened from the time of the last event in
680 * [segment] onwards. If [inPlace] is true, then it returns this
681 * LogEntryList after removing the earlier entries; otherwise a new list
682 * is created.
683 */
684 LogEntryList fromLast(LogEntryList segment, [bool inPlace = false]) =>
685 fromEntry(segment.last, inPlace);
686
687 /**
688 * Returns log events that happened until the first event in [segment].
689 * If [inPlace] is true, then it returns this LogEntryList after removing
690 * the entries that happened later; otherwise a new list is created.
691 */
692 LogEntryList untilFirst(LogEntryList segment, [bool inPlace = false]) =>
693 untilEntry(segment.first, inPlace);
694
695 /**
696 * Returns log events that happened until the last event in [segment].
697 * If [inPlace] is true, then it returns this LogEntryList after removing
698 * the entries that happened later; otherwise a new list is created.
699 */
700 LogEntryList untilLast(LogEntryList segment, [bool inPlace = false]) =>
701 untilEntry(segment.last, inPlace);
702
703 /**
704 * Returns log events that happened before the first event in [segment].
705 * If [inPlace] is true, then it returns this LogEntryList after removing
706 * the entries that happened later; otherwise a new list is created.
707 */
708 LogEntryList beforeFirst(LogEntryList segment, [bool inPlace = false]) =>
709 beforeEntry(segment.first, inPlace);
710
711 /**
712 * Returns log events that happened before the last event in [segment].
713 * If [inPlace] is true, then it returns this LogEntryList after removing
714 * the entries that happened later; otherwise a new list is created.
715 */
716 LogEntryList beforeLast(LogEntryList segment, [bool inPlace = false]) =>
717 beforeEntry(segment.last, inPlace);
718
719 /**
720 * Iterate through the LogEntryList looking for matches to the entries
721 * in [keys]; for each match found the closest [distance] neighboring log
722 * entries that match [mockNameFilter] and [logFilter] will be included in
723 * the result. If [isPreceding] is true we use the neighbors that precede
724 * the matched entry; else we use the neighbors that followed.
725 * If [includeKeys] is true then the entries in [keys] that resulted in
726 * entries in the output list are themselves included in the output list. If
727 * [distance] is zero then all matches are included.
728 */
729 LogEntryList _neighboring(bool isPreceding,
730 LogEntryList keys,
731 mockNameFilter,
732 logFilter,
733 int distance,
734 bool includeKeys) {
735 String filterName = 'Calls to '
736 '${_qualifiedName(mockNameFilter, logFilter.toString())} '
737 '${isPreceding?"preceding":"following"} ${keys.filter}';
738
739 LogEntryList rtn = new LogEntryList(filterName);
740
741 // Deal with the trivial case.
742 if (logs.length == 0 || keys.logs.length == 0) {
743 return rtn;
744 }
745
746 // Normalize the mockNameFilter and logFilter values.
747 if (mockNameFilter == null) {
748 mockNameFilter = anything;
749 } else {
750 mockNameFilter = wrapMatcher(mockNameFilter);
751 }
752 logFilter = _makePredicate(logFilter);
753
754 // The scratch list is used to hold matching entries when we
755 // are doing preceding neighbors. The remainingCount is used to
756 // keep track of how many matching entries we can still add in the
757 // current segment (0 if we are doing doing following neighbors, until
758 // we get our first key match).
759 List scratch = null;
760 int remainingCount = 0;
761 if (isPreceding) {
762 scratch = new List();
763 remainingCount = logs.length;
764 }
765
766 var keyIterator = keys.logs.iterator();
767 LogEntry keyEntry = keyIterator.next();
768 MatchState matchState = new MatchState();
769
770 for (LogEntry logEntry in logs) {
771 // If we have a log entry match, copy the saved matches from the
772 // scratch buffer into the return list, as well as the matching entry,
773 // if appropriate, and reset the scratch buffer. Continue processing
774 // from the next key entry.
775 if (keyEntry == logEntry) {
776 if (scratch != null) {
777 int numToCopy = scratch.length;
778 if (distance > 0 && distance < numToCopy) {
779 numToCopy = distance;
780 }
781 for (var i = scratch.length - numToCopy; i < scratch.length; i++) {
782 rtn.logs.add(scratch[i]);
783 }
784 scratch.clear();
785 } else {
786 remainingCount = distance > 0 ? distance : logs.length;
787 }
788 if (includeKeys) {
789 rtn.logs.add(keyEntry);
790 }
791 if (keyIterator.hasNext) {
792 keyEntry = keyIterator.next();
793 } else if (isPreceding) { // We're done.
794 break;
795 }
796 } else if (remainingCount > 0 &&
797 mockNameFilter.matches(logEntry.mockName, matchState) &&
798 logFilter(logEntry)) {
799 if (scratch != null) {
800 scratch.add(logEntry);
801 } else {
802 rtn.logs.add(logEntry);
803 --remainingCount;
804 }
805 }
806 }
807 return rtn;
808 }
809
810 /**
811 * Iterate through the LogEntryList looking for matches to the entries
812 * in [keys]; for each match found the closest [distance] prior log entries
813 * that match [mocknameFilter] and [logFilter] will be included in the result.
814 * If [includeKeys] is true then the entries in [keys] that resulted in
815 * entries in the output list are themselves included in the output list. If
816 * [distance] is zero then all matches are included.
817 *
818 * The idea here is that you could find log entries that are related to
819 * other logs entries in some temporal sense. For example, say we have a
820 * method commit() that returns -1 on failure. Before commit() gets called
821 * the value being committed is created by process(). We may want to find
822 * the calls to process() that preceded calls to commit() that failed.
823 * We could do this with:
824 *
825 * print(log.preceding(log.getLogs(callsTo('commit'), returning(-1)),
826 * logFilter: callsTo('process')).toString());
827 *
828 * We might want to include the details of the failing calls to commit()
829 * to see what parameters were passed in, in which case we would set
830 * [includeKeys].
831 *
832 * As another simple example, say we wanted to know the three method
833 * calls that immediately preceded each failing call to commit():
834 *
835 * print(log.preceding(log.getLogs(callsTo('commit'), returning(-1)),
836 * distance: 3).toString());
837 */
838 LogEntryList preceding(LogEntryList keys,
839 {mockNameFilter: null,
840 logFilter: null,
841 int distance: 1,
842 bool includeKeys: false}) =>
843 _neighboring(true, keys, mockNameFilter, logFilter,
844 distance, includeKeys);
845
846 /**
847 * Iterate through the LogEntryList looking for matches to the entries
848 * in [keys]; for each match found the closest [distance] subsequent log
849 * entries that match [mocknameFilter] and [logFilter] will be included in
850 * the result. If [includeKeys] is true then the entries in [keys] that
851 * resulted in entries in the output list are themselves included in the
852 * output list. If [distance] is zero then all matches are included.
853 * See [preceding] for a usage example.
854 */
855 LogEntryList following(LogEntryList keys,
856 {mockNameFilter: null,
857 logFilter: null,
858 int distance: 1,
859 bool includeKeys: false}) =>
860 _neighboring(false, keys, mockNameFilter, logFilter,
861 distance, includeKeys);
862 }
863
864 /**
865 * [_TimesMatcher]s are used to make assertions about the number of
866 * times a method was called.
867 */
868 class _TimesMatcher extends BaseMatcher {
869 final int min, max;
870
871 const _TimesMatcher(this.min, [this.max = -1]);
872
873 bool matches(logList, MatchState matchState) => logList.length >= min &&
874 (max < 0 || logList.length <= max);
875
876 Description describe(Description description) {
877 description.add('to be called ');
878 if (max < 0) {
879 description.add('at least $min');
880 } else if (max == min) {
881 description.add('$max');
882 } else if (min == 0) {
883 description.add('at most $max');
884 } else {
885 description.add('between $min and $max');
886 }
887 return description.add(' times');
888 }
889
890 Description describeMismatch(logList, Description mismatchDescription,
891 MatchState matchState, bool verbose) =>
892 mismatchDescription.add('was called ${logList.length} times');
893 }
894
895 /** [happenedExactly] matches an exact number of calls. */
896 Matcher happenedExactly(count) {
897 return new _TimesMatcher(count, count);
898 }
899
900 /** [happenedAtLeast] matches a minimum number of calls. */
901 Matcher happenedAtLeast(count) {
902 return new _TimesMatcher(count);
903 }
904
905 /** [happenedAtMost] matches a maximum number of calls. */
906 Matcher happenedAtMost(count) {
907 return new _TimesMatcher(0, count);
908 }
909
910 /** [neverHappened] matches zero calls. */
911 const Matcher neverHappened = const _TimesMatcher(0, 0);
912
913 /** [happenedOnce] matches exactly one call. */
914 const Matcher happenedOnce = const _TimesMatcher(1, 1);
915
916 /** [happenedAtLeastOnce] matches one or more calls. */
917 const Matcher happenedAtLeastOnce = const _TimesMatcher(1);
918
919 /** [happenedAtMostOnce] matches zero or one call. */
920 const Matcher happenedAtMostOnce = const _TimesMatcher(0, 1);
921
922 /**
923 * [_ResultMatcher]s are used to make assertions about the results
924 * of method calls. These can be used as optional parameters to [getLogs].
925 */
926 class _ResultMatcher extends BaseMatcher {
927 final Action action;
928 final Matcher value;
929
930 const _ResultMatcher(this.action, this.value);
931
932 bool matches(item, MatchState matchState) {
933 if (item is! LogEntry) {
934 return false;
935 }
936 // normalize the action; _PROXY is like _RETURN.
937 Action eaction = item.action;
938 if (eaction == Action.PROXY) {
939 eaction = Action.RETURN;
940 }
941 return (eaction == action && value.matches(item.value, matchState));
942 }
943
944 Description describe(Description description) {
945 description.add(' to ');
946 if (action == Action.RETURN || action == Action.PROXY)
947 description.add('return ');
948 else
949 description.add('throw ');
950 return description.addDescriptionOf(value);
951 }
952
953 Description describeMismatch(item, Description mismatchDescription,
954 MatchState matchState, bool verbose) {
955 if (item.action == Action.RETURN || item.action == Action.PROXY) {
956 mismatchDescription.add('returned ');
957 } else {
958 mismatchDescription.add('threw ');
959 }
960 mismatchDescription.add(item.value);
961 return mismatchDescription;
962 }
963 }
964
965 /**
966 *[returning] matches log entries where the call to a method returned
967 * a value that matched [value].
968 */
969 Matcher returning(value) =>
970 new _ResultMatcher(Action.RETURN, wrapMatcher(value));
971
972 /**
973 *[throwing] matches log entrues where the call to a method threw
974 * a value that matched [value].
975 */
976 Matcher throwing(value) =>
977 new _ResultMatcher(Action.THROW, wrapMatcher(value));
978
979 /** Special values for use with [_ResultSetMatcher] [frequency]. */
980 class _Frequency {
981 /** Every call/throw must match */
982 static const ALL = const _Frequency._('ALL');
983
984 /** At least one call/throw must match. */
985 static const SOME = const _Frequency._('SOME');
986
987 /** No calls/throws should match. */
988 static const NONE = const _Frequency._('NONE');
989
990 const _Frequency._(this.name);
991
992 final String name;
993 }
994
995 /**
996 * [_ResultSetMatcher]s are used to make assertions about the results
997 * of method calls. When filtering an execution log by calling
998 * [getLogs], a [LogEntrySet] of matching call logs is returned;
999 * [_ResultSetMatcher]s can then assert various things about this
1000 * (sub)set of logs.
1001 *
1002 * We could make this class use _ResultMatcher but it doesn't buy that
1003 * match and adds some perf hit, so there is some duplication here.
1004 */
1005 class _ResultSetMatcher extends BaseMatcher {
1006 final Action action;
1007 final Matcher value;
1008 final _Frequency frequency; // ALL, SOME, or NONE.
1009
1010 const _ResultSetMatcher(this.action, this.value, this.frequency);
1011
1012 bool matches(logList, MatchState matchState) {
1013 for (LogEntry entry in logList) {
1014 // normalize the action; PROXY is like RETURN.
1015 Action eaction = entry.action;
1016 if (eaction == Action.PROXY) {
1017 eaction = Action.RETURN;
1018 }
1019 if (eaction == action && value.matches(entry.value, matchState)) {
1020 if (frequency == _Frequency.NONE) {
1021 matchState.state = {
1022 'state' : matchState.state,
1023 'entry' : entry
1024 };
1025 return false;
1026 } else if (frequency == _Frequency.SOME) {
1027 return true;
1028 }
1029 } else {
1030 // Mismatch.
1031 if (frequency == _Frequency.ALL) { // We need just one mismatch to fail.
1032 matchState.state = {
1033 'state' : matchState.state,
1034 'entry' : entry
1035 };
1036 return false;
1037 }
1038 }
1039 }
1040 // If we get here, then if count is _ALL we got all matches and
1041 // this is success; otherwise we got all mismatched which is
1042 // success for count == _NONE and failure for count == _SOME.
1043 return (frequency != _Frequency.SOME);
1044 }
1045
1046 Description describe(Description description) {
1047 description.add(' to ');
1048 description.add(frequency == _Frequency.ALL ? 'alway ' :
1049 (frequency == _Frequency.NONE ? 'never ' : 'sometimes '));
1050 if (action == Action.RETURN || action == Action.PROXY)
1051 description.add('return ');
1052 else
1053 description.add('throw ');
1054 return description.addDescriptionOf(value);
1055 }
1056
1057 Description describeMismatch(logList, Description mismatchDescription,
1058 MatchState matchState, bool verbose) {
1059 if (frequency != _Frequency.SOME) {
1060 LogEntry entry = matchState.state['entry'];
1061 if (entry.action == Action.RETURN || entry.action == Action.PROXY) {
1062 mismatchDescription.add('returned');
1063 } else {
1064 mismatchDescription.add('threw');
1065 }
1066 mismatchDescription.add(' value that ');
1067 value.describeMismatch(entry.value, mismatchDescription,
1068 matchState.state['state'], verbose);
1069 mismatchDescription.add(' at least once');
1070 } else {
1071 mismatchDescription.add('never did');
1072 }
1073 return mismatchDescription;
1074 }
1075 }
1076
1077 /**
1078 *[alwaysReturned] asserts that all matching calls to a method returned
1079 * a value that matched [value].
1080 */
1081 Matcher alwaysReturned(value) =>
1082 new _ResultSetMatcher(Action.RETURN, wrapMatcher(value), _Frequency.ALL);
1083
1084 /**
1085 *[sometimeReturned] asserts that at least one matching call to a method
1086 * returned a value that matched [value].
1087 */
1088 Matcher sometimeReturned(value) =>
1089 new _ResultSetMatcher(Action.RETURN, wrapMatcher(value), _Frequency.SOME);
1090
1091 /**
1092 *[neverReturned] asserts that no matching calls to a method returned
1093 * a value that matched [value].
1094 */
1095 Matcher neverReturned(value) =>
1096 new _ResultSetMatcher(Action.RETURN, wrapMatcher(value), _Frequency.NONE);
1097
1098 /**
1099 *[alwaysThrew] asserts that all matching calls to a method threw
1100 * a value that matched [value].
1101 */
1102 Matcher alwaysThrew(value) =>
1103 new _ResultSetMatcher(Action.THROW, wrapMatcher(value), _Frequency.ALL);
1104
1105 /**
1106 *[sometimeThrew] asserts that at least one matching call to a method threw
1107 * a value that matched [value].
1108 */
1109 Matcher sometimeThrew(value) =>
1110 new _ResultSetMatcher(Action.THROW, wrapMatcher(value), _Frequency.SOME);
1111
1112 /**
1113 *[neverThrew] asserts that no matching call to a method threw
1114 * a value that matched [value].
1115 */
1116 Matcher neverThrew(value) =>
1117 new _ResultSetMatcher(Action.THROW, wrapMatcher(value), _Frequency.NONE);
1118
1119 /** The shared log used for named mocks. */
1120 LogEntryList sharedLog = null;
1121
1122 /**
1123 * [Mock] is the base class for all mocked objects, with
1124 * support for basic mocking.
1125 *
1126 * To create a mock objects for some class T, create a new class using:
1127 *
1128 * class MockT extends Mock implements T {};
1129 *
1130 * Then specify the [Behavior] of the Mock for different methods using
1131 * [when] (to select the method and parameters) and then the [Action]s
1132 * for the [Behavior] by calling [thenReturn], [alwaysReturn], [thenThrow],
1133 * [alwaysThrow], [thenCall] or [alwaysCall].
1134 *
1135 * [thenReturn], [thenThrow] and [thenCall] are one-shot so you would
1136 * typically call these more than once to specify a sequence of actions;
1137 * this can be done with chained calls, e.g.:
1138 *
1139 * m.when(callsTo('foo')).
1140 * thenReturn(0).thenReturn(1).thenReturn(2);
1141 *
1142 * [thenCall] and [alwaysCall] allow you to proxy mocked methods, chaining
1143 * to some other implementation. This provides a way to implement 'spies'.
1144 *
1145 * You can disable logging for a particular [Behavior] easily:
1146 *
1147 * m.when(callsTo('bar')).logging = false;
1148 *
1149 * You can then use the mock object. Once you are done, to verify the
1150 * behavior, use [getLogs] to extract a relevant subset of method call
1151 * logs and apply [Matchers] to these through calling [verify].
1152 *
1153 * A Mock can be given a name when constructed. In this case instead of
1154 * keeping its own log, it uses a shared log. This can be useful to get an
1155 * audit trail of interleaved behavior. It is the responsibility of the user
1156 * to ensure that mock names, if used, are unique.
1157 *
1158 * Limitations:
1159 * - only positional parameters are supported (up to 10);
1160 * - to mock getters you will need to include parentheses in the call
1161 * (e.g. m.length() will work but not m.length).
1162 *
1163 * Here is a simple example:
1164 *
1165 * class MockList extends Mock implements List {};
1166 *
1167 * List m = new MockList();
1168 * m.when(callsTo('add', anything)).alwaysReturn(0);
1169 *
1170 * m.add('foo');
1171 * m.add('bar');
1172 *
1173 * getLogs(m, callsTo('add', anything)).verify(happenedExactly(2));
1174 * getLogs(m, callsTo('add', 'foo')).verify(happenedOnce);
1175 * getLogs(m, callsTo('add', 'isNull)).verify(neverHappened);
1176 *
1177 * Note that we don't need to provide argument matchers for all arguments,
1178 * but we do need to provide arguments for all matchers. So this is allowed:
1179 *
1180 * m.when(callsTo('add')).alwaysReturn(0);
1181 * m.add(1, 2);
1182 *
1183 * But this is not allowed and will throw an exception:
1184 *
1185 * m.when(callsTo('add', anything, anything)).alwaysReturn(0);
1186 * m.add(1);
1187 *
1188 * Here is a way to implement a 'spy', which is where we log the call
1189 * but then hand it off to some other function, which is the same
1190 * method in a real instance of the class being mocked:
1191 *
1192 * class Foo {
1193 * bar(a, b, c) => a + b + c;
1194 * }
1195 *
1196 * class MockFoo extends Mock implements Foo {
1197 * Foo real;
1198 * MockFoo() {
1199 * real = new Foo();
1200 * this.when(callsTo('bar')).alwaysCall(real.bar);
1201 * }
1202 * }
1203 *
1204 */
1205 class Mock {
1206 /** The mock name. Needed if the log is shared; optional otherwise. */
1207 final String name;
1208
1209 /** The set of [Behavior]s supported. */
1210 Map<String,Behavior> _behaviors;
1211
1212 /** The [log] of calls made. Only used if [name] is null. */
1213 LogEntryList log;
1214
1215 /** How to handle unknown method calls - swallow or throw. */
1216 final bool _throwIfNoBehavior;
1217
1218 /** Whether to create an audit log or not. */
1219 bool _logging;
1220
1221 bool get logging => _logging;
1222 set logging(bool value) {
1223 if (value && log == null) {
1224 log = new LogEntryList();
1225 }
1226 _logging = value;
1227 }
1228
1229 /**
1230 * Default constructor. Unknown method calls are allowed and logged,
1231 * the mock has no name, and has its own log.
1232 */
1233 Mock() : _throwIfNoBehavior = false, log = null, name = null {
1234 logging = true;
1235 _behaviors = new Map<String,Behavior>();
1236 }
1237
1238 /**
1239 * This constructor makes a mock that has a [name] and possibly uses
1240 * a shared [log]. If [throwIfNoBehavior] is true, any calls to methods
1241 * that have no defined behaviors will throw an exception; otherwise they
1242 * will be allowed and logged (but will not do anything).
1243 * If [enableLogging] is false, no logging will be done initially (whether
1244 * or not a [log] is supplied), but [logging] can be set to true later.
1245 */
1246 Mock.custom({this.name,
1247 this.log,
1248 throwIfNoBehavior: false,
1249 enableLogging: true}) : _throwIfNoBehavior = throwIfNoBehavior {
1250 if (log != null && name == null) {
1251 throw new Exception("Mocks with shared logs must have a name.");
1252 }
1253 logging = enableLogging;
1254 _behaviors = new Map<String,Behavior>();
1255 }
1256
1257 /**
1258 * [when] is used to create a new or extend an existing [Behavior].
1259 * A [CallMatcher] [filter] must be supplied, and the [Behavior]s for
1260 * that signature are returned (being created first if needed).
1261 *
1262 * Typical use case:
1263 *
1264 * mock.when(callsTo(...)).alwaysReturn(...);
1265 */
1266 Behavior when(CallMatcher logFilter) {
1267 String key = logFilter.toString();
1268 if (!_behaviors.containsKey(key)) {
1269 Behavior b = new Behavior(logFilter);
1270 _behaviors[key] = b;
1271 return b;
1272 } else {
1273 return _behaviors[key];
1274 }
1275 }
1276
1277 /**
1278 * This is the handler for method calls. We loo through the list
1279 * of [Behavior]s, and find the first match that still has return
1280 * values available, and then do the action specified by that
1281 * return value. If we find no [Behavior] to apply an exception is
1282 * thrown.
1283 */
1284 noSuchMethod(String method, List args) {
1285 if (method.startsWith('get:')) {
1286 method = 'get ${method.substring(4)}';
1287 }
1288 bool matchedMethodName = false;
1289 MatchState matchState = new MatchState();
1290 for (String k in _behaviors.keys) {
1291 Behavior b = _behaviors[k];
1292 if (b.matcher.nameFilter.matches(method, matchState)) {
1293 matchedMethodName = true;
1294 }
1295 if (b.matches(method, args)) {
1296 List actions = b.actions;
1297 if (actions == null || actions.length == 0) {
1298 continue; // No return values left in this Behavior.
1299 }
1300 // Get the first response.
1301 Responder response = actions[0];
1302 // If it is exhausted, remove it from the list.
1303 // Note that for endlessly repeating values, we started the count at
1304 // 0, so we get a potentially useful value here, which is the
1305 // (negation of) the number of times we returned the value.
1306 if (--response.count == 0) {
1307 actions.removeRange(0, 1);
1308 }
1309 // Do the response.
1310 Action action = response.action;
1311 var value = response.value;
1312 if (action == Action.RETURN) {
1313 if (_logging && b.logging) {
1314 log.add(new LogEntry(name, method, args, action, value));
1315 }
1316 return value;
1317 } else if (action == Action.THROW) {
1318 if (_logging && b.logging) {
1319 log.add(new LogEntry(name, method, args, action, value));
1320 }
1321 throw value;
1322 } else if (action == Action.PROXY) {
1323 var rtn;
1324 switch (args.length) {
1325 case 0:
1326 rtn = value();
1327 break;
1328 case 1:
1329 rtn = value(args[0]);
1330 break;
1331 case 2:
1332 rtn = value(args[0], args[1]);
1333 break;
1334 case 3:
1335 rtn = value(args[0], args[1], args[2]);
1336 break;
1337 case 4:
1338 rtn = value(args[0], args[1], args[2], args[3]);
1339 break;
1340 case 5:
1341 rtn = value(args[0], args[1], args[2], args[3], args[4]);
1342 break;
1343 case 6:
1344 rtn = value(args[0], args[1], args[2], args[3],
1345 args[4], args[5]);
1346 break;
1347 case 7:
1348 rtn = value(args[0], args[1], args[2], args[3],
1349 args[4], args[5], args[6]);
1350 break;
1351 case 8:
1352 rtn = value(args[0], args[1], args[2], args[3],
1353 args[4], args[5], args[6], args[7]);
1354 break;
1355 case 9:
1356 rtn = value(args[0], args[1], args[2], args[3],
1357 args[4], args[5], args[6], args[7], args[8]);
1358 break;
1359 case 9:
1360 rtn = value(args[0], args[1], args[2], args[3],
1361 args[4], args[5], args[6], args[7], args[8], args[9]);
1362 break;
1363 default:
1364 throw new Exception(
1365 "Cannot proxy calls with more than 10 parameters.");
1366 }
1367 if (_logging && b.logging) {
1368 log.add(new LogEntry(name, method, args, action, rtn));
1369 }
1370 return rtn;
1371 }
1372 }
1373 }
1374 if (matchedMethodName) {
1375 // User did specify behavior for this method, but all the
1376 // actions are exhausted. This is considered an error.
1377 throw new Exception('No more actions for method '
1378 '${_qualifiedName(name, method)}.');
1379 } else if (_throwIfNoBehavior) {
1380 throw new Exception('No behavior specified for method '
1381 '${_qualifiedName(name, method)}.');
1382 }
1383 // Otherwise user hasn't specified behavior for this method; we don't throw
1384 // so we can underspecify.
1385 if (_logging) {
1386 log.add(new LogEntry(name, method, args, Action.IGNORE));
1387 }
1388 }
1389
1390 /** [verifyZeroInteractions] returns true if no calls were made */
1391 bool verifyZeroInteractions() {
1392 if (log == null) {
1393 // This means we created the mock with logging off and have never turned
1394 // it on, so it doesn't make sense to verify behavior on such a mock.
1395 throw new
1396 Exception("Can't verify behavior when logging was never enabled.");
1397 }
1398 return log.logs.length == 0;
1399 }
1400
1401 /**
1402 * [getLogs] extracts all calls from the call log that match the
1403 * [logFilter], and returns the matching list of [LogEntry]s. If
1404 * [destructive] is false (the default) the matching calls are left
1405 * in the log, else they are removed. Removal allows us to verify a
1406 * set of interactions and then verify that there are no other
1407 * interactions left. [actionMatcher] can be used to further
1408 * restrict the returned logs based on the action the mock performed.
1409 * [logFilter] can be a [CallMatcher] or a predicate function that
1410 * takes a [LogEntry] and returns a bool.
1411 *
1412 * Typical usage:
1413 *
1414 * getLogs(callsTo(...)).verify(...);
1415 */
1416 LogEntryList getLogs([CallMatcher logFilter,
1417 Matcher actionMatcher,
1418 bool destructive = false]) {
1419 if (log == null) {
1420 // This means we created the mock with logging off and have never turned
1421 // it on, so it doesn't make sense to get logs from such a mock.
1422 throw new
1423 Exception("Can't retrieve logs when logging was never enabled.");
1424 } else {
1425 return log.getMatches(name, logFilter, actionMatcher, destructive);
1426 }
1427 }
1428
1429 /**
1430 * Useful shorthand method that creates a [CallMatcher] from its arguments
1431 * and then calls [getLogs].
1432 */
1433 LogEntryList calls(method,
1434 [arg0 = _noArg,
1435 arg1 = _noArg,
1436 arg2 = _noArg,
1437 arg3 = _noArg,
1438 arg4 = _noArg,
1439 arg5 = _noArg,
1440 arg6 = _noArg,
1441 arg7 = _noArg,
1442 arg8 = _noArg,
1443 arg9 = _noArg]) =>
1444 getLogs(callsTo(method, arg0, arg1, arg2, arg3, arg4,
1445 arg5, arg6, arg7, arg8, arg9));
1446
1447 /** Clear the behaviors for the Mock. */
1448 void resetBehavior() => _behaviors.clear();
1449
1450 /** Clear the logs for the Mock. */
1451 void clearLogs() {
1452 if (log != null) {
1453 if (name == null) { // This log is not shared.
1454 log.logs.clear();
1455 } else { // This log may be shared.
1456 log.logs = log.logs.filter((e) => e.mockName != name);
1457 }
1458 }
1459 }
1460
1461 /** Clear both logs and behavior. */
1462 void reset() {
1463 resetBehavior();
1464 clearLogs();
1465 }
1466 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698