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