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