| Index: pkg/mock/lib/src/log_entry_list.dart
|
| diff --git a/pkg/mock/lib/src/log_entry_list.dart b/pkg/mock/lib/src/log_entry_list.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..6c612ba5354e9e094d0814959145f647cf30fa6b
|
| --- /dev/null
|
| +++ b/pkg/mock/lib/src/log_entry_list.dart
|
| @@ -0,0 +1,564 @@
|
| +// Copyright (c) 2014, 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.
|
| +
|
| +library mock.log_entry_list;
|
| +
|
| +import 'package:matcher/matcher.dart';
|
| +
|
| +import 'call_matcher.dart';
|
| +import 'log_entry.dart';
|
| +import 'util.dart';
|
| +
|
| +/**
|
| +* [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);
|
| + var 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) {
|
| + int startIndex = i--;
|
| + logs.removeRange(startIndex, startIndex + 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([DateTime 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);
|
| + }
|
| + 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(DateTime when, [bool inPlace = false]) =>
|
| + _tail((e) => e.time.isAfter(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(DateTime when, [bool inPlace = false]) =>
|
| + _tail((e) => !e.time.isBefore(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(DateTime when, [bool inPlace = false]) =>
|
| + _head((e) => e.time.isAfter(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(DateTime when, [bool inPlace = false]) =>
|
| + _head((e) => !e.time.isBefore(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 DateTime.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 DateTime.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 DateTime.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 DateTime.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;
|
| + keyIterator.moveNext();
|
| + LogEntry keyEntry = keyIterator.current;
|
| + Map 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.moveNext()) {
|
| + keyEntry = keyIterator.current;
|
| + } 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);
|
| +}
|
| +
|
| +_MockFailureHandler _mockFailureHandler = null;
|
| +
|
| +/**
|
| + * 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,
|
| + Map matchState, bool verbose) {
|
| + proxy.fail(_mockingErrorFormatter(actual, matcher, reason,
|
| + matchState, verbose));
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * 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,
|
| + Map 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();
|
| +}
|
|
|