| Index: pkg/mock/lib/src/mock.dart
|
| diff --git a/pkg/mock/lib/src/mock.dart b/pkg/mock/lib/src/mock.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..29598b83915e1c752d24bcf4185551247ed75a50
|
| --- /dev/null
|
| +++ b/pkg/mock/lib/src/mock.dart
|
| @@ -0,0 +1,325 @@
|
| +// 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.mock;
|
| +
|
| +// TOOD(kevmoo): just use `Map`
|
| +import 'dart:collection' show LinkedHashMap;
|
| +import 'dart:mirrors';
|
| +
|
| +import 'package:matcher/matcher.dart';
|
| +
|
| +import 'action.dart';
|
| +import 'behavior.dart';
|
| +import 'call_matcher.dart';
|
| +import 'log_entry.dart';
|
| +import 'log_entry_list.dart';
|
| +import 'responder.dart';
|
| +import 'util.dart';
|
| +
|
| +/** The base class for all mocked objects. */
|
| +@proxy
|
| +class Mock {
|
| + /** The mock name. Needed if the log is shared; optional otherwise. */
|
| + final String name;
|
| +
|
| + /** The set of [Behavior]s supported. */
|
| + final LinkedHashMap<String,Behavior> _behaviors;
|
| +
|
| + /** How to handle unknown method calls - swallow or throw. */
|
| + final bool _throwIfNoBehavior;
|
| +
|
| + /** For spys, the real object that we are spying on. */
|
| + final Object _realObject;
|
| +
|
| + /** The [log] of calls made. Only used if [name] is null. */
|
| + LogEntryList log;
|
| +
|
| + /** 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, _realObject = null,
|
| + _behaviors = new LinkedHashMap<String,Behavior>() {
|
| + logging = true;
|
| + }
|
| +
|
| + /**
|
| + * 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, _realObject = null,
|
| + _behaviors = new LinkedHashMap<String,Behavior>() {
|
| + if (log != null && name == null) {
|
| + throw new Exception("Mocks with shared logs must have a name.");
|
| + }
|
| + logging = enableLogging;
|
| + }
|
| +
|
| + /**
|
| + * This constructor creates a spy with no user-defined behavior.
|
| + * This is simply a proxy for a real object that passes calls
|
| + * through to that real object but captures an audit trail of
|
| + * calls made to the object that can be queried and validated
|
| + * later.
|
| + */
|
| + Mock.spy(this._realObject, {this.name, this.log})
|
| + : _behaviors = null,
|
| + _throwIfNoBehavior = true {
|
| + logging = true;
|
| + }
|
| +
|
| + /**
|
| + * [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 loop 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(Invocation invocation) {
|
| + var method = MirrorSystem.getName(invocation.memberName);
|
| + var args = invocation.positionalArguments;
|
| + if (invocation.isGetter) {
|
| + method = 'get $method';
|
| + } else if (invocation.isSetter) {
|
| + method = 'set $method';
|
| + // Remove the trailing '='.
|
| + if (method[method.length-1] == '=') {
|
| + method = method.substring(0, method.length - 1);
|
| + }
|
| + }
|
| + if (_behaviors == null) { // Spy.
|
| + var mirror = reflect(_realObject);
|
| + try {
|
| + var result = mirror.delegate(invocation);
|
| + log.add(new LogEntry(name, method, args, Action.PROXY, result));
|
| + return result;
|
| + } catch (e) {
|
| + log.add(new LogEntry(name, method, args, Action.THROW, e));
|
| + throw e;
|
| + }
|
| + }
|
| + bool matchedMethodName = false;
|
| + Map 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) {
|
| + // TODO(gram): Replace all this with:
|
| + // var rtn = reflect(value).apply(invocation.positionalArguments,
|
| + // invocation.namedArguments);
|
| + // once that is supported.
|
| + 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 = NO_ARG,
|
| + arg1 = NO_ARG,
|
| + arg2 = NO_ARG,
|
| + arg3 = NO_ARG,
|
| + arg4 = NO_ARG,
|
| + arg5 = NO_ARG,
|
| + arg6 = NO_ARG,
|
| + arg7 = NO_ARG,
|
| + arg8 = NO_ARG,
|
| + arg9 = NO_ARG]) =>
|
| + 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.where((e) => e.mockName != name).toList();
|
| + }
|
| + }
|
| + }
|
| +
|
| + /** Clear both logs and behavior. */
|
| + void reset() {
|
| + resetBehavior();
|
| + clearLogs();
|
| + }
|
| +}
|
|
|