OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, 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 library mock.log_entry_list; | |
6 | |
7 import 'package:matcher/matcher.dart'; | |
8 | |
9 import 'call_matcher.dart'; | |
10 import 'log_entry.dart'; | |
11 import 'util.dart'; | |
12 | |
13 /** | |
14 * [StepValidator]s are used by [stepwiseValidate] in [LogEntryList], which | |
15 * iterates through the list and call the [StepValidator] function with the | |
16 * log [List] and position. The [StepValidator] should return the number of | |
17 * positions to advance upon success, or zero upon failure. When zero is | |
18 * returned an error is reported. | |
19 */ | |
20 typedef int StepValidator(List<LogEntry> logs, int pos); | |
21 | |
22 /** | |
23 * We do verification on a list of [LogEntry]s. To allow chaining | |
24 * of calls to verify, we encapsulate such a list in the [LogEntryList] | |
25 * class. | |
26 */ | |
27 class LogEntryList { | |
28 String filter; | |
29 List<LogEntry> logs; | |
30 LogEntryList([this.filter]) { | |
31 logs = new List<LogEntry>(); | |
32 } | |
33 | |
34 /** Add a [LogEntry] to the log. */ | |
35 add(LogEntry entry) => logs.add(entry); | |
36 | |
37 /** Get the first entry, or null if no entries. */ | |
38 get first => (logs == null || logs.length == 0) ? null : logs[0]; | |
39 | |
40 /** Get the last entry, or null if no entries. */ | |
41 get last => (logs == null || logs.length == 0) ? null : logs.last; | |
42 | |
43 /** Creates a LogEntry predicate function from the argument. */ | |
44 Function _makePredicate(arg) { | |
45 if (arg == null) { | |
46 return (e) => true; | |
47 } else if (arg is CallMatcher) { | |
48 return (e) => arg.matches(e.methodName, e.args); | |
49 } else if (arg is Function) { | |
50 return arg; | |
51 } else { | |
52 throw new Exception("Invalid argument to _makePredicate."); | |
53 } | |
54 } | |
55 | |
56 /** | |
57 * Create a new [LogEntryList] consisting of [LogEntry]s from | |
58 * this list that match the specified [mockNameFilter] and [logFilter]. | |
59 * [mockNameFilter] can be null, a [String], a predicate [Function], | |
60 * or a [Matcher]. If [mockNameFilter] is null, this is the same as | |
61 * [anything]. | |
62 * If [logFilter] is null, all entries in the log will be returned. | |
63 * Otherwise [logFilter] should be a [CallMatcher] or predicate function | |
64 * that takes a [LogEntry] and returns a bool. | |
65 * If [destructive] is true, the log entries are removed from the | |
66 * original list. | |
67 */ | |
68 LogEntryList getMatches([mockNameFilter, | |
69 logFilter, | |
70 Matcher actionMatcher, | |
71 bool destructive = false]) { | |
72 if (mockNameFilter == null) { | |
73 mockNameFilter = anything; | |
74 } else { | |
75 mockNameFilter = wrapMatcher(mockNameFilter); | |
76 } | |
77 Function entryFilter = _makePredicate(logFilter); | |
78 String filterName = qualifiedName(mockNameFilter, logFilter.toString()); | |
79 LogEntryList rtn = new LogEntryList(filterName); | |
80 var matchState = {}; | |
81 for (var i = 0; i < logs.length; i++) { | |
82 LogEntry entry = logs[i]; | |
83 if (mockNameFilter.matches(entry.mockName, matchState) && | |
84 entryFilter(entry)) { | |
85 if (actionMatcher == null || | |
86 actionMatcher.matches(entry, matchState)) { | |
87 rtn.add(entry); | |
88 if (destructive) { | |
89 int startIndex = i--; | |
90 logs.removeRange(startIndex, startIndex + 1); | |
91 } | |
92 } | |
93 } | |
94 } | |
95 return rtn; | |
96 } | |
97 | |
98 /** Apply a unit test [Matcher] to the [LogEntryList]. */ | |
99 LogEntryList verify(Matcher matcher) { | |
100 if (_mockFailureHandler == null) { | |
101 _mockFailureHandler = | |
102 new _MockFailureHandler(getOrCreateExpectFailureHandler()); | |
103 } | |
104 expect(logs, matcher, reason: filter, failureHandler: _mockFailureHandler); | |
105 return this; | |
106 } | |
107 | |
108 /** | |
109 * Iterate through the list and call the [validator] function with the | |
110 * log [List] and position. The [validator] should return the number of | |
111 * positions to advance upon success, or zero upon failure. When zero is | |
112 * returned an error is reported. [reason] can be used to provide a | |
113 * more descriptive failure message. If a failure occurred false will be | |
114 * returned (unless the failure handler itself threw an exception); | |
115 * otherwise true is returned. | |
116 * The use case here is to perform more complex validations; for example | |
117 * we may want to assert that the return value from some function is | |
118 * later used as a parameter to a following function. If we filter the logs | |
119 * to include just these two functions we can write a simple validator to | |
120 * do this check. | |
121 */ | |
122 bool stepwiseValidate(StepValidator validator, [String reason = '']) { | |
123 if (_mockFailureHandler == null) { | |
124 _mockFailureHandler = | |
125 new _MockFailureHandler(getOrCreateExpectFailureHandler()); | |
126 } | |
127 var i = 0; | |
128 while (i < logs.length) { | |
129 var n = validator(logs, i); | |
130 if (n == 0) { | |
131 if (reason.length > 0) { | |
132 reason = ': $reason'; | |
133 } | |
134 _mockFailureHandler.fail("Stepwise validation failed at $filter " | |
135 "position $i$reason"); | |
136 return false; | |
137 } else { | |
138 i += n; | |
139 } | |
140 } | |
141 return true; | |
142 } | |
143 | |
144 /** | |
145 * Turn the logs into human-readable text. If [baseTime] is specified | |
146 * then each entry is prefixed with the offset from that time in | |
147 * milliseconds; otherwise the time of day is used. | |
148 */ | |
149 String toString([DateTime baseTime]) { | |
150 String s = ''; | |
151 for (var e in logs) { | |
152 s = '$s${e.toString(baseTime)}\n'; | |
153 } | |
154 return s; | |
155 } | |
156 | |
157 /** | |
158 * Find the first log entry that satisfies [logFilter] and | |
159 * return its position. A search [start] position can be provided | |
160 * to allow for repeated searches. [logFilter] can be a [CallMatcher], | |
161 * or a predicate function that takes a [LogEntry] argument and returns | |
162 * a bool. If [logFilter] is null, it will match any [LogEntry]. | |
163 * If no entry is found, then [failureReturnValue] is returned. | |
164 * After each check the position is updated by [skip], so using | |
165 * [skip] of -1 allows backward searches, using a [skip] of 2 can | |
166 * be used to check pairs of adjacent entries, and so on. | |
167 */ | |
168 int findLogEntry(logFilter, [int start = 0, int failureReturnValue = -1, | |
169 skip = 1]) { | |
170 logFilter = _makePredicate(logFilter); | |
171 int pos = start; | |
172 while (pos >= 0 && pos < logs.length) { | |
173 if (logFilter(logs[pos])) { | |
174 return pos; | |
175 } | |
176 pos += skip; | |
177 } | |
178 return failureReturnValue; | |
179 } | |
180 | |
181 /** | |
182 * Returns log events that happened up to the first one that | |
183 * satisfies [logFilter]. If [inPlace] is true, then returns | |
184 * this LogEntryList after removing the from the first satisfier; | |
185 * onwards otherwise a new list is created. [description] | |
186 * is used to create a new name for the resulting list. | |
187 * [defaultPosition] is used as the index of the matching item in | |
188 * the case that no match is found. | |
189 */ | |
190 LogEntryList _head(logFilter, bool inPlace, | |
191 String description, int defaultPosition) { | |
192 if (filter != null) { | |
193 description = '$filter $description'; | |
194 } | |
195 int pos = findLogEntry(logFilter, 0, defaultPosition); | |
196 if (inPlace) { | |
197 if (pos < logs.length) { | |
198 logs.removeRange(pos, logs.length); | |
199 } | |
200 filter = description; | |
201 return this; | |
202 } else { | |
203 LogEntryList newList = new LogEntryList(description); | |
204 for (var i = 0; i < pos; i++) { | |
205 newList.logs.add(logs[i]); | |
206 } | |
207 return newList; | |
208 } | |
209 } | |
210 | |
211 /** | |
212 * Returns log events that happened from the first one that | |
213 * satisfies [logFilter]. If [inPlace] is true, then returns | |
214 * this LogEntryList after removing the entries up to the first | |
215 * satisfier; otherwise a new list is created. [description] | |
216 * is used to create a new name for the resulting list. | |
217 * [defaultPosition] is used as the index of the matching item in | |
218 * the case that no match is found. | |
219 */ | |
220 LogEntryList _tail(logFilter, bool inPlace, | |
221 String description, int defaultPosition) { | |
222 if (filter != null) { | |
223 description = '$filter $description'; | |
224 } | |
225 int pos = findLogEntry(logFilter, 0, defaultPosition); | |
226 if (inPlace) { | |
227 if (pos > 0) { | |
228 logs.removeRange(0, pos); | |
229 } | |
230 filter = description; | |
231 return this; | |
232 } else { | |
233 LogEntryList newList = new LogEntryList(description); | |
234 while (pos < logs.length) { | |
235 newList.logs.add(logs[pos++]); | |
236 } | |
237 return newList; | |
238 } | |
239 } | |
240 | |
241 /** | |
242 * Returns log events that happened after [when]. If [inPlace] | |
243 * is true, then it returns this LogEntryList after removing | |
244 * the entries that happened up to [when]; otherwise a new | |
245 * list is created. | |
246 */ | |
247 LogEntryList after(DateTime when, [bool inPlace = false]) => | |
248 _tail((e) => e.time.isAfter(when), inPlace, 'after $when', logs.length); | |
249 | |
250 /** | |
251 * Returns log events that happened from [when] onwards. If | |
252 * [inPlace] is true, then it returns this LogEntryList after | |
253 * removing the entries that happened before [when]; otherwise | |
254 * a new list is created. | |
255 */ | |
256 LogEntryList from(DateTime when, [bool inPlace = false]) => | |
257 _tail((e) => !e.time.isBefore(when), inPlace, 'from $when', logs.length); | |
258 | |
259 /** | |
260 * Returns log events that happened until [when]. If [inPlace] | |
261 * is true, then it returns this LogEntryList after removing | |
262 * the entries that happened after [when]; otherwise a new | |
263 * list is created. | |
264 */ | |
265 LogEntryList until(DateTime when, [bool inPlace = false]) => | |
266 _head((e) => e.time.isAfter(when), inPlace, 'until $when', logs.length); | |
267 | |
268 /** | |
269 * Returns log events that happened before [when]. If [inPlace] | |
270 * is true, then it returns this LogEntryList after removing | |
271 * the entries that happened from [when] onwards; otherwise a new | |
272 * list is created. | |
273 */ | |
274 LogEntryList before(DateTime when, [bool inPlace = false]) => | |
275 _head((e) => !e.time.isBefore(when), | |
276 inPlace, | |
277 'before $when', | |
278 logs.length); | |
279 | |
280 /** | |
281 * Returns log events that happened after [logEntry]'s time. | |
282 * If [inPlace] is true, then it returns this LogEntryList after | |
283 * removing the entries that happened up to [when]; otherwise a new | |
284 * list is created. If [logEntry] is null the current time is used. | |
285 */ | |
286 LogEntryList afterEntry(LogEntry logEntry, [bool inPlace = false]) => | |
287 after(logEntry == null ? new DateTime.now() : logEntry.time); | |
288 | |
289 /** | |
290 * Returns log events that happened from [logEntry]'s time onwards. | |
291 * If [inPlace] is true, then it returns this LogEntryList after | |
292 * removing the entries that happened before [when]; otherwise | |
293 * a new list is created. If [logEntry] is null the current time is used. | |
294 */ | |
295 LogEntryList fromEntry(LogEntry logEntry, [bool inPlace = false]) => | |
296 from(logEntry == null ? new DateTime.now() : logEntry.time); | |
297 | |
298 /** | |
299 * Returns log events that happened until [logEntry]'s time. If | |
300 * [inPlace] is true, then it returns this LogEntryList after removing | |
301 * the entries that happened after [when]; otherwise a new | |
302 * list is created. If [logEntry] is null the epoch time is used. | |
303 */ | |
304 LogEntryList untilEntry(LogEntry logEntry, [bool inPlace = false]) => | |
305 until(logEntry == null ? | |
306 new DateTime.fromMillisecondsSinceEpoch(0) : logEntry.time); | |
307 | |
308 /** | |
309 * Returns log events that happened before [logEntry]'s time. If | |
310 * [inPlace] is true, then it returns this LogEntryList after removing | |
311 * the entries that happened from [when] onwards; otherwise a new | |
312 * list is created. If [logEntry] is null the epoch time is used. | |
313 */ | |
314 LogEntryList beforeEntry(LogEntry logEntry, [bool inPlace = false]) => | |
315 before(logEntry == null ? | |
316 new DateTime.fromMillisecondsSinceEpoch(0) : logEntry.time); | |
317 | |
318 /** | |
319 * Returns log events that happened after the first event in [segment]. | |
320 * If [inPlace] is true, then it returns this LogEntryList after removing | |
321 * the entries that happened earlier; otherwise a new list is created. | |
322 */ | |
323 LogEntryList afterFirst(LogEntryList segment, [bool inPlace = false]) => | |
324 afterEntry(segment.first, inPlace); | |
325 | |
326 /** | |
327 * Returns log events that happened after the last event in [segment]. | |
328 * If [inPlace] is true, then it returns this LogEntryList after removing | |
329 * the entries that happened earlier; otherwise a new list is created. | |
330 */ | |
331 LogEntryList afterLast(LogEntryList segment, [bool inPlace = false]) => | |
332 afterEntry(segment.last, inPlace); | |
333 | |
334 /** | |
335 * Returns log events that happened from the time of the first event in | |
336 * [segment] onwards. If [inPlace] is true, then it returns this | |
337 * LogEntryList after removing the earlier entries; otherwise a new list | |
338 * is created. | |
339 */ | |
340 LogEntryList fromFirst(LogEntryList segment, [bool inPlace = false]) => | |
341 fromEntry(segment.first, inPlace); | |
342 | |
343 /** | |
344 * Returns log events that happened from the time of the last event in | |
345 * [segment] onwards. If [inPlace] is true, then it returns this | |
346 * LogEntryList after removing the earlier entries; otherwise a new list | |
347 * is created. | |
348 */ | |
349 LogEntryList fromLast(LogEntryList segment, [bool inPlace = false]) => | |
350 fromEntry(segment.last, inPlace); | |
351 | |
352 /** | |
353 * Returns log events that happened until the first event in [segment]. | |
354 * If [inPlace] is true, then it returns this LogEntryList after removing | |
355 * the entries that happened later; otherwise a new list is created. | |
356 */ | |
357 LogEntryList untilFirst(LogEntryList segment, [bool inPlace = false]) => | |
358 untilEntry(segment.first, inPlace); | |
359 | |
360 /** | |
361 * Returns log events that happened until the last event in [segment]. | |
362 * If [inPlace] is true, then it returns this LogEntryList after removing | |
363 * the entries that happened later; otherwise a new list is created. | |
364 */ | |
365 LogEntryList untilLast(LogEntryList segment, [bool inPlace = false]) => | |
366 untilEntry(segment.last, inPlace); | |
367 | |
368 /** | |
369 * Returns log events that happened before the first event in [segment]. | |
370 * If [inPlace] is true, then it returns this LogEntryList after removing | |
371 * the entries that happened later; otherwise a new list is created. | |
372 */ | |
373 LogEntryList beforeFirst(LogEntryList segment, [bool inPlace = false]) => | |
374 beforeEntry(segment.first, inPlace); | |
375 | |
376 /** | |
377 * Returns log events that happened before the last event in [segment]. | |
378 * If [inPlace] is true, then it returns this LogEntryList after removing | |
379 * the entries that happened later; otherwise a new list is created. | |
380 */ | |
381 LogEntryList beforeLast(LogEntryList segment, [bool inPlace = false]) => | |
382 beforeEntry(segment.last, inPlace); | |
383 | |
384 /** | |
385 * Iterate through the LogEntryList looking for matches to the entries | |
386 * in [keys]; for each match found the closest [distance] neighboring log | |
387 * entries that match [mockNameFilter] and [logFilter] will be included in | |
388 * the result. If [isPreceding] is true we use the neighbors that precede | |
389 * the matched entry; else we use the neighbors that followed. | |
390 * If [includeKeys] is true then the entries in [keys] that resulted in | |
391 * entries in the output list are themselves included in the output list. If | |
392 * [distance] is zero then all matches are included. | |
393 */ | |
394 LogEntryList _neighboring(bool isPreceding, | |
395 LogEntryList keys, | |
396 mockNameFilter, | |
397 logFilter, | |
398 int distance, | |
399 bool includeKeys) { | |
400 String filterName = 'Calls to ' | |
401 '${qualifiedName(mockNameFilter, logFilter.toString())} ' | |
402 '${isPreceding?"preceding":"following"} ${keys.filter}'; | |
403 | |
404 LogEntryList rtn = new LogEntryList(filterName); | |
405 | |
406 // Deal with the trivial case. | |
407 if (logs.length == 0 || keys.logs.length == 0) { | |
408 return rtn; | |
409 } | |
410 | |
411 // Normalize the mockNameFilter and logFilter values. | |
412 if (mockNameFilter == null) { | |
413 mockNameFilter = anything; | |
414 } else { | |
415 mockNameFilter = wrapMatcher(mockNameFilter); | |
416 } | |
417 logFilter = _makePredicate(logFilter); | |
418 | |
419 // The scratch list is used to hold matching entries when we | |
420 // are doing preceding neighbors. The remainingCount is used to | |
421 // keep track of how many matching entries we can still add in the | |
422 // current segment (0 if we are doing doing following neighbors, until | |
423 // we get our first key match). | |
424 List scratch = null; | |
425 int remainingCount = 0; | |
426 if (isPreceding) { | |
427 scratch = new List(); | |
428 remainingCount = logs.length; | |
429 } | |
430 | |
431 var keyIterator = keys.logs.iterator; | |
432 keyIterator.moveNext(); | |
433 LogEntry keyEntry = keyIterator.current; | |
434 Map matchState = {}; | |
435 | |
436 for (LogEntry logEntry in logs) { | |
437 // If we have a log entry match, copy the saved matches from the | |
438 // scratch buffer into the return list, as well as the matching entry, | |
439 // if appropriate, and reset the scratch buffer. Continue processing | |
440 // from the next key entry. | |
441 if (keyEntry == logEntry) { | |
442 if (scratch != null) { | |
443 int numToCopy = scratch.length; | |
444 if (distance > 0 && distance < numToCopy) { | |
445 numToCopy = distance; | |
446 } | |
447 for (var i = scratch.length - numToCopy; i < scratch.length; i++) { | |
448 rtn.logs.add(scratch[i]); | |
449 } | |
450 scratch.clear(); | |
451 } else { | |
452 remainingCount = distance > 0 ? distance : logs.length; | |
453 } | |
454 if (includeKeys) { | |
455 rtn.logs.add(keyEntry); | |
456 } | |
457 if (keyIterator.moveNext()) { | |
458 keyEntry = keyIterator.current; | |
459 } else if (isPreceding) { // We're done. | |
460 break; | |
461 } | |
462 } else if (remainingCount > 0 && | |
463 mockNameFilter.matches(logEntry.mockName, matchState) && | |
464 logFilter(logEntry)) { | |
465 if (scratch != null) { | |
466 scratch.add(logEntry); | |
467 } else { | |
468 rtn.logs.add(logEntry); | |
469 --remainingCount; | |
470 } | |
471 } | |
472 } | |
473 return rtn; | |
474 } | |
475 | |
476 /** | |
477 * Iterate through the LogEntryList looking for matches to the entries | |
478 * in [keys]; for each match found the closest [distance] prior log entries | |
479 * that match [mocknameFilter] and [logFilter] will be included in the result. | |
480 * If [includeKeys] is true then the entries in [keys] that resulted in | |
481 * entries in the output list are themselves included in the output list. If | |
482 * [distance] is zero then all matches are included. | |
483 * | |
484 * The idea here is that you could find log entries that are related to | |
485 * other logs entries in some temporal sense. For example, say we have a | |
486 * method commit() that returns -1 on failure. Before commit() gets called | |
487 * the value being committed is created by process(). We may want to find | |
488 * the calls to process() that preceded calls to commit() that failed. | |
489 * We could do this with: | |
490 * | |
491 * print(log.preceding(log.getLogs(callsTo('commit'), returning(-1)), | |
492 * logFilter: callsTo('process')).toString()); | |
493 * | |
494 * We might want to include the details of the failing calls to commit() | |
495 * to see what parameters were passed in, in which case we would set | |
496 * [includeKeys]. | |
497 * | |
498 * As another simple example, say we wanted to know the three method | |
499 * calls that immediately preceded each failing call to commit(): | |
500 * | |
501 * print(log.preceding(log.getLogs(callsTo('commit'), returning(-1)), | |
502 * distance: 3).toString()); | |
503 */ | |
504 LogEntryList preceding(LogEntryList keys, | |
505 {mockNameFilter: null, | |
506 logFilter: null, | |
507 int distance: 1, | |
508 bool includeKeys: false}) => | |
509 _neighboring(true, keys, mockNameFilter, logFilter, | |
510 distance, includeKeys); | |
511 | |
512 /** | |
513 * Iterate through the LogEntryList looking for matches to the entries | |
514 * in [keys]; for each match found the closest [distance] subsequent log | |
515 * entries that match [mocknameFilter] and [logFilter] will be included in | |
516 * the result. If [includeKeys] is true then the entries in [keys] that | |
517 * resulted in entries in the output list are themselves included in the | |
518 * output list. If [distance] is zero then all matches are included. | |
519 * See [preceding] for a usage example. | |
520 */ | |
521 LogEntryList following(LogEntryList keys, | |
522 {mockNameFilter: null, | |
523 logFilter: null, | |
524 int distance: 1, | |
525 bool includeKeys: false}) => | |
526 _neighboring(false, keys, mockNameFilter, logFilter, | |
527 distance, includeKeys); | |
528 } | |
529 | |
530 _MockFailureHandler _mockFailureHandler = null; | |
531 | |
532 /** | |
533 * The failure handler for the [expect()] calls that occur in [verify()] | |
534 * methods in the mock objects. This calls the real failure handler used | |
535 * by the unit test library after formatting the error message with | |
536 * the custom formatter. | |
537 */ | |
538 class _MockFailureHandler implements FailureHandler { | |
539 FailureHandler proxy; | |
540 _MockFailureHandler(this.proxy); | |
541 void fail(String reason) { | |
542 proxy.fail(reason); | |
543 } | |
544 void failMatch(actual, Matcher matcher, String reason, | |
545 Map matchState, bool verbose) { | |
546 proxy.fail(_mockingErrorFormatter(actual, matcher, reason, | |
547 matchState, verbose)); | |
548 } | |
549 } | |
550 | |
551 /** | |
552 * The error formatter for mocking is a bit different from the default one | |
553 * for unit testing; instead of the third argument being a 'reason' | |
554 * it is instead a [signature] describing the method signature filter | |
555 * that was used to select the logs that were verified. | |
556 */ | |
557 String _mockingErrorFormatter(actual, Matcher matcher, String signature, | |
558 Map matchState, bool verbose) { | |
559 var description = new StringDescription(); | |
560 description.add('Expected ${signature} ').addDescriptionOf(matcher). | |
561 add('\n but: '); | |
562 matcher.describeMismatch(actual, description, matchState, verbose).add('.'); | |
563 return description.toString(); | |
564 } | |
OLD | NEW |