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