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

Unified Diff: pkg/unittest/lib/mock.dart

Issue 11301046: Restructure pkg/unittest and pkg/webdriver to follow the pub conventions. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 8 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: pkg/unittest/lib/mock.dart
===================================================================
--- pkg/unittest/lib/mock.dart (revision 0)
+++ pkg/unittest/lib/mock.dart (revision 0)
@@ -0,0 +1,1466 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// TODO(gram) - when we can do package imports from within packages
+// without Dartdoc choking, this should be moved to a separate package.
Siggi Cherem (dart-lang) 2012/10/31 20:54:02 same question as before
gram 2012/11/01 21:23:19 Done.
+
+library mock;
+import 'matcher.dart';
+
+/**
+ * The error formatter for mocking is a bit different from the default one
+ * for unit testing; instead of the third argument being a 'reason'
+ * it is instead a [signature] describing the method signature filter
+ * that was used to select the logs that were verified.
+ */
+String _mockingErrorFormatter(actual, Matcher matcher, String signature,
+ MatchState matchState, bool verbose) {
+ var description = new StringDescription();
+ description.add('Expected ${signature} ').addDescriptionOf(matcher).
+ add('\n but: ');
+ matcher.describeMismatch(actual, description, matchState, verbose).add('.');
+ return description.toString();
+}
+
+/**
+ * The failure handler for the [expect()] calls that occur in [verify()]
+ * methods in the mock objects. This calls the real failure handler used
+ * by the unit test library after formatting the error message with
+ * the custom formatter.
+ */
+class _MockFailureHandler implements FailureHandler {
+ FailureHandler proxy;
+ _MockFailureHandler(this.proxy);
+ void fail(String reason) {
+ proxy.fail(reason);
+ }
+ void failMatch(actual, Matcher matcher, String reason,
+ MatchState matchState, bool verbose) {
+ proxy.fail(_mockingErrorFormatter(actual, matcher, reason,
+ matchState, verbose));
+ }
+}
+
+_MockFailureHandler _mockFailureHandler = null;
+
+/** Sentinel value for representing no argument. */
+class _Sentinel {
+ const _Sentinel();
+}
+const _noArg = const _Sentinel();
+
+/** The ways in which a call to a mock method can be handled. */
+class Action {
+ /** Do nothing (void method) */
+ static const IGNORE = const Action._('IGNORE');
+
+ /** Return a supplied value. */
+ static const RETURN = const Action._('RETURN');
+
+ /** Throw a supplied value. */
+ static const THROW = const Action._('THROW');
+
+ /** Call a supplied function. */
+ static const PROXY = const Action._('PROXY');
+
+ const Action._(this.name);
+
+ final String name;
+}
+
+/**
+ * The behavior of a method call in the mock library is specified
+ * with [Responder]s. A [Responder] has a [value] to throw
+ * or return (depending on the type of [action]),
+ * and can either be one-shot, multi-shot, or infinitely repeating,
+ * depending on the value of [count (1, greater than 1, or 0 respectively).
+ */
+class Responder {
+ var value;
+ Action action;
+ int count;
+ Responder(this.value, [this.count = 1, this.action = Action.RETURN]);
+}
+
+/**
+ * A [CallMatcher] is a special matcher used to match method calls (i.e.
+ * a method name and set of arguments). It is not a [Matcher] like the
+ * unit test [Matcher], but instead represents a method name and a
+ * collection of [Matcher]s, one per argument, that will be applied
+ * to the parameters to decide if the method call is a match.
+ */
+class CallMatcher {
+ Matcher nameFilter;
+ List<Matcher> argMatchers;
+
+ /**
+ * Constructor for [CallMatcher]. [name] can be null to
+ * match anything, or a literal [String], a predicate [Function],
+ * or a [Matcher]. The various arguments can be scalar values or
+ * [Matcher]s.
+ */
+ CallMatcher([name,
+ arg0 = _noArg,
+ arg1 = _noArg,
+ arg2 = _noArg,
+ arg3 = _noArg,
+ arg4 = _noArg,
+ arg5 = _noArg,
+ arg6 = _noArg,
+ arg7 = _noArg,
+ arg8 = _noArg,
+ arg9 = _noArg]) {
+ if (name == null) {
+ nameFilter = anything;
+ } else {
+ nameFilter = wrapMatcher(name);
+ }
+ argMatchers = new List<Matcher>();
+ if (arg0 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg0));
+ if (arg1 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg1));
+ if (arg2 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg2));
+ if (arg3 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg3));
+ if (arg4 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg4));
+ if (arg5 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg5));
+ if (arg6 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg6));
+ if (arg7 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg7));
+ if (arg8 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg8));
+ if (arg9 === _noArg) return;
+ argMatchers.add(wrapMatcher(arg9));
+ }
+
+ /**
+ * We keep our behavior specifications in a Map, which is keyed
+ * by the [CallMatcher]. To make the keys unique and to get a
+ * descriptive value for the [CallMatcher] we have this override
+ * of [toString()].
+ */
+ String toString() {
+ Description d = new StringDescription();
+ d.addDescriptionOf(nameFilter);
+ // If the nameFilter was a simple string - i.e. just a method name -
+ // strip the quotes to make this more natural in appearance.
+ if (d.toString()[0] == "'") {
+ d.replace(d.toString().substring(1, d.toString().length - 1));
+ }
+ d.add('(');
+ for (var i = 0; i < argMatchers.length; i++) {
+ if (i > 0) d.add(', ');
+ d.addDescriptionOf(argMatchers[i]);
+ }
+ d.add(')');
+ return d.toString();
+ }
+
+ /**
+ * Given a [method] name and list of [arguments], return true
+ * if it matches this [CallMatcher.
+ */
+ bool matches(String method, List arguments) {
+ var matchState = new MatchState();
+ if (!nameFilter.matches(method, matchState)) {
+ return false;
+ }
+ var numArgs = (arguments == null) ? 0 : arguments.length;
+ if (numArgs < argMatchers.length) {
+ throw new Exception("Less arguments than matchers for $method.");
+ }
+ for (var i = 0; i < argMatchers.length; i++) {
+ if (!argMatchers[i].matches(arguments[i], matchState)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+/**
+ * Returns a [CallMatcher] for the specified signature. [method] can be
+ * null to match anything, or a literal [String], a predicate [Function],
+ * or a [Matcher]. The various arguments can be scalar values or [Matcher]s.
+ */
+CallMatcher callsTo([method,
+ arg0 = _noArg,
+ arg1 = _noArg,
+ arg2 = _noArg,
+ arg3 = _noArg,
+ arg4 = _noArg,
+ arg5 = _noArg,
+ arg6 = _noArg,
+ arg7 = _noArg,
+ arg8 = _noArg,
+ arg9 = _noArg]) {
+ return new CallMatcher(method, arg0, arg1, arg2, arg3, arg4,
+ arg5, arg6, arg7, arg8, arg9);
+}
+
+/**
+ * A [Behavior] represents how a [Mock] will respond to one particular
+ * type of method call.
+ */
+class Behavior {
+ CallMatcher matcher; // The method call matcher.
+ List<Responder> actions; // The values to return/throw or proxies to call.
+ bool logging = true;
+
+ Behavior (this.matcher) {
+ actions = new List<Responder>();
+ }
+
+ /**
+ * Adds a [Responder] that returns a [value] for [count] calls
+ * (1 by default).
+ */
+ Behavior thenReturn(value, [count = 1]) {
+ actions.add(new Responder(value, count, Action.RETURN));
+ return this; // For chaining calls.
+ }
+
+ /** Adds a [Responder] that repeatedly returns a [value]. */
+ Behavior alwaysReturn(value) {
+ return thenReturn(value, 0);
+ }
+
+ /**
+ * Adds a [Responder] that throws [value] [count]
+ * times (1 by default).
+ */
+ Behavior thenThrow(value, [count = 1]) {
+ actions.add(new Responder(value, count, Action.THROW));
+ return this; // For chaining calls.
+ }
+
+ /** Adds a [Responder] that throws [value] endlessly. */
+ Behavior alwaysThrow(value) {
+ return thenThrow(value, 0);
+ }
+
+ /**
+ * [thenCall] creates a proxy Responder, that is called [count]
+ * times (1 by default; 0 is used for unlimited calls, and is
+ * exposed as [alwaysCall]). [value] is the function that will
+ * be called with the same arguments that were passed to the
+ * mock. Proxies can be used to wrap real objects or to define
+ * more complex return/throw behavior. You could even (if you
+ * wanted) use proxies to emulate the behavior of thenReturn;
+ * e.g.:
+ *
+ * m.when(callsTo('foo')).thenReturn(0)
+ *
+ * is equivalent to:
+ *
+ * m.when(callsTo('foo')).thenCall(() => 0)
+ */
+ Behavior thenCall(value, [count = 1]) {
+ actions.add(new Responder(value, count, Action.PROXY));
+ return this; // For chaining calls.
+ }
+
+ /** Creates a repeating proxy call. */
+ Behavior alwaysCall(value) {
+ return thenCall(value, 0);
+ }
+
+ /** Returns true if a method call matches the [Behavior]. */
+ bool matches(String method, List args) => matcher.matches(method, args);
+
+ /** Returns the [matcher]'s representation. */
+ String toString() => matcher.toString();
+}
+
+/**
+ * Every call to a [Mock] object method is logged. The logs are
+ * kept in instances of [LogEntry].
+ */
+class LogEntry {
+ /** The time of the event. */
+ Date time;
+
+ /** The mock object name, if any. */
+ final String mockName;
+
+ /** The method name. */
+ final String methodName;
+
+ /** The parameters. */
+ final List args;
+
+ /** The behavior that resulted. */
+ final Action action;
+
+ /** The value that was returned (if no throw). */
+ final value;
+
+ LogEntry(this.mockName, this.methodName,
+ this.args, this.action, [this.value]) {
+ time = new Date.now();
+ }
+
+ String _pad2(int val) => (val >= 10 ? '$val' : '0$val');
+
+ String toString([Date baseTime]) {
+ Description d = new StringDescription();
+ if (baseTime == null) {
+ // Show absolute time.
+ d.add('${time.hour}:${_pad2(time.minute)}:'
+ '${_pad2(time.second)}.${time.millisecond}> ');
+ } else {
+ // Show relative time.
+ int delta = time.millisecondsSinceEpoch - baseTime.millisecondsSinceEpoch;
+ int secs = delta ~/ 1000;
+ int msecs = delta % 1000;
+ d.add('$secs.$msecs> ');
+ }
+ d.add('${_qualifiedName(mockName, methodName)}(');
+ if (args != null) {
+ for (var i = 0; i < args.length; i++) {
+ if (i != 0) d.add(', ');
+ d.addDescriptionOf(args[i]);
+ }
+ }
+ d.add(') ${action == Action.THROW ? "threw" : "returned"} ');
+ d.addDescriptionOf(value);
+ return d.toString();
+ }
+}
+
+/** Utility function for optionally qualified method names */
+String _qualifiedName(owner, String method) {
+ if (owner == null || owner === anything) {
+ return method;
+ } else if (owner is Matcher) {
+ Description d = new StringDescription();
+ d.addDescriptionOf(owner);
+ d.add('.');
+ d.add(method);
+ return d.toString();
+ } else {
+ return '$owner.$method';
+ }
+}
+
+/**
+* [StepValidator]s are used by [stepwiseValidate] in [LogEntryList], which
+* iterates through the list and call the [StepValidator] function with the
+* log [List] and position. The [StepValidator] should return the number of
+* positions to advance upon success, or zero upon failure. When zero is
+* returned an error is reported.
+*/
+typedef int StepValidator(List<LogEntry> logs, int pos);
+
+/**
+ * We do verification on a list of [LogEntry]s. To allow chaining
+ * of calls to verify, we encapsulate such a list in the [LogEntryList]
+ * class.
+ */
+class LogEntryList {
+ String filter;
+ List<LogEntry> logs;
+ LogEntryList([this.filter]) {
+ logs = new List<LogEntry>();
+ }
+
+ /** Add a [LogEntry] to the log. */
+ add(LogEntry entry) => logs.add(entry);
+
+ /** Get the first entry, or null if no entries. */
+ get first => (logs == null || logs.length == 0) ? null : logs[0];
+
+ /** Get the last entry, or null if no entries. */
+ get last => (logs == null || logs.length == 0) ? null : logs.last;
+
+ /** Creates a LogEntry predicate function from the argument. */
+ Function _makePredicate(arg) {
+ if (arg == null) {
+ return (e) => true;
+ } else if (arg is CallMatcher) {
+ return (e) => arg.matches(e.methodName, e.args);
+ } else if (arg is Function) {
+ return arg;
+ } else {
+ throw new Exception("Invalid argument to _makePredicate.");
+ }
+ }
+
+ /**
+ * Create a new [LogEntryList] consisting of [LogEntry]s from
+ * this list that match the specified [mockNameFilter] and [logFilter].
+ * [mockNameFilter] can be null, a [String], a predicate [Function],
+ * or a [Matcher]. If [mockNameFilter] is null, this is the same as
+ * [anything].
+ * If [logFilter] is null, all entries in the log will be returned.
+ * Otherwise [logFilter] should be a [CallMatcher] or predicate function
+ * that takes a [LogEntry] and returns a bool.
+ * If [destructive] is true, the log entries are removed from the
+ * original list.
+ */
+ LogEntryList getMatches([mockNameFilter,
+ logFilter,
+ Matcher actionMatcher,
+ bool destructive = false]) {
+ if (mockNameFilter == null) {
+ mockNameFilter = anything;
+ } else {
+ mockNameFilter = wrapMatcher(mockNameFilter);
+ }
+ Function entryFilter = _makePredicate(logFilter);
+ String filterName = _qualifiedName(mockNameFilter, logFilter.toString());
+ LogEntryList rtn = new LogEntryList(filterName);
+ MatchState matchState = new MatchState();
+ for (var i = 0; i < logs.length; i++) {
+ LogEntry entry = logs[i];
+ if (mockNameFilter.matches(entry.mockName, matchState) &&
+ entryFilter(entry)) {
+ if (actionMatcher == null ||
+ actionMatcher.matches(entry, matchState)) {
+ rtn.add(entry);
+ if (destructive) {
+ logs.removeRange(i--, 1);
+ }
+ }
+ }
+ }
+ return rtn;
+ }
+
+ /** Apply a unit test [Matcher] to the [LogEntryList]. */
+ LogEntryList verify(Matcher matcher) {
+ if (_mockFailureHandler == null) {
+ _mockFailureHandler =
+ new _MockFailureHandler(getOrCreateExpectFailureHandler());
+ }
+ expect(logs, matcher, reason:filter, failureHandler: _mockFailureHandler);
+ return this;
+ }
+
+ /**
+ * Iterate through the list and call the [validator] function with the
+ * log [List] and position. The [validator] should return the number of
+ * positions to advance upon success, or zero upon failure. When zero is
+ * returned an error is reported. [reason] can be used to provide a
+ * more descriptive failure message. If a failure occurred false will be
+ * returned (unless the failure handler itself threw an exception);
+ * otherwise true is returned.
+ * The use case here is to perform more complex validations; for example
+ * we may want to assert that the return value from some function is
+ * later used as a parameter to a following function. If we filter the logs
+ * to include just these two functions we can write a simple validator to
+ * do this check.
+ */
+ bool stepwiseValidate(StepValidator validator, [String reason = '']) {
+ if (_mockFailureHandler == null) {
+ _mockFailureHandler =
+ new _MockFailureHandler(getOrCreateExpectFailureHandler());
+ }
+ var i = 0;
+ while (i < logs.length) {
+ var n = validator(logs, i);
+ if (n == 0) {
+ if (reason.length > 0) {
+ reason = ': $reason';
+ }
+ _mockFailureHandler.fail("Stepwise validation failed at $filter "
+ "position $i$reason");
+ return false;
+ } else {
+ i += n;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Turn the logs into human-readable text. If [baseTime] is specified
+ * then each entry is prefixed with the offset from that time in
+ * milliseconds; otherwise the time of day is used.
+ */
+ String toString([Date baseTime]) {
+ String s = '';
+ for (var e in logs) {
+ s = '$s${e.toString(baseTime)}\n';
+ }
+ return s;
+ }
+
+ /**
+ * Find the first log entry that satisfies [logFilter] and
+ * return its position. A search [start] position can be provided
+ * to allow for repeated searches. [logFilter] can be a [CallMatcher],
+ * or a predicate function that takes a [LogEntry] argument and returns
+ * a bool. If [logFilter] is null, it will match any [LogEntry].
+ * If no entry is found, then [failureReturnValue] is returned.
+ * After each check the position is updated by [skip], so using
+ * [skip] of -1 allows backward searches, using a [skip] of 2 can
+ * be used to check pairs of adjacent entries, and so on.
+ */
+ int findLogEntry(logFilter, [int start = 0, int failureReturnValue = -1,
+ skip = 1]) {
+ logFilter = _makePredicate(logFilter);
+ int pos = start;
+ while (pos >= 0 && pos < logs.length) {
+ if (logFilter(logs[pos])) {
+ return pos;
+ }
+ pos += skip;
+ }
+ return failureReturnValue;
+ }
+
+ /**
+ * Returns log events that happened up to the first one that
+ * satisfies [logFilter]. If [inPlace] is true, then returns
+ * this LogEntryList after removing the from the first satisfier;
+ * onwards otherwise a new list is created. [description]
+ * is used to create a new name for the resulting list.
+ * [defaultPosition] is used as the index of the matching item in
+ * the case that no match is found.
+ */
+ LogEntryList _head(logFilter, bool inPlace,
+ String description, int defaultPosition) {
+ if (filter != null) {
+ description = '$filter $description';
+ }
+ int pos = findLogEntry(logFilter, 0, defaultPosition);
+ if (inPlace) {
+ if (pos < logs.length) {
+ logs.removeRange(pos, logs.length - pos);
+ }
+ filter = description;
+ return this;
+ } else {
+ LogEntryList newList = new LogEntryList(description);
+ for (var i = 0; i < pos; i++) {
+ newList.logs.add(logs[i]);
+ }
+ return newList;
+ }
+ }
+
+ /**
+ * Returns log events that happened from the first one that
+ * satisfies [logFilter]. If [inPlace] is true, then returns
+ * this LogEntryList after removing the entries up to the first
+ * satisfier; otherwise a new list is created. [description]
+ * is used to create a new name for the resulting list.
+ * [defaultPosition] is used as the index of the matching item in
+ * the case that no match is found.
+ */
+ LogEntryList _tail(logFilter, bool inPlace,
+ String description, int defaultPosition) {
+ if (filter != null) {
+ description = '$filter $description';
+ }
+ int pos = findLogEntry(logFilter, 0, defaultPosition);
+ if (inPlace) {
+ if (pos > 0) {
+ logs.removeRange(0, pos);
+ }
+ filter = description;
+ return this;
+ } else {
+ LogEntryList newList = new LogEntryList(description);
+ while (pos < logs.length) {
+ newList.logs.add(logs[pos++]);
+ }
+ return newList;
+ }
+ }
+
+ /**
+ * Returns log events that happened after [when]. If [inPlace]
+ * is true, then it returns this LogEntryList after removing
+ * the entries that happened up to [when]; otherwise a new
+ * list is created.
+ */
+ LogEntryList after(Date when, [bool inPlace = false]) =>
+ _tail((e) => e.time > when, inPlace, 'after $when', logs.length);
+
+ /**
+ * Returns log events that happened from [when] onwards. If
+ * [inPlace] is true, then it returns this LogEntryList after
+ * removing the entries that happened before [when]; otherwise
+ * a new list is created.
+ */
+ LogEntryList from(Date when, [bool inPlace = false]) =>
+ _tail((e) => e.time >= when, inPlace, 'from $when', logs.length);
+
+ /**
+ * Returns log events that happened until [when]. If [inPlace]
+ * is true, then it returns this LogEntryList after removing
+ * the entries that happened after [when]; otherwise a new
+ * list is created.
+ */
+ LogEntryList until(Date when, [bool inPlace = false]) =>
+ _head((e) => e.time > when, inPlace, 'until $when', logs.length);
+
+ /**
+ * Returns log events that happened before [when]. If [inPlace]
+ * is true, then it returns this LogEntryList after removing
+ * the entries that happened from [when] onwards; otherwise a new
+ * list is created.
+ */
+ LogEntryList before(Date when, [bool inPlace = false]) =>
+ _head((e) => e.time >= when, inPlace, 'before $when', logs.length);
+
+ /**
+ * Returns log events that happened after [logEntry]'s time.
+ * If [inPlace] is true, then it returns this LogEntryList after
+ * removing the entries that happened up to [when]; otherwise a new
+ * list is created. If [logEntry] is null the current time is used.
+ */
+ LogEntryList afterEntry(LogEntry logEntry, [bool inPlace = false]) =>
+ after(logEntry == null ? new Date.now() : logEntry.time);
+
+ /**
+ * Returns log events that happened from [logEntry]'s time onwards.
+ * If [inPlace] is true, then it returns this LogEntryList after
+ * removing the entries that happened before [when]; otherwise
+ * a new list is created. If [logEntry] is null the current time is used.
+ */
+ LogEntryList fromEntry(LogEntry logEntry, [bool inPlace = false]) =>
+ from(logEntry == null ? new Date.now() : logEntry.time);
+
+ /**
+ * Returns log events that happened until [logEntry]'s time. If
+ * [inPlace] is true, then it returns this LogEntryList after removing
+ * the entries that happened after [when]; otherwise a new
+ * list is created. If [logEntry] is null the epoch time is used.
+ */
+ LogEntryList untilEntry(LogEntry logEntry, [bool inPlace = false]) =>
+ until(logEntry == null ?
+ new Date.fromMillisecondsSinceEpoch(0) : logEntry.time);
+
+ /**
+ * Returns log events that happened before [logEntry]'s time. If
+ * [inPlace] is true, then it returns this LogEntryList after removing
+ * the entries that happened from [when] onwards; otherwise a new
+ * list is created. If [logEntry] is null the epoch time is used.
+ */
+ LogEntryList beforeEntry(LogEntry logEntry, [bool inPlace = false]) =>
+ before(logEntry == null ?
+ new Date.fromMillisecondsSinceEpoch(0) : logEntry.time);
+
+ /**
+ * Returns log events that happened after the first event in [segment].
+ * If [inPlace] is true, then it returns this LogEntryList after removing
+ * the entries that happened earlier; otherwise a new list is created.
+ */
+ LogEntryList afterFirst(LogEntryList segment, [bool inPlace = false]) =>
+ afterEntry(segment.first, inPlace);
+
+ /**
+ * Returns log events that happened after the last event in [segment].
+ * If [inPlace] is true, then it returns this LogEntryList after removing
+ * the entries that happened earlier; otherwise a new list is created.
+ */
+ LogEntryList afterLast(LogEntryList segment, [bool inPlace = false]) =>
+ afterEntry(segment.last, inPlace);
+
+ /**
+ * Returns log events that happened from the time of the first event in
+ * [segment] onwards. If [inPlace] is true, then it returns this
+ * LogEntryList after removing the earlier entries; otherwise a new list
+ * is created.
+ */
+ LogEntryList fromFirst(LogEntryList segment, [bool inPlace = false]) =>
+ fromEntry(segment.first, inPlace);
+
+ /**
+ * Returns log events that happened from the time of the last event in
+ * [segment] onwards. If [inPlace] is true, then it returns this
+ * LogEntryList after removing the earlier entries; otherwise a new list
+ * is created.
+ */
+ LogEntryList fromLast(LogEntryList segment, [bool inPlace = false]) =>
+ fromEntry(segment.last, inPlace);
+
+ /**
+ * Returns log events that happened until the first event in [segment].
+ * If [inPlace] is true, then it returns this LogEntryList after removing
+ * the entries that happened later; otherwise a new list is created.
+ */
+ LogEntryList untilFirst(LogEntryList segment, [bool inPlace = false]) =>
+ untilEntry(segment.first, inPlace);
+
+ /**
+ * Returns log events that happened until the last event in [segment].
+ * If [inPlace] is true, then it returns this LogEntryList after removing
+ * the entries that happened later; otherwise a new list is created.
+ */
+ LogEntryList untilLast(LogEntryList segment, [bool inPlace = false]) =>
+ untilEntry(segment.last, inPlace);
+
+ /**
+ * Returns log events that happened before the first event in [segment].
+ * If [inPlace] is true, then it returns this LogEntryList after removing
+ * the entries that happened later; otherwise a new list is created.
+ */
+ LogEntryList beforeFirst(LogEntryList segment, [bool inPlace = false]) =>
+ beforeEntry(segment.first, inPlace);
+
+ /**
+ * Returns log events that happened before the last event in [segment].
+ * If [inPlace] is true, then it returns this LogEntryList after removing
+ * the entries that happened later; otherwise a new list is created.
+ */
+ LogEntryList beforeLast(LogEntryList segment, [bool inPlace = false]) =>
+ beforeEntry(segment.last, inPlace);
+
+ /**
+ * Iterate through the LogEntryList looking for matches to the entries
+ * in [keys]; for each match found the closest [distance] neighboring log
+ * entries that match [mockNameFilter] and [logFilter] will be included in
+ * the result. If [isPreceding] is true we use the neighbors that precede
+ * the matched entry; else we use the neighbors that followed.
+ * If [includeKeys] is true then the entries in [keys] that resulted in
+ * entries in the output list are themselves included in the output list. If
+ * [distance] is zero then all matches are included.
+ */
+ LogEntryList _neighboring(bool isPreceding,
+ LogEntryList keys,
+ mockNameFilter,
+ logFilter,
+ int distance,
+ bool includeKeys) {
+ String filterName = 'Calls to '
+ '${_qualifiedName(mockNameFilter, logFilter.toString())} '
+ '${isPreceding?"preceding":"following"} ${keys.filter}';
+
+ LogEntryList rtn = new LogEntryList(filterName);
+
+ // Deal with the trivial case.
+ if (logs.length == 0 || keys.logs.length == 0) {
+ return rtn;
+ }
+
+ // Normalize the mockNameFilter and logFilter values.
+ if (mockNameFilter == null) {
+ mockNameFilter = anything;
+ } else {
+ mockNameFilter = wrapMatcher(mockNameFilter);
+ }
+ logFilter = _makePredicate(logFilter);
+
+ // The scratch list is used to hold matching entries when we
+ // are doing preceding neighbors. The remainingCount is used to
+ // keep track of how many matching entries we can still add in the
+ // current segment (0 if we are doing doing following neighbors, until
+ // we get our first key match).
+ List scratch = null;
+ int remainingCount = 0;
+ if (isPreceding) {
+ scratch = new List();
+ remainingCount = logs.length;
+ }
+
+ var keyIterator = keys.logs.iterator();
+ LogEntry keyEntry = keyIterator.next();
+ MatchState matchState = new MatchState();
+
+ for (LogEntry logEntry in logs) {
+ // If we have a log entry match, copy the saved matches from the
+ // scratch buffer into the return list, as well as the matching entry,
+ // if appropriate, and reset the scratch buffer. Continue processing
+ // from the next key entry.
+ if (keyEntry == logEntry) {
+ if (scratch != null) {
+ int numToCopy = scratch.length;
+ if (distance > 0 && distance < numToCopy) {
+ numToCopy = distance;
+ }
+ for (var i = scratch.length - numToCopy; i < scratch.length; i++) {
+ rtn.logs.add(scratch[i]);
+ }
+ scratch.clear();
+ } else {
+ remainingCount = distance > 0 ? distance : logs.length;
+ }
+ if (includeKeys) {
+ rtn.logs.add(keyEntry);
+ }
+ if (keyIterator.hasNext) {
+ keyEntry = keyIterator.next();
+ } else if (isPreceding) { // We're done.
+ break;
+ }
+ } else if (remainingCount > 0 &&
+ mockNameFilter.matches(logEntry.mockName, matchState) &&
+ logFilter(logEntry)) {
+ if (scratch != null) {
+ scratch.add(logEntry);
+ } else {
+ rtn.logs.add(logEntry);
+ --remainingCount;
+ }
+ }
+ }
+ return rtn;
+ }
+
+ /**
+ * Iterate through the LogEntryList looking for matches to the entries
+ * in [keys]; for each match found the closest [distance] prior log entries
+ * that match [mocknameFilter] and [logFilter] will be included in the result.
+ * If [includeKeys] is true then the entries in [keys] that resulted in
+ * entries in the output list are themselves included in the output list. If
+ * [distance] is zero then all matches are included.
+ *
+ * The idea here is that you could find log entries that are related to
+ * other logs entries in some temporal sense. For example, say we have a
+ * method commit() that returns -1 on failure. Before commit() gets called
+ * the value being committed is created by process(). We may want to find
+ * the calls to process() that preceded calls to commit() that failed.
+ * We could do this with:
+ *
+ * print(log.preceding(log.getLogs(callsTo('commit'), returning(-1)),
+ * logFilter: callsTo('process')).toString());
+ *
+ * We might want to include the details of the failing calls to commit()
+ * to see what parameters were passed in, in which case we would set
+ * [includeKeys].
+ *
+ * As another simple example, say we wanted to know the three method
+ * calls that immediately preceded each failing call to commit():
+ *
+ * print(log.preceding(log.getLogs(callsTo('commit'), returning(-1)),
+ * distance: 3).toString());
+ */
+ LogEntryList preceding(LogEntryList keys,
+ {mockNameFilter: null,
+ logFilter: null,
+ int distance: 1,
+ bool includeKeys: false}) =>
+ _neighboring(true, keys, mockNameFilter, logFilter,
+ distance, includeKeys);
+
+ /**
+ * Iterate through the LogEntryList looking for matches to the entries
+ * in [keys]; for each match found the closest [distance] subsequent log
+ * entries that match [mocknameFilter] and [logFilter] will be included in
+ * the result. If [includeKeys] is true then the entries in [keys] that
+ * resulted in entries in the output list are themselves included in the
+ * output list. If [distance] is zero then all matches are included.
+ * See [preceding] for a usage example.
+ */
+ LogEntryList following(LogEntryList keys,
+ {mockNameFilter: null,
+ logFilter: null,
+ int distance: 1,
+ bool includeKeys: false}) =>
+ _neighboring(false, keys, mockNameFilter, logFilter,
+ distance, includeKeys);
+}
+
+/**
+ * [_TimesMatcher]s are used to make assertions about the number of
+ * times a method was called.
+ */
+class _TimesMatcher extends BaseMatcher {
+ final int min, max;
+
+ const _TimesMatcher(this.min, [this.max = -1]);
+
+ bool matches(logList, MatchState matchState) => logList.length >= min &&
+ (max < 0 || logList.length <= max);
+
+ Description describe(Description description) {
+ description.add('to be called ');
+ if (max < 0) {
+ description.add('at least $min');
+ } else if (max == min) {
+ description.add('$max');
+ } else if (min == 0) {
+ description.add('at most $max');
+ } else {
+ description.add('between $min and $max');
+ }
+ return description.add(' times');
+ }
+
+ Description describeMismatch(logList, Description mismatchDescription,
+ MatchState matchState, bool verbose) =>
+ mismatchDescription.add('was called ${logList.length} times');
+}
+
+/** [happenedExactly] matches an exact number of calls. */
+Matcher happenedExactly(count) {
+ return new _TimesMatcher(count, count);
+}
+
+/** [happenedAtLeast] matches a minimum number of calls. */
+Matcher happenedAtLeast(count) {
+ return new _TimesMatcher(count);
+}
+
+/** [happenedAtMost] matches a maximum number of calls. */
+Matcher happenedAtMost(count) {
+ return new _TimesMatcher(0, count);
+}
+
+/** [neverHappened] matches zero calls. */
+const Matcher neverHappened = const _TimesMatcher(0, 0);
+
+/** [happenedOnce] matches exactly one call. */
+const Matcher happenedOnce = const _TimesMatcher(1, 1);
+
+/** [happenedAtLeastOnce] matches one or more calls. */
+const Matcher happenedAtLeastOnce = const _TimesMatcher(1);
+
+/** [happenedAtMostOnce] matches zero or one call. */
+const Matcher happenedAtMostOnce = const _TimesMatcher(0, 1);
+
+/**
+ * [_ResultMatcher]s are used to make assertions about the results
+ * of method calls. These can be used as optional parameters to [getLogs].
+ */
+class _ResultMatcher extends BaseMatcher {
+ final Action action;
+ final Matcher value;
+
+ const _ResultMatcher(this.action, this.value);
+
+ bool matches(item, MatchState matchState) {
+ if (item is! LogEntry) {
+ return false;
+ }
+ // normalize the action; _PROXY is like _RETURN.
+ Action eaction = item.action;
+ if (eaction == Action.PROXY) {
+ eaction = Action.RETURN;
+ }
+ return (eaction == action && value.matches(item.value, matchState));
+ }
+
+ Description describe(Description description) {
+ description.add(' to ');
+ if (action == Action.RETURN || action == Action.PROXY)
+ description.add('return ');
+ else
+ description.add('throw ');
+ return description.addDescriptionOf(value);
+ }
+
+ Description describeMismatch(item, Description mismatchDescription,
+ MatchState matchState, bool verbose) {
+ if (item.action == Action.RETURN || item.action == Action.PROXY) {
+ mismatchDescription.add('returned ');
+ } else {
+ mismatchDescription.add('threw ');
+ }
+ mismatchDescription.add(item.value);
+ return mismatchDescription;
+ }
+}
+
+/**
+ *[returning] matches log entries where the call to a method returned
+ * a value that matched [value].
+ */
+Matcher returning(value) =>
+ new _ResultMatcher(Action.RETURN, wrapMatcher(value));
+
+/**
+ *[throwing] matches log entrues where the call to a method threw
+ * a value that matched [value].
+ */
+Matcher throwing(value) =>
+ new _ResultMatcher(Action.THROW, wrapMatcher(value));
+
+/** Special values for use with [_ResultSetMatcher] [frequency]. */
+class _Frequency {
+ /** Every call/throw must match */
+ static const ALL = const _Frequency._('ALL');
+
+ /** At least one call/throw must match. */
+ static const SOME = const _Frequency._('SOME');
+
+ /** No calls/throws should match. */
+ static const NONE = const _Frequency._('NONE');
+
+ const _Frequency._(this.name);
+
+ final String name;
+}
+
+/**
+ * [_ResultSetMatcher]s are used to make assertions about the results
+ * of method calls. When filtering an execution log by calling
+ * [getLogs], a [LogEntrySet] of matching call logs is returned;
+ * [_ResultSetMatcher]s can then assert various things about this
+ * (sub)set of logs.
+ *
+ * We could make this class use _ResultMatcher but it doesn't buy that
+ * match and adds some perf hit, so there is some duplication here.
+ */
+class _ResultSetMatcher extends BaseMatcher {
+ final Action action;
+ final Matcher value;
+ final _Frequency frequency; // ALL, SOME, or NONE.
+
+ const _ResultSetMatcher(this.action, this.value, this.frequency);
+
+ bool matches(logList, MatchState matchState) {
+ for (LogEntry entry in logList) {
+ // normalize the action; PROXY is like RETURN.
+ Action eaction = entry.action;
+ if (eaction == Action.PROXY) {
+ eaction = Action.RETURN;
+ }
+ if (eaction == action && value.matches(entry.value, matchState)) {
+ if (frequency == _Frequency.NONE) {
+ matchState.state = {
+ 'state' : matchState.state,
+ 'entry' : entry
+ };
+ return false;
+ } else if (frequency == _Frequency.SOME) {
+ return true;
+ }
+ } else {
+ // Mismatch.
+ if (frequency == _Frequency.ALL) { // We need just one mismatch to fail.
+ matchState.state = {
+ 'state' : matchState.state,
+ 'entry' : entry
+ };
+ return false;
+ }
+ }
+ }
+ // If we get here, then if count is _ALL we got all matches and
+ // this is success; otherwise we got all mismatched which is
+ // success for count == _NONE and failure for count == _SOME.
+ return (frequency != _Frequency.SOME);
+ }
+
+ Description describe(Description description) {
+ description.add(' to ');
+ description.add(frequency == _Frequency.ALL ? 'alway ' :
+ (frequency == _Frequency.NONE ? 'never ' : 'sometimes '));
+ if (action == Action.RETURN || action == Action.PROXY)
+ description.add('return ');
+ else
+ description.add('throw ');
+ return description.addDescriptionOf(value);
+ }
+
+ Description describeMismatch(logList, Description mismatchDescription,
+ MatchState matchState, bool verbose) {
+ if (frequency != _Frequency.SOME) {
+ LogEntry entry = matchState.state['entry'];
+ if (entry.action == Action.RETURN || entry.action == Action.PROXY) {
+ mismatchDescription.add('returned');
+ } else {
+ mismatchDescription.add('threw');
+ }
+ mismatchDescription.add(' value that ');
+ value.describeMismatch(entry.value, mismatchDescription,
+ matchState.state['state'], verbose);
+ mismatchDescription.add(' at least once');
+ } else {
+ mismatchDescription.add('never did');
+ }
+ return mismatchDescription;
+ }
+}
+
+/**
+ *[alwaysReturned] asserts that all matching calls to a method returned
+ * a value that matched [value].
+ */
+Matcher alwaysReturned(value) =>
+ new _ResultSetMatcher(Action.RETURN, wrapMatcher(value), _Frequency.ALL);
+
+/**
+ *[sometimeReturned] asserts that at least one matching call to a method
+ * returned a value that matched [value].
+ */
+Matcher sometimeReturned(value) =>
+ new _ResultSetMatcher(Action.RETURN, wrapMatcher(value), _Frequency.SOME);
+
+/**
+ *[neverReturned] asserts that no matching calls to a method returned
+ * a value that matched [value].
+ */
+Matcher neverReturned(value) =>
+ new _ResultSetMatcher(Action.RETURN, wrapMatcher(value), _Frequency.NONE);
+
+/**
+ *[alwaysThrew] asserts that all matching calls to a method threw
+ * a value that matched [value].
+ */
+Matcher alwaysThrew(value) =>
+ new _ResultSetMatcher(Action.THROW, wrapMatcher(value), _Frequency.ALL);
+
+/**
+ *[sometimeThrew] asserts that at least one matching call to a method threw
+ * a value that matched [value].
+ */
+Matcher sometimeThrew(value) =>
+ new _ResultSetMatcher(Action.THROW, wrapMatcher(value), _Frequency.SOME);
+
+/**
+ *[neverThrew] asserts that no matching call to a method threw
+ * a value that matched [value].
+ */
+Matcher neverThrew(value) =>
+ new _ResultSetMatcher(Action.THROW, wrapMatcher(value), _Frequency.NONE);
+
+/** The shared log used for named mocks. */
+LogEntryList sharedLog = null;
+
+/**
+ * [Mock] is the base class for all mocked objects, with
+ * support for basic mocking.
+ *
+ * To create a mock objects for some class T, create a new class using:
+ *
+ * class MockT extends Mock implements T {};
+ *
+ * Then specify the [Behavior] of the Mock for different methods using
+ * [when] (to select the method and parameters) and then the [Action]s
+ * for the [Behavior] by calling [thenReturn], [alwaysReturn], [thenThrow],
+ * [alwaysThrow], [thenCall] or [alwaysCall].
+ *
+ * [thenReturn], [thenThrow] and [thenCall] are one-shot so you would
+ * typically call these more than once to specify a sequence of actions;
+ * this can be done with chained calls, e.g.:
+ *
+ * m.when(callsTo('foo')).
+ * thenReturn(0).thenReturn(1).thenReturn(2);
+ *
+ * [thenCall] and [alwaysCall] allow you to proxy mocked methods, chaining
+ * to some other implementation. This provides a way to implement 'spies'.
+ *
+ * You can disable logging for a particular [Behavior] easily:
+ *
+ * m.when(callsTo('bar')).logging = false;
+ *
+ * You can then use the mock object. Once you are done, to verify the
+ * behavior, use [getLogs] to extract a relevant subset of method call
+ * logs and apply [Matchers] to these through calling [verify].
+ *
+ * A Mock can be given a name when constructed. In this case instead of
+ * keeping its own log, it uses a shared log. This can be useful to get an
+ * audit trail of interleaved behavior. It is the responsibility of the user
+ * to ensure that mock names, if used, are unique.
+ *
+ * Limitations:
+ * - only positional parameters are supported (up to 10);
+ * - to mock getters you will need to include parentheses in the call
+ * (e.g. m.length() will work but not m.length).
+ *
+ * Here is a simple example:
+ *
+ * class MockList extends Mock implements List {};
+ *
+ * List m = new MockList();
+ * m.when(callsTo('add', anything)).alwaysReturn(0);
+ *
+ * m.add('foo');
+ * m.add('bar');
+ *
+ * getLogs(m, callsTo('add', anything)).verify(happenedExactly(2));
+ * getLogs(m, callsTo('add', 'foo')).verify(happenedOnce);
+ * getLogs(m, callsTo('add', 'isNull)).verify(neverHappened);
+ *
+ * Note that we don't need to provide argument matchers for all arguments,
+ * but we do need to provide arguments for all matchers. So this is allowed:
+ *
+ * m.when(callsTo('add')).alwaysReturn(0);
+ * m.add(1, 2);
+ *
+ * But this is not allowed and will throw an exception:
+ *
+ * m.when(callsTo('add', anything, anything)).alwaysReturn(0);
+ * m.add(1);
+ *
+ * Here is a way to implement a 'spy', which is where we log the call
+ * but then hand it off to some other function, which is the same
+ * method in a real instance of the class being mocked:
+ *
+ * class Foo {
+ * bar(a, b, c) => a + b + c;
+ * }
+ *
+ * class MockFoo extends Mock implements Foo {
+ * Foo real;
+ * MockFoo() {
+ * real = new Foo();
+ * this.when(callsTo('bar')).alwaysCall(real.bar);
+ * }
+ * }
+ *
+ */
+class Mock {
+ /** The mock name. Needed if the log is shared; optional otherwise. */
+ final String name;
+
+ /** The set of [Behavior]s supported. */
+ Map<String,Behavior> _behaviors;
+
+ /** The [log] of calls made. Only used if [name] is null. */
+ LogEntryList log;
+
+ /** How to handle unknown method calls - swallow or throw. */
+ final bool _throwIfNoBehavior;
+
+ /** Whether to create an audit log or not. */
+ bool _logging;
+
+ bool get logging => _logging;
+ set logging(bool value) {
+ if (value && log == null) {
+ log = new LogEntryList();
+ }
+ _logging = value;
+ }
+
+ /**
+ * Default constructor. Unknown method calls are allowed and logged,
+ * the mock has no name, and has its own log.
+ */
+ Mock() : _throwIfNoBehavior = false, log = null, name = null {
+ logging = true;
+ _behaviors = new Map<String,Behavior>();
+ }
+
+ /**
+ * This constructor makes a mock that has a [name] and possibly uses
+ * a shared [log]. If [throwIfNoBehavior] is true, any calls to methods
+ * that have no defined behaviors will throw an exception; otherwise they
+ * will be allowed and logged (but will not do anything).
+ * If [enableLogging] is false, no logging will be done initially (whether
+ * or not a [log] is supplied), but [logging] can be set to true later.
+ */
+ Mock.custom({this.name,
+ this.log,
+ throwIfNoBehavior: false,
+ enableLogging: true}) : _throwIfNoBehavior = throwIfNoBehavior {
+ if (log != null && name == null) {
+ throw new Exception("Mocks with shared logs must have a name.");
+ }
+ logging = enableLogging;
+ _behaviors = new Map<String,Behavior>();
+ }
+
+ /**
+ * [when] is used to create a new or extend an existing [Behavior].
+ * A [CallMatcher] [filter] must be supplied, and the [Behavior]s for
+ * that signature are returned (being created first if needed).
+ *
+ * Typical use case:
+ *
+ * mock.when(callsTo(...)).alwaysReturn(...);
+ */
+ Behavior when(CallMatcher logFilter) {
+ String key = logFilter.toString();
+ if (!_behaviors.containsKey(key)) {
+ Behavior b = new Behavior(logFilter);
+ _behaviors[key] = b;
+ return b;
+ } else {
+ return _behaviors[key];
+ }
+ }
+
+ /**
+ * This is the handler for method calls. We loo through the list
+ * of [Behavior]s, and find the first match that still has return
+ * values available, and then do the action specified by that
+ * return value. If we find no [Behavior] to apply an exception is
+ * thrown.
+ */
+ noSuchMethod(String method, List args) {
+ if (method.startsWith('get:')) {
+ method = 'get ${method.substring(4)}';
+ }
+ bool matchedMethodName = false;
+ MatchState matchState = new MatchState();
+ for (String k in _behaviors.keys) {
+ Behavior b = _behaviors[k];
+ if (b.matcher.nameFilter.matches(method, matchState)) {
+ matchedMethodName = true;
+ }
+ if (b.matches(method, args)) {
+ List actions = b.actions;
+ if (actions == null || actions.length == 0) {
+ continue; // No return values left in this Behavior.
+ }
+ // Get the first response.
+ Responder response = actions[0];
+ // If it is exhausted, remove it from the list.
+ // Note that for endlessly repeating values, we started the count at
+ // 0, so we get a potentially useful value here, which is the
+ // (negation of) the number of times we returned the value.
+ if (--response.count == 0) {
+ actions.removeRange(0, 1);
+ }
+ // Do the response.
+ Action action = response.action;
+ var value = response.value;
+ if (action == Action.RETURN) {
+ if (_logging && b.logging) {
+ log.add(new LogEntry(name, method, args, action, value));
+ }
+ return value;
+ } else if (action == Action.THROW) {
+ if (_logging && b.logging) {
+ log.add(new LogEntry(name, method, args, action, value));
+ }
+ throw value;
+ } else if (action == Action.PROXY) {
+ var rtn;
+ switch (args.length) {
+ case 0:
+ rtn = value();
+ break;
+ case 1:
+ rtn = value(args[0]);
+ break;
+ case 2:
+ rtn = value(args[0], args[1]);
+ break;
+ case 3:
+ rtn = value(args[0], args[1], args[2]);
+ break;
+ case 4:
+ rtn = value(args[0], args[1], args[2], args[3]);
+ break;
+ case 5:
+ rtn = value(args[0], args[1], args[2], args[3], args[4]);
+ break;
+ case 6:
+ rtn = value(args[0], args[1], args[2], args[3],
+ args[4], args[5]);
+ break;
+ case 7:
+ rtn = value(args[0], args[1], args[2], args[3],
+ args[4], args[5], args[6]);
+ break;
+ case 8:
+ rtn = value(args[0], args[1], args[2], args[3],
+ args[4], args[5], args[6], args[7]);
+ break;
+ case 9:
+ rtn = value(args[0], args[1], args[2], args[3],
+ args[4], args[5], args[6], args[7], args[8]);
+ break;
+ case 9:
+ rtn = value(args[0], args[1], args[2], args[3],
+ args[4], args[5], args[6], args[7], args[8], args[9]);
+ break;
+ default:
+ throw new Exception(
+ "Cannot proxy calls with more than 10 parameters.");
+ }
+ if (_logging && b.logging) {
+ log.add(new LogEntry(name, method, args, action, rtn));
+ }
+ return rtn;
+ }
+ }
+ }
+ if (matchedMethodName) {
+ // User did specify behavior for this method, but all the
+ // actions are exhausted. This is considered an error.
+ throw new Exception('No more actions for method '
+ '${_qualifiedName(name, method)}.');
+ } else if (_throwIfNoBehavior) {
+ throw new Exception('No behavior specified for method '
+ '${_qualifiedName(name, method)}.');
+ }
+ // Otherwise user hasn't specified behavior for this method; we don't throw
+ // so we can underspecify.
+ if (_logging) {
+ log.add(new LogEntry(name, method, args, Action.IGNORE));
+ }
+ }
+
+ /** [verifyZeroInteractions] returns true if no calls were made */
+ bool verifyZeroInteractions() {
+ if (log == null) {
+ // This means we created the mock with logging off and have never turned
+ // it on, so it doesn't make sense to verify behavior on such a mock.
+ throw new
+ Exception("Can't verify behavior when logging was never enabled.");
+ }
+ return log.logs.length == 0;
+ }
+
+ /**
+ * [getLogs] extracts all calls from the call log that match the
+ * [logFilter], and returns the matching list of [LogEntry]s. If
+ * [destructive] is false (the default) the matching calls are left
+ * in the log, else they are removed. Removal allows us to verify a
+ * set of interactions and then verify that there are no other
+ * interactions left. [actionMatcher] can be used to further
+ * restrict the returned logs based on the action the mock performed.
+ * [logFilter] can be a [CallMatcher] or a predicate function that
+ * takes a [LogEntry] and returns a bool.
+ *
+ * Typical usage:
+ *
+ * getLogs(callsTo(...)).verify(...);
+ */
+ LogEntryList getLogs([CallMatcher logFilter,
+ Matcher actionMatcher,
+ bool destructive = false]) {
+ if (log == null) {
+ // This means we created the mock with logging off and have never turned
+ // it on, so it doesn't make sense to get logs from such a mock.
+ throw new
+ Exception("Can't retrieve logs when logging was never enabled.");
+ } else {
+ return log.getMatches(name, logFilter, actionMatcher, destructive);
+ }
+ }
+
+ /**
+ * Useful shorthand method that creates a [CallMatcher] from its arguments
+ * and then calls [getLogs].
+ */
+ LogEntryList calls(method,
+ [arg0 = _noArg,
+ arg1 = _noArg,
+ arg2 = _noArg,
+ arg3 = _noArg,
+ arg4 = _noArg,
+ arg5 = _noArg,
+ arg6 = _noArg,
+ arg7 = _noArg,
+ arg8 = _noArg,
+ arg9 = _noArg]) =>
+ getLogs(callsTo(method, arg0, arg1, arg2, arg3, arg4,
+ arg5, arg6, arg7, arg8, arg9));
+
+ /** Clear the behaviors for the Mock. */
+ void resetBehavior() => _behaviors.clear();
+
+ /** Clear the logs for the Mock. */
+ void clearLogs() {
+ if (log != null) {
+ if (name == null) { // This log is not shared.
+ log.logs.clear();
+ } else { // This log may be shared.
+ log.logs = log.logs.filter((e) => e.mockName != name);
+ }
+ }
+ }
+
+ /** Clear both logs and behavior. */
+ void reset() {
+ resetBehavior();
+ clearLogs();
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698