blob: b96baab2e6e94858728e7e727216cfa200d1bdb0 [file] [log] [blame]
// 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();
}