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

Unified Diff: pkg/mock/lib/src/log_entry_list.dart

Issue 233243007: pkg/mock: restructure library layout (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 8 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
« no previous file with comments | « pkg/mock/lib/src/log_entry.dart ('k') | pkg/mock/lib/src/mock.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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();
+}
« no previous file with comments | « pkg/mock/lib/src/log_entry.dart ('k') | pkg/mock/lib/src/mock.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698