diff options
author | Gustavo Niemeyer <gustavo@niemeyer.net> | 2007-10-29 00:13:18 -0200 |
---|---|---|
committer | Gustavo Niemeyer <gustavo@niemeyer.net> | 2007-10-29 00:13:18 -0200 |
commit | dddccbae4b89957a5664462b1ca953357d51d59e (patch) | |
tree | ff53d141c9f2af07e535bbfd08f5b97bf30c6086 | |
parent | 672b6a5b727c10a32c5274aee4e11a639f5ae27f (diff) | |
download | mocker-dddccbae4b89957a5664462b1ca953357d51d59e.tar.gz |
Moved it forward, most importantly implementing the patching feature.
-rw-r--r-- | mocker.py | 885 | ||||
-rwxr-xr-x | test.py | 1333 |
2 files changed, 1744 insertions, 474 deletions
@@ -1,15 +1,21 @@ +import __builtin__ import inspect +import types import sys +import os import gc __all__ = ["Mocker", "expect", "SAME", "CONTAINS", "ANY", "VARIOUS"] +ERROR_PREFIX = "[Mocker] " + + # -------------------------------------------------------------------- # Exceptions -class UnexpectedExprError(AssertionError): +class MatchError(AssertionError): """Raised when an unknown expression is seen in playback mode.""" @@ -73,7 +79,55 @@ RESTORE = State("RESTORE") class MockerBase(object): - """This is the implementation of Mocker, without any recorders.""" + """Controller of mock objects. + + A mocker instance is used to command recording and replay of + expectations on any number of mock objects. + + Expectations should be expressed for the mock object while in + record mode (the initial one) by using the mock object itself, + and using the mocker (and/or C{expect()} as a helper) to define + additional behavior for each event. For instance: + + mock = mocker.mock() + mock.hello() + mocker.result(10) + mocker.replay() + assert mock.hello() == 10 + mock.restore() + mock.verify() + + In this short excerpt a mock object is being created, then an + expectation of a call to the C{hello()} method was recorded, and + when that happens the method should return the value C{10}. Then, + the mocker is put in replay mode, and the expectation is satisfied + by calling the C{hello()} method, which indeed returns 10. Finally, + a call to the L{restore()} method is performed to undo any needed + changes made in the environment, and the L{verify()} method is + called to ensure that all defined expectations were met. + + The same logic can be expressed more elegantly using the + C{with mocker:} statement, as follows:: + + mock = mocker.mock() + mock.hello() + mocker.result(10) + with mocker: + assert mock.hello() == 10 + + Also, the MockerTestCase class, which integrates the mocker on + a unittest.TestCase subclass, may be used to reduce the overhead + of controlling the mocker. A test could be written as follows:: + + class SampleTest(MockerTestCase): + + def test_hello(self): + mock = self.mocker.mock() + mock.hello() + self.mocker.result(10) + self.mocker.replay() + assert mock.hello() == 10 + """ _recorders = [] @@ -92,39 +146,105 @@ class MockerBase(object): self._ordering = False self._last_orderer = None - def get_state(self): - return self._state - def _set_state(self, state): if state is not self._state: self._state = state for event in self._events: event.set_state(state) + def get_state(self): + """Return the current state (RECORD, REPLAY or RESTORE). + + The initial state is RECORD. + """ + return self._state + def replay(self): + """Change state to expect recorded events to be reproduced. + + An alternative and more comfortable way to replay changes is + using the 'with' statement, as follows:: + + mocker = Mocker() + <record events> + with mocker: + <reproduce events> + + The 'with' statement will automatically put mocker in replay + mode, and will also verify if all events were correctly reproduced + at the end (using L{verify()}), and also restore any changes done + in the environment (with L{restore()}). + + Also check the MockerTestCase class, which integrates the + unittest.TestCase class with mocker. + """ self._set_state(REPLAY) def record(self): + """Put the mocker on recording mode, where expectations are defined. + + That's the initial state when the mocker is constructed. + """ self._set_state(RECORD) def restore(self): + """Restore all changes done in the environment. + + This should always be called after the test is complete (succeeding + or not). There are ways to call this method automatically on + completion (e.g. using a with mocker: statement, or using the + MockerTestCase class. + """ self._set_state(RESTORE) def is_ordering(self): + """Return true if all events are being ordered. + + See the L{ordered()} method. + """ return self._ordering def ordered(self): + """Expect following events to be reproduced in the recorded order. + + By default, mocker won't force events to happen precisely in + the order they were recorded. Calling this method will change + this behavior so that events will only match if reproduced in + the correct order. + + Running the L{unordered()} method will put the mocker back on + unordered mode. + + This method may also be used with the 'with' statement, like so:: + + with mocker.ordered(): + <record events> + + In this case, only expressions in <record events> will be ordered, + and the mocker will be back in unordered mode after the 'with' block. + """ self._ordering = True return OrderedContext(self) def unordered(self): + """Don't expect following events to be reproduce in the recorded order. + + This will undo the effect of the L{ordered()} method, putting the + mocker back in the default unordered mode. + """ self._ordering = False self._last_orderer = None def get_events(self): + """Return all recorded events.""" return self._events[:] def add_event(self, event): + """Add an event. + + This method is used internally by the implementation, and + shouldn't be needed on normal mocker usage. + """ self._events.append(event) if self._ordering: orderer = event.add_task(Orderer()) @@ -134,34 +254,178 @@ class MockerBase(object): return event def verify(self): - for event in self._events: - event.verify() + """Check if all expectations were met, and raise AssertionError if not. - def obj(self, spec=None): - """Return a new mock object.""" - return Mock(self, spec=spec) + The exception message will include a nice description of which + expectations were not met, and why. + """ + errors = [] + for event in self._events: + try: + event.verify() + except AssertionError, e: + error = str(e) + if not error: + raise RuntimeError("Empty error message from %r" + % event) + errors.append(error) + if errors: + message = [ERROR_PREFIX + "Unmet expectations:", ""] + for error in errors: + lines = error.splitlines() + message.append("=> " + lines.pop(0)) + message.extend(" " + line for line in lines) + message.append("") + raise AssertionError(os.linesep.join(message)) + + def mock(self, spec_and_type=None, spec=None, type=None, name=None): + """Return a new mock object. + + @param spec_and_type: Handy positional argument which sets both + spec and type. + @param spec: Method calls will be checked for correctness against + the given class. + @param type: If set, the Mock's __class__ attribute will return + the given type. This will make C{isinstance()} calls + on the object work. + @param name: Name for the mock object, used in the representation of + expressions. The name is rarely needed, as it's usually + guessed correctly from the variable name used. + """ + if spec_and_type is not None: + spec = type = spec_and_type + return Mock(self, spec=spec, type=type, name=name) - def proxy(self, object, spec=None, name=None, - passthrough=True, install=False): + def proxy(self, object, spec=True, type=True, name=None, passthrough=True): """Return a new mock object which proxies to the given object. - Unknown expressions are passed through to the proxied object - by default, unless passthrough is set to False. When set to - False, pass through only happens when explicitly requested. + Proxies are useful when only part of the behavior of an object + is to be mocked. Unknown expressions may be passed through to + the real implementation implicitly (if the C{passthrough} argument + is True), or explicitly (using the L{passthrough()} method + on the event). + + @param object: Real object to be proxied. + @param spec: Method calls will be checked for correctness against + the given object, which may be a class or an instance + where attributes will be looked up. Defaults to the + the C{object} parameter. May be set to None explicitly, + in which case spec checking is disabled. Checks may + also be disabled explicitly on a per-event basis with + the L{nospec()} method. + @param type: If set, the Mock's __class__ attribute will return + the given type. This will make C{isinstance()} calls + on the object work. Defaults to the type of the + C{object} parameter. May be set to None explicitly. + @param name: Name for the mock object, used in the representation of + expressions. The name is rarely needed, as it's usually + guessed correctly from the variable name used. + @param passthrough: If set to False, passthrough of actions on the + proxy to the real object will only happen when + explicitly requested via the L{passthrough()} + method. """ - mock = Mock(self, spec=spec, object=object, + if spec is True: + spec = object + if type is True: + type = __builtin__.type(object) + return Mock(self, spec=spec, type=type, object=object, name=name, passthrough=passthrough) - if install: - event = self.add_event(Event()) - event.add_task(ProxyInstaller(mock)) + + def replace(self, object, spec=True, type=True, name=None, + passthrough=True): + """Create a proxy, and replace the original object with the mock. + + On replay, the original object will be replaced by the returned + proxy in all dictionaries found in the running interpreter via + the garbage collecting system. This should cover module + namespaces, class namespaces, instance namespaces, and so on. + + @param object: Real object to be proxied, and replaced by the mock + on replay mode. + @param spec: Method calls will be checked for correctness against + the given object, which may be a class or an instance + where attributes will be looked up. Defaults to the + the C{object} parameter. May be set to None explicitly, + in which case spec checking is disabled. Checks may + also be disabled explicitly on a per-event basis with + the L{nospec()} method. + @param type: If set, the Mock's __class__ attribute will return + the given type. This will make C{isinstance()} calls + on the object work. Defaults to the type of the + C{object} parameter. May be set to None explicitly. + @param name: Name for the mock object, used in the representation of + expressions. The name is rarely needed, as it's usually + guessed correctly from the variable name used. + @param passthrough: If set to False, passthrough of actions on the + proxy to the real object will only happen when + explicitly requested via the L{passthrough()} + method. + """ + if isinstance(object, basestring): + if name is None: + name = object + import_stack = object.split(".") + attr_stack = [] + while import_stack: + module_path = ".".join(import_stack) + try: + object = __import__(module_path, {}, {}, [""]) + except ImportError: + attr_stack.insert(0, import_stack.pop()) + continue + else: + for attr in attr_stack: + object = getattr(object, attr) + break + mock = self.proxy(object, spec, type, name, passthrough) + event = self.add_event(Event()) + event.add_task(ProxyReplacer(mock)) return mock - def module(self, name, passthrough=True): - return self.proxy(__import__(name, {}, {}, [""]), - name=name, passthrough=passthrough, install=True) + def patch(self, object): + """Patch an existing object to reproduce recorded events. + + @param object: Class or instance to be patched. + + The result of this method is still a mock object, which can be + used like any other mock object to record events. The difference + is that when the mocker is put on replay mode, the *real* object + will be modified to behave according to recorded expectations. + + Patching works in individual instances, and also in classes. + When an instance is patched, recorded events will only be + considered on this specific instance, and other instances should + behave normally. When a class is patched, the reproduction of + events will be considered on any instance of this class once + created (collectively). + + Observe that, unlike with proxies which catch only events done + through the mock object, *all* accesses to recorded expectations + will be considered; even these coming from the object itself + (e.g. C{self.hello()} is considered if this method was patched). + While this is a very powerful feature, and many times the reason + to use patches in the first place, it's important to keep this + behavior in mind. + + Patching of the original object only takes place when the mocker + is put on replay mode, and the patched object will be restored + to its original state once the L{restore()} method is called + (explicitly, or implicitly with alternative conventions, such as + a C{with mocker:} block, or a MockerTestCase class). + """ + patcher = Patcher() + event = self.add_event(Event()) + event.add_task(patcher) + mock = Mock(self, object=object, patcher=patcher, passthrough=True) + object.__mocker_mock__ = mock + return mock def act(self, path): """This is called by mock objects whenever something happens to them. + + This method is part of the implementation between the mocker + and mock objects. """ if self._state is RECORD: event = self.add_event(Event(path)) @@ -172,48 +436,83 @@ class MockerBase(object): for event in sorted(self._events, key=lambda e:e.satisfied()): if event.matches(path): return event.run(path) - raise UnexpectedExprError("[Mocker] Unexpected expression: %s" - % path) + raise MatchError(ERROR_PREFIX + "Unexpected expression: %s" % path) # XXX Show the full state here. else: - raise RuntimeError("Mocker isn't recording or replaying.") + raise RuntimeError(ERROR_PREFIX + "Not in record or playback mode") @classinstancemethod def get_recorders(cls, self): + """Return recorders associated with this mocker class or instance. + + This method may be called on mocker instances and also on mocker + classes. See the L{add_recorder()} method for more information. + """ return (self or cls)._recorders[:] @classinstancemethod def add_recorder(cls, self, recorder): + """Add a recorder to this mocker class or instance. + + @param recorder: Callable accepting C{(mocker, event)} as parameters. + + This is part of the implementation of mocker. + + All registered recorders are called for translating events that + happen during recording into expectations to be met once the state + is switched to replay mode. + + This method may be called on mocker instances and also on mocker + classes. When called on a class, the recorder will be used by + all instances, and also inherited on subclassing. When called on + instances, the recorder is added only to the given instance. + """ (self or cls)._recorders.append(recorder) return recorder @classinstancemethod def remove_recorder(cls, self, recorder): + """Remove the given recorder from this mocker class or instance. + + This method may be called on mocker classes and also on mocker + instances. See the L{add_recorder()} method for more information. + """ (self or cls)._recorders.remove(recorder) def result(self, value): - """Last recorded event will return the given value.""" + """Make the last recorded event return the given value on replay. + + @param value: Object to be returned when the event is replayed. + """ self.call(lambda *args, **kwargs: value) def throw(self, exception): - """Last recorded event will raise the given exception.""" + """Make the last recorded event raise the given exception on replay. + + @param exception: Class or instance of exception to be raised. + """ def raise_exception(*args, **kwargs): raise exception self.call(raise_exception) def call(self, func): - """Last recorded event will cause the given function to be called. + """Make the last recorded event cause the given function to be called. + + @param func: Function to be called. The result of the function will be used as the event result. """ self._events[-1].add_task(FunctionRunner(func)) def count(self, min, max=False): - """Last recorded event must happen between min and max times. - - If the max argument isn't given, it's assumed to be the same - as min. If max is None, the event must just happen at least - min times. + """Last recorded event must be replayed between min and max times. + + @param min: Minimum number of times that the event must happen. + @param max: Maximum number of times that the event must happen. If + not given, it defaults to the same value of the C{min} + parameter. If set to None, there is no upper limit, and + the expectation is met as long as it happens at least + C{min} times. """ event = self._events[-1] for task in event.get_tasks(): @@ -224,8 +523,20 @@ class MockerBase(object): def order(self, *path_holders): """Ensure that events referred to by given objects happen in order. - Arguments passed to this method might be mock objects returned - from recorded operations. + @param *path_holders: Objects returned as the result of recorded + events. + + As an example:: + + mock = mocker.mock() + expr1 = mock.hello() + expr2 = mock.world + expr3 = mock.x.y.z + mocker.order(expr1, expr2, expr3) + + This method of ordering only works when the expression returns + another object. For other methods of ordering check the + L{ordered()}, L{after()}, and L{before()} methods. """ last_orderer = None for path_holder in path_holders: @@ -250,8 +561,10 @@ class MockerBase(object): def after(self, *path_holders): """Last recorded event must happen after events referred to. - Arguments passed to this method might be mock objects returned - from recorded operations. + @param *path_holders: Objects returned as the result of recorded + events. + + See L{order()} for more information. """ last_path = self._events[-1].path for path_holder in path_holders: @@ -260,25 +573,49 @@ class MockerBase(object): def before(self, *path_holders): """Last recorded event must happen before events referred to. - Arguments passed to this method might be mock objects returned - from recorded operations. + @param *path_holders: Objects returned as the result of recorded + events. + + See L{order()} for more information. """ last_path = self._events[-1].path for path_holder in path_holders: self.order(last_path, path_holder) def nospec(self): - """Don't check method specification of real class on last event.""" + """Don't check method specification of real object on last event.""" event = self._events[-1] for task in event.get_tasks(): if isinstance(task, SpecChecker): event.remove_task(task) def passthrough(self): + """Make the last recorder event act on the real object once seen. + + This can only be used on proxies, as returned by the L{proxy()} + and L{replace()} methods, or on patched objects (L{patch()}). + """ event = self._events[-1] - if not event.path.root_mock.__mocker_object__: + if event.path.root_object is None: raise TypeError("Mock object isn't a proxy") - event.add_task(PathApplier()) + event.add_task(PathExecuter()) + + def __enter__(self): + """Enter in a 'with' context. This will run replay().""" + self.replay() + return self + + def __exit__(self, type, value, traceback): + """Exit from a 'with' context. + + This will run restore() at all times, but will only run verify() + if the 'with' block itself hasn't raised an exception. Exceptions + in that block are never swallowed. + """ + self.restore() + if type is None: + self.verify() + return False class OrderedContext(object): @@ -294,7 +631,7 @@ class OrderedContext(object): class Mocker(MockerBase): - pass + __doc__ = MockerBase.__doc__ # Decorator to add recorders on the standard Mocker class. recorder = Mocker.add_recorder @@ -305,36 +642,53 @@ recorder = Mocker.add_recorder class Mock(object): - def __init__(self, mocker, path=None, name=None, spec=None, - object=None, passthrough=False): + def __init__(self, mocker, path=None, name=None, spec=None, type=None, + object=None, passthrough=False, patcher=None): self.__mocker__ = mocker - self.__mocker_path__ = path or Path(self) + self.__mocker_path__ = path or Path(self, object) self.__mocker_name__ = name self.__mocker_spec__ = spec self.__mocker_object__ = object self.__mocker_passthrough__ = passthrough + self.__mocker_patcher__ = patcher + self.__mocker_replace__ = False + self.__mocker_type__ = type - def __mocker_act__(self, kind, *args, **kwargs): + def __mocker_act__(self, kind, args=(), kwargs={}, object=None): if self.__mocker_name__ is None: self.__mocker_name__ = find_object_name(self, 2) - action = Action(self.__mocker_path__, kind, args, kwargs) + action = Action(kind, args, kwargs, self.__mocker_path__) path = self.__mocker_path__ + action + if object is not None: + path.root_object = object try: return self.__mocker__.act(path) - except UnexpectedExprError: + except MatchError, exception: + if (self.__mocker_type__ is not None and + kind == "getattr" and args == ("__class__",)): + return self.__mocker_type__ root_mock = path.root_mock - if (root_mock.__mocker_passthrough__ and - root_mock.__mocker_object__ is not None): - return path.apply(root_mock.__mocker_object__) - raise + if (path.root_object is not None and + root_mock.__mocker_passthrough__): + return path.execute(path.root_object) + # Reinstantiate to show raise statement on traceback, and + # also to make it shorter. + raise MatchError(str(exception)) + except AssertionError, e: + lines = str(e).splitlines() + message = [ERROR_PREFIX + "Unmet expectation:", ""] + message.append("=> " + lines.pop(0)) + message.extend(" " + line for line in lines) + message.append("") + raise AssertionError(os.linesep.join(message)) def __getattribute__(self, name): if name.startswith("__mocker_"): return super(Mock, self).__getattribute__(name) - return self.__mocker_act__("getattr", name) + return self.__mocker_act__("getattr", (name,)) def __call__(self, *args, **kwargs): - return self.__mocker_act__("call", *args, **kwargs) + return self.__mocker_act__("call", args, kwargs) def find_object_name(obj, depth=0): @@ -364,35 +718,62 @@ def find_object_name(obj, depth=0): class Action(object): - def __init__(self, path, kind, args, kwargs): - self.path = path + def __init__(self, kind, args, kwargs, path=None): self.kind = kind self.args = args self.kwargs = kwargs - self._apply_cache = {} + self.path = path + self._execute_cache = {} - def apply(self, object): + def __repr__(self): + if self.path is None: + return "Action(%r, %r, %r)" % (self.kind, self.args, self.kwargs) + return "Action(%r, %r, %r, %r)" % \ + (self.kind, self.args, self.kwargs, self.path) + + def __eq__(self, other): + return (self.kind == other.kind and + self.args == other.args and + self.kwargs == other.kwargs) + + def __ne__(self, other): + return not self.__eq__(other) + + def matches(self, other): + return (self.kind == other.kind and + match_params(self.args, self.kwargs, other.args, other.kwargs)) + + def execute(self, object): # This caching scheme may fail if the object gets deallocated before # the action, as the id might get reused. It's somewhat easy to fix # that with a weakref callback. For our uses, though, the object # should never get deallocated before the action itself, so we'll # just keep it simple. - if id(object) in self._apply_cache: - return self._apply_cache[id(object)] - kind = self.kind - if kind == "getattr": - kind = "getattribute" - method = getattr(object, "__%s__" % kind) - result = method(*self.args, **self.kwargs) - self._apply_cache[id(object)] = result + if id(object) in self._execute_cache: + return self._execute_cache[id(object)] + execute = getattr(object, "__mocker_execute__", None) + if execute is not None: + result = execute(self, object) + else: + kind = self.kind + if kind == "getattr": + result = getattr(object, self.args[0]) + elif kind == "call": + result = object(*self.args, **self.kwargs) + else: + raise RuntimeError("Don't know how to apply %r kind" + % self.kind) + self._execute_cache[id(object)] = result return result class Path(object): - def __init__(self, root_mock, actions=()): + def __init__(self, root_mock, root_object=None, actions=()): self.root_mock = root_mock + self.root_object = root_object self.actions = tuple(actions) + self.__mocker_replace__ = False @property def parent_path(self): @@ -402,7 +783,8 @@ class Path(object): def __add__(self, action): """Return a new path which includes the given action at the end.""" - return self.__class__(self.root_mock, self.actions + (action,)) + return self.__class__(self.root_mock, self.root_object, + self.actions + (action,)) def __eq__(self, other): """Verify if the two paths are equal. @@ -411,12 +793,11 @@ class Path(object): have the actions with equal kind, args and kwargs. """ if (self.root_mock is not other.root_mock or + self.root_object is not other.root_object or len(self.actions) != len(other.actions)): return False for action, other_action in zip(self.actions, other.actions): - if (action.kind != other_action.kind or - action.args != other_action.args or - action.kwargs != other_action.kwargs): + if action != other_action: return False return True @@ -430,17 +811,15 @@ class Path(object): len(self.actions) != len(other.actions)): return False for action, other_action in zip(self.actions, other.actions): - if (action.kind != other_action.kind or - not match_params(action.args, action.kwargs, - other_action.args, other_action.kwargs)): + if not action.matches(other_action): return False return True - def apply(self, object): - """Apply all actions sequentially on object, and return result. + def execute(self, object): + """Execute all actions sequentially on object, and return result. """ for action in self.actions: - object = action.apply(object) + object = action.execute(object) return object def __str__(self): @@ -461,66 +840,114 @@ class Path(object): class SpecialArgument(object): - """Markup base for special arguments for matching parameters.""" + """Base for special arguments for matching parameters.""" + def __init__(self, object=None): + self.object = object -class ANY(SpecialArgument): - def __eq__(self, other): - return not isinstance(other, SpecialArgument) or self is other def __repr__(self): - return "ANY" + if self.object is None: + return self.__class__.__name__ + else: + return "%s(%r)" % (self.__class__.__name__, self.object) + + def matches(self, other): + return True + + def __eq__(self, other): + return type(other) == type(self) and self.object == other.object + + +class ANY(SpecialArgument): + """Matches any single argument.""" + ANY = ANY() class VARIOUS(SpecialArgument): - def __repr__(self): - return "VARIOUS" - def __eq__(self, other): - return self is other + """Matches zero or more arguments.""" + VARIOUS = VARIOUS() +class ARGS(SpecialArgument): + """Matches zero or more positional arguments.""" + +ARGS = ARGS() + + +class KWARGS(SpecialArgument): + """Matches zero or more keyword arguments.""" + +KWARGS = KWARGS() + + class SAME(SpecialArgument): - def __init__(self, object): - self.object = object - def __repr__(self): - return "SAME(%r)" % (self.object,) - def __eq__(self, other): - if isinstance(other, SpecialArgument): - return type(other) == type(self) and self.object is other.object + + def matches(self, other): return self.object is other + def __eq__(self, other): + return type(other) == type(self) and self.object is other.object + class CONTAINS(SpecialArgument): - def __init__(self, object): - self.object = object - def __repr__(self): - return "CONTAINS(%r)" % (self.object,) - def __eq__(self, other): - if isinstance(other, SpecialArgument): - return type(other) == type(self) and self.object == other.object + + def matches(self, other): + try: + other.__contains__ + except AttributeError: + try: + iter(other) + except TypeError: + # If an object can't be iterated, and has no __contains__ + # hook, it'd blow up on the test below. We test this in + # advance to prevent catching more errors than we really + # want. + return False return self.object in other def match_params(args1, kwargs1, args2, kwargs2): """Match the two sets of parameters, considering the special VARIOUS.""" - # If they are equal, we're done. - if args1 == args2 and kwargs1 == kwargs2: - return True + has_args = ARGS in args1 + has_kwargs = KWARGS in args1 + + if has_kwargs: + args1 = [arg1 for arg1 in args1 if arg1 is not KWARGS] + elif len(kwargs1) != len(kwargs2): + return False - # Then, only if we have a VARIOUS argument we have a chance of matching. - if VARIOUS not in args1: + if not has_args and len(args1) != len(args2): return False - # Any keyword requests should be honored, but unrequested keywords - # are also accepted, since the user is fine with whatever (VARIOUS!). - for key, value in kwargs1.iteritems(): - if kwargs2.get(key) != value: + # Either we have the same number of kwargs, or unknown keywords are + # accepted (KWARGS was used), so check just the ones in kwargs1. + for key, arg1 in kwargs1.iteritems(): + if key not in kwargs2: return False + arg2 = kwargs2[key] + if isinstance(arg1, SpecialArgument): + if not arg1.matches(arg2): + return False + elif arg1 != arg2: + return False + + # Keywords match. Now either we have the same number of + # arguments, or ARGS was used. If ARGS wasn't used, arguments + # must match one-on-one necessarily. + if not has_args: + for arg1, arg2 in zip(args1, args2): + if isinstance(arg1, SpecialArgument): + if not arg1.matches(arg2): + return False + elif arg1 != arg2: + return False + return True - # Easy choice. Keywords are matching, and anything on args are accepted. - if (VARIOUS,) == args1: + # Easy choice. Keywords are matching, and anything on args is accepted. + if (ARGS,) == args1: return True # We have something different there. If we don't have positional @@ -528,7 +955,7 @@ def match_params(args1, kwargs1, args2, kwargs2): if not args2: # Unless we have just several VARIOUS (which is bizarre, but..). for arg1 in args1: - if arg1 is not VARIOUS: + if arg1 is not ARGS: return False return True @@ -536,14 +963,14 @@ def match_params(args1, kwargs1, args2, kwargs2): # matching. This is an algorithm based on the idea of the Levenshtein # Distance between two strings, but heavily hacked for this purpose. args2l = len(args2) - if args1[0] is VARIOUS: + if args1[0] is ARGS: args1 = args1[1:] array = [0]*args2l else: array = [1]*args2l for i in range(len(args1)): last = array[0] - if args1[i] is VARIOUS: + if args1[i] is ARGS: for j in range(1, args2l): last, array[j] = array[j], min(array[j-1], array[j], last) else: @@ -604,10 +1031,25 @@ class Event(object): which isn't None, or None if they're all None. """ result = None + errors = [] for task in self._tasks: - task_result = task.run(path) - if task_result is not None: - result = task_result + try: + task_result = task.run(path) + except AssertionError, e: + error = str(e) + if not error: + raise RuntimeError("Empty error message from %r" % task) + errors.append(error) + else: + if task_result is not None: + result = task_result + if errors: + message = [str(self.path)] + for error in errors: + lines = error.splitlines() + message.append("- " + lines.pop(0)) + message.extend(" " + line for line in lines) + raise AssertionError(os.linesep.join(message)) return result def satisfied(self): @@ -615,21 +1057,37 @@ class Event(object): Being satisfied means that there are no unmet expectations. """ - try: - self.verify() - except AssertionError: - return False + for task in self._tasks: + try: + task.verify() + except AssertionError: + return False return True def verify(self): """Run verify on all tasks. The verify method is supposed to raise an AssertionError if the - task has unmet expectations, with a nice debugging message - explaining why it wasn't met. + task has unmet expectations, with a one-line explanation about + why this item is unmet. This method should be safe to be called + multiple times without side effects. """ + errors = [] for task in self._tasks: - task.verify() + try: + task.verify() + except AssertionError, e: + error = str(e) + if not error: + raise RuntimeError("Empty error message from %r" % task) + errors.append(error) + if errors: + message = [str(self.path)] + for error in errors: + lines = error.splitlines() + message.append("- " + lines.pop(0)) + message.extend(" " + line for line in lines) + raise AssertionError(os.linesep.join(message)) def set_state(self, state): """Change the task state of all tasks to reflect that of the mocker. @@ -660,8 +1118,10 @@ class Task(object): def verify(self): """Raise AssertionError if expectations for this item are unmet. - The exception should include a nice explanation about why this - item is unmet. + The verify method is supposed to raise an AssertionError if the + task has unmet expectations, with a one-line explanation about + why this item is unmet. This method should be safe to be called + multiple times without side effects. """ def set_state(self, state): @@ -784,11 +1244,11 @@ class FunctionRunner(Task): return self._func(*action.args, **action.kwargs) -class PathApplier(Task): - """Task that applies a path in the real object, and returns the result.""" +class PathExecuter(Task): + """Task that executes a path in the real object, and returns the result.""" def run(self, path): - return path.apply(path.root_mock.__mocker_object__) + return path.execute(path.root_object) class Orderer(Task): @@ -838,25 +1298,30 @@ class SpecChecker(Task): def get_method(self): return self._method + def _raise(self, message): + spec = inspect.formatargspec(self._args, self._varargs, + self._varkwargs, self._defaults) + raise AssertionError("Specification is %s%s: %s" % + (self._method.__name__, spec, message)) + def run(self, path): if not self._method: - raise AssertionError("method not existent in real class") + raise AssertionError("Method not found in real specification") action = path.actions[-1] obtained_len = len(action.args) obtained_kwargs = action.kwargs.copy() nodefaults_len = len(self._args) - len(self._defaults) for i, name in enumerate(self._args): if i < obtained_len and name in action.kwargs: - raise AssertionError("%r parameter provided twice" % name) + self._raise("%r provided twice" % name) if (i >= obtained_len and i < nodefaults_len and name not in action.kwargs): - raise AssertionError("%r parameter not provided" % name) + self._raise("%r not provided" % name) obtained_kwargs.pop(name, None) if obtained_len > len(self._args) and not self._varargs: - raise AssertionError("maximum number of parameters exceeded") + self._raise("too many args provided") if obtained_kwargs and not self._varkwargs: - raise AssertionError("unknown kwargs: %s" % - ", ".join(obtained_kwargs)) + self._raise("unknown kwargs: %s" % ", ".join(obtained_kwargs)) @recorder def spec_checker_recorder(mocker, event): @@ -868,7 +1333,7 @@ def spec_checker_recorder(mocker, event): event.add_task(SpecChecker(method)) -class ProxyInstaller(Task): +class ProxyReplacer(Task): """Task which installs and deinstalls proxy mocks. This task will replace a real object by a mock in all dictionaries @@ -877,19 +1342,147 @@ class ProxyInstaller(Task): def __init__(self, mock): self.mock = mock + self.__mocker_replace__ = False + + def matches(self, path): + return False + + def set_state(self, state): + if state is REPLAY: + global_replace(self.mock.__mocker_object__, self.mock) + else: + global_replace(self.mock, self.mock.__mocker_object__) + + +def global_replace(remove, install): + """Replace object 'remove' with object 'install' on all dictionaries.""" + for referrer in gc.get_referrers(remove): + if (type(referrer) is dict and + referrer.get("__mocker_replace__", True)): + for key, value in referrer.iteritems(): + if value is remove: + referrer[key] = install + + +class Undefined(object): + + def __repr__(self): + return "Undefined" + +Undefined = Undefined() + + +class Patcher(Task): + + def __init__(self): + super(Patcher, self).__init__() + self._monitored = {} # {kind: {id(object): object}} + self._patched = {} def matches(self, path): return False + def is_monitoring(self, obj, kind): + monitored = self._monitored.get(kind) + if monitored: + if id(obj) in monitored: + return True + cls = type(obj) + if issubclass(cls, type): + cls = obj + bases = set(id(base) for base in cls.__mro__) + bases.intersection_update(monitored) + return bool(bases) + return False + + def monitor(self, obj, kind): + if kind not in self._monitored: + self._monitored[kind] = {} + self._monitored[kind][id(obj)] = obj + + def patch_attr(self, obj, attr, value): + original = obj.__dict__.get(attr, Undefined) + self._patched[id(obj), attr] = obj, attr, original + setattr(obj, attr, value) + + def get_unpatched_attr(self, obj, attr): + cls = type(obj) + if issubclass(cls, type): + cls = obj + result = Undefined + for mro_cls in cls.__mro__: + key = (id(mro_cls), attr) + if key in self._patched: + result = self._patched[key][2] + if result is not Undefined: + break + elif attr in mro_cls.__dict__: + result = mro_cls.__dict__.get(attr, Undefined) + break + if isinstance(result, object) and hasattr(type(result), "__get__"): + if cls is obj: + obj = None + return result.__get__(obj, cls) + return result + + def _get_kind_attr(self, kind): + if kind == "getattr": + return "__getattribute__" + return "__%s__" % kind + def set_state(self, state): if state is REPLAY: - install, remove = self.mock, self.mock.__mocker_object__ + for kind in self._monitored: + attr = self._get_kind_attr(kind) + seen = set() + for obj in self._monitored[kind].itervalues(): + cls = type(obj) + if issubclass(cls, type): + cls = obj + if cls not in seen: + seen.add(cls) + unpatched = getattr(cls, attr, Undefined) + self.patch_attr(cls, attr, + PatchedMethod(kind, unpatched, + self.is_monitoring)) + self.patch_attr(cls, "__mocker_execute__", + self.execute) else: - install, remove = self.mock.__mocker_object__, self.mock - mock_dict = object.__getattribute__(self.mock, "__dict__") - protected = set((id(self.__dict__), id(mock_dict))) - for referrer in gc.get_referrers(remove): - if id(referrer) not in protected and type(referrer) is dict: - for key, value in referrer.iteritems(): - if value is remove: - referrer[key] = install + for obj, attr, original in self._patched.itervalues(): + if original is Undefined: + delattr(obj, attr) + else: + setattr(obj, attr, original) + self._patched.clear() + + def execute(self, action, object): + attr = self._get_kind_attr(action.kind) + unpatched = self.get_unpatched_attr(object, attr) + return unpatched(*action.args, **action.kwargs) + + +class PatchedMethod(object): + + def __init__(self, kind, unpatched, is_monitoring): + self._kind = kind + self._unpatched = unpatched + self._is_monitoring = is_monitoring + + def __get__(self, obj, cls=None): + object = obj or cls + if not self._is_monitoring(object, self._kind): + return self._unpatched.__get__(obj, cls) + def method(*args, **kwargs): + if self._kind == "getattr" and args[0].startswith("__mocker_"): + return self._unpatched.__get__(obj, cls)(args[0]) + mock = object.__mocker_mock__ + return mock.__mocker_act__(self._kind, args, kwargs, object) + return method + + +@recorder +def patcher_recorder(mocker, event): + mock = event.path.root_mock + if mock.__mocker_patcher__ and len(event.path.actions) == 1: + patcher = mock.__mocker_patcher__ + patcher.monitor(mock.__mocker_object__, event.path.actions[0].kind) @@ -1,6 +1,7 @@ #!/usr/bin/python import unittest import sys +import os import gc from types import ModuleType @@ -11,7 +12,8 @@ from mocker import ( run_counter_recorder, run_counter_removal_recorder, MockReturner, mock_returner_recorder, FunctionRunner, Orderer, SpecChecker, spec_checker_recorder, match_params, ANY, VARIOUS, SAME, CONTAINS, - UnexpectedExprError, PathApplier, RECORD, REPLAY, RESTORE, ProxyInstaller) + ARGS, KWARGS, MatchError, PathExecuter, RECORD, REPLAY, RESTORE, + ProxyReplacer, Patcher, Undefined, PatchedMethod) class CleanMocker(MockerBase): @@ -24,7 +26,7 @@ class IntegrationTest(unittest.TestCase): self.mocker = Mocker() def test_count(self): - obj = self.mocker.obj() + obj = self.mocker.mock() obj.x self.mocker.count(2, 3) @@ -38,7 +40,7 @@ class IntegrationTest(unittest.TestCase): self.assertRaises(AssertionError, getattr, obj, "x") def test_ordered(self): - obj = self.mocker.obj() + obj = self.mocker.mock() with_manager = self.mocker.ordered() with_manager.__enter__() @@ -56,11 +58,11 @@ class IntegrationTest(unittest.TestCase): obj.y obj.z - def test_spec(self): + def test_spec_and_type(self): class C(object): def m(self, a): pass - obj = self.mocker.obj(C) + obj = self.mocker.mock(C) obj.m(1) obj.m(a=1) @@ -73,6 +75,8 @@ class IntegrationTest(unittest.TestCase): self.mocker.replay() + self.assertTrue(isinstance(obj, C)) + obj.m(1) obj.m(a=1) obj.y() @@ -82,14 +86,14 @@ class IntegrationTest(unittest.TestCase): self.assertRaises(AssertionError, obj.z) def test_result(self): - obj = self.mocker.obj() + obj = self.mocker.mock() obj.x self.mocker.result(42) self.mocker.replay() self.assertEquals(obj.x, 42) def test_throw(self): - obj = self.mocker.obj() + obj = self.mocker.mock() obj.x() self.mocker.throw(ValueError) self.mocker.replay() @@ -100,7 +104,7 @@ class IntegrationTest(unittest.TestCase): def func(arg): calls.append(arg) return 42 - obj = self.mocker.obj() + obj = self.mocker.mock() obj.x(24) self.mocker.call(func) self.mocker.replay() @@ -112,7 +116,7 @@ class IntegrationTest(unittest.TestCase): def func(arg): calls.append(arg) return arg - obj = self.mocker.obj() + obj = self.mocker.mock() obj.x(24) self.mocker.call(func) self.mocker.result(42) @@ -126,7 +130,7 @@ class IntegrationTest(unittest.TestCase): return sum(args) obj = self.mocker.proxy(C()) - expect(obj.multiply(2, 3)).result(6) + expect(obj.multiply(2, 3)).result(6).nospec() expect(obj.sum(0, 0)).result(1) expect(obj.sum(0, 0)).passthrough() @@ -140,9 +144,9 @@ class IntegrationTest(unittest.TestCase): self.assertEquals(obj.sum(0, 0), 0) # Passed through explicitly. self.assertRaises(AssertionError, obj.sum, 0, 0) # Seen twice. - def test_module_install_and_restore(self): + def test_replace_install_and_restore(self): try: - module = self.mocker.module("calendar") + module = self.mocker.replace("calendar") import calendar self.assertTrue(calendar is not module) self.mocker.replay() @@ -154,11 +158,11 @@ class IntegrationTest(unittest.TestCase): finally: self.mocker.restore() - def test_module_os_path(self): + def test_replace_os_path_join(self): try: - path = self.mocker.module("os.path") - expect(path.join(VARIOUS)).call(lambda *args: "-".join(args)) - expect(path.join("e", VARIOUS)).passthrough() + path = self.mocker.replace("os.path") + expect(path.join(ARGS)).call(lambda *args: "-".join(args)) + expect(path.join("e", ARGS)).passthrough() self.mocker.replay() import os self.assertEquals(os.path.join("a", "b", "c"), "a-b-c") @@ -166,6 +170,18 @@ class IntegrationTest(unittest.TestCase): finally: self.mocker.restore() + def test_replace_os_path_isfile(self): + try: + path = self.mocker.replace("os.path") + expect(path.isfile("unexistent")).result(True) + expect(path.isfile(ANY)).passthrough().count(2) + self.mocker.replay() + import os + self.assertFalse(os.path.isfile("another-unexistent")) + self.assertTrue(os.path.isfile("unexistent")) + self.assertFalse(os.path.isfile("unexistent")) + finally: + self.mocker.restore() class ExpectTest(unittest.TestCase): @@ -173,13 +189,13 @@ class ExpectTest(unittest.TestCase): self.mocker = CleanMocker() def test_calling_mocker(self): - obj = self.mocker.obj() + obj = self.mocker.mock() expect(obj.attr).result(123) self.mocker.replay() self.assertEquals(obj.attr, 123) def test_chaining(self): - obj = self.mocker.obj() + obj = self.mocker.mock() expect(obj.attr).result(123).result(42) self.mocker.replay() self.assertEquals(obj.attr, 42) @@ -194,8 +210,8 @@ class MockerTest(unittest.TestCase): def recorder(mocker, event): self.recorded.append((mocker, event)) - self.action = Action(Path(Mock(self.mocker, name="mock")), - "getattr", ("attr",), {}) + self.action = Action("getattr", ("attr",), {}, + Path(Mock(self.mocker, name="mock"))) self.path = self.action.path + self.action def test_default_state(self): @@ -233,18 +249,77 @@ class MockerTest(unittest.TestCase): self.assertEquals(calls, [RESTORE]) def test_verify(self): - calls = [] class MyEvent(object): - def __init__(self, name): - self.name = name + def __init__(self, id, failed): + self.id = id + self.failed = failed def verify(self): - calls.append(self.name) - self.mocker.add_event(MyEvent("1")) - self.mocker.add_event(MyEvent("2")) + if self.failed: + raise AssertionError("%d failed\n- Line 1\n- Line 2\n" + % self.id) - self.mocker.verify() + self.mocker.add_event(MyEvent(1, True)) + self.mocker.add_event(MyEvent(2, False)) + self.mocker.add_event(MyEvent(3, True)) + + try: + self.mocker.verify() + except AssertionError, e: + message = os.linesep.join(["[Mocker] Unmet expectations:", + "", + "=> 1 failed", + " - Line 1", + " - Line 2", + "", + "=> 3 failed", + " - Line 1", + " - Line 2", + ""]) + self.assertEquals(str(e), message) + else: + self.fail("AssertionError not raised") + + def test_mocker_as_context_manager(self): + calls = [] + throw = False + class MyEvent(Event): + def verify(self): + calls.append("verify") + if throw: + raise AssertionError("Some problem") + def set_state(self, state): + calls.append({REPLAY: "replay", RESTORE: "restore"}[state]) + + event = MyEvent() + self.mocker.add_event(event) + + self.assertEquals(calls, []) + + mocker = self.mocker.__enter__() + self.assertTrue(mocker is self.mocker) + self.assertEquals(calls, ["replay"]) + + # Verify without errors. + del calls[:] + result = self.mocker.__exit__(None, None, None) + self.assertEquals(result, False) + self.assertEquals(calls, ["restore", "verify"]) - self.assertEquals(calls, ["1", "2"]) + throw = True + + # Verify raising an error. + self.mocker.replay() + del calls[:] + self.assertRaises(AssertionError, + self.mocker.__exit__, None, None, None) + self.assertEquals(calls, ["restore", "verify"]) + + # An exception happened in the 'with' block. Verify won't raise. + self.mocker.replay() + del calls[:] + result = self.mocker.__exit__(AssertionError, None, None) + self.assertEquals(result, False) + self.assertEquals(calls, ["restore"]) def test_add_recorder_on_instance(self): obj1 = object() @@ -305,72 +380,136 @@ class MockerTest(unittest.TestCase): MyMocker.remove_recorder(obj1) self.assertEquals(MyMocker.get_recorders(), [obj2]) - def test_obj(self): - self.mocker = CleanMocker() - obj = self.mocker.obj() - self.assertEquals(type(obj), Mock) + def test_mock_with_name(self): + mock = self.mocker.mock(name="name") + self.assertEquals(mock.__mocker_name__, "name") - def test_obj_with_spec(self): + def test_mock_with_spec(self): class C(object): pass - self.mocker = CleanMocker() - obj = self.mocker.obj(C) - self.assertEquals(obj.__mocker_spec__, C) + mock = self.mocker.mock(spec=C) + self.assertEquals(mock.__mocker_spec__, C) + + def test_mock_with_type(self): + class C(object): pass + mock = self.mocker.mock(type=C) + self.assertEquals(mock.__mocker_type__, C) + + def test_mock_with_spec_and_type(self): + class C(object): pass + mock = self.mocker.mock(C) + self.assertEquals(mock.__mocker_spec__, C) + self.assertEquals(mock.__mocker_type__, C) def test_proxy(self): original = object() - self.mocker = CleanMocker() - obj = self.mocker.proxy(original) - self.assertEquals(type(obj), Mock) - self.assertEquals(obj.__mocker_object__, original) + mock = self.mocker.proxy(original) + self.assertEquals(type(mock), Mock) + self.assertEquals(mock.__mocker_object__, original) + self.assertEquals(mock.__mocker_path__.root_object, original) def test_proxy_with_spec(self): original = object() class C(object): pass - self.mocker = CleanMocker() - obj = self.mocker.proxy(original, C) - self.assertEquals(obj.__mocker_object__, original) - self.assertEquals(obj.__mocker_spec__, C) + mock = self.mocker.proxy(original, C) + self.assertEquals(mock.__mocker_object__, original) + self.assertEquals(mock.__mocker_spec__, C) + + def test_proxy_with_type(self): + original = object() + class C(object): pass + mock = self.mocker.proxy(original, type=C) + self.assertEquals(mock.__mocker_type__, C) + + def test_proxy_spec_defaults_to_the_object_itself(self): + original = object() + mock = self.mocker.proxy(original) + self.assertEquals(mock.__mocker_spec__, original) + + def test_proxy_type_defaults_to_the_object_type(self): + original = object() + mock = self.mocker.proxy(original) + self.assertEquals(mock.__mocker_type__, object) + + def test_proxy_with_spec_and_type_none(self): + original = object() + mock = self.mocker.proxy(original, spec=None, type=None) + self.assertEquals(mock.__mocker_spec__, None) + self.assertEquals(mock.__mocker_type__, None) def test_proxy_with_passthrough_false(self): original = object() class C(object): pass - self.mocker = CleanMocker() - obj = self.mocker.proxy(original, C, passthrough=False) - self.assertEquals(obj.__mocker_object__, original) - self.assertEquals(obj.__mocker_spec__, C) - self.assertEquals(obj.__mocker_passthrough__, False) + mock = self.mocker.proxy(original, C, passthrough=False) + self.assertEquals(mock.__mocker_object__, original) + self.assertEquals(mock.__mocker_spec__, C) + self.assertEquals(mock.__mocker_passthrough__, False) - def test_proxy_install(self): + def test_replace(self): from os import path obj = object() - proxy = self.mocker.proxy(obj, install=True) + proxy = self.mocker.replace(obj, spec=object, name="obj", + passthrough=False) self.assertEquals(type(proxy), Mock) self.assertEquals(type(proxy.__mocker_object__), object) self.assertEquals(proxy.__mocker_object__, obj) + self.assertEquals(proxy.__mocker_spec__, object) + self.assertEquals(proxy.__mocker_name__, "obj") (event,) = self.mocker.get_events() (task,) = event.get_tasks() - self.assertEquals(type(task), ProxyInstaller) + self.assertEquals(type(task), ProxyReplacer) self.assertTrue(task.mock is proxy) self.assertTrue(task.mock.__mocker_object__ is obj) self.assertTrue(proxy is not obj) - def test_module(self): + def test_replace_with_submodule_string(self): from os import path - module = self.mocker.module("os.path") + module = self.mocker.replace("os.path") self.assertEquals(type(module), Mock) self.assertEquals(type(module.__mocker_object__), ModuleType) self.assertEquals(module.__mocker_name__, "os.path") self.assertEquals(module.__mocker_object__, path) (event,) = self.mocker.get_events() (task,) = event.get_tasks() - self.assertEquals(type(task), ProxyInstaller) + self.assertEquals(type(task), ProxyReplacer) self.assertTrue(task.mock is module) self.assertTrue(task.mock.__mocker_object__ is path) self.assertTrue(module is not path) - def test_module_with_passthrough_false(self): - module = self.mocker.module("calendar", passthrough=False) - self.assertEquals(module.__mocker_passthrough__, False) + def test_replace_with_module_function_string(self): + mock = self.mocker.replace("os.path.join.func_name") + self.assertEquals(mock.__mocker_object__, "join") + + def test_replace_with_string_and_name(self): + module = self.mocker.replace("os.path", name="mock") + self.assertEquals(module.__mocker_name__, "mock") + + def test_replace_with_type(self): + original = object() + class C(object): pass + mock = self.mocker.replace(original, type=C) + self.assertEquals(mock.__mocker_type__, C) + + def test_replace_spec_defaults_to_the_object_itself(self): + original = object() + mock = self.mocker.replace(original) + self.assertEquals(mock.__mocker_spec__, original) + + def test_replace_type_defaults_to_the_object_type(self): + original = object() + mock = self.mocker.replace(original) + self.assertEquals(mock.__mocker_type__, object) + + def test_replace_with_spec_and_type_none(self): + original = object() + mock = self.mocker.replace(original, spec=None, type=None) + self.assertEquals(mock.__mocker_spec__, None) + self.assertEquals(mock.__mocker_type__, None) + + def test_replace_with_passthrough_false(self): + original = object() + class C(object): pass + mock = self.mocker.replace(original, passthrough=False) + self.assertEquals(mock.__mocker_passthrough__, False) def test_add_and_get_event(self): self.mocker.add_event(41) @@ -378,31 +517,32 @@ class MockerTest(unittest.TestCase): self.assertEquals(self.mocker.get_events(), [41, 42]) def test_recording(self): - obj = self.mocker.obj() + obj = self.mocker.mock() obj.attr() self.assertEquals(len(self.recorded), 2) - action1 = Action(None, "getattr", ("attr",), {}) - action2 = Action(None, "call", (), {}) + action1 = Action("getattr", ("attr",), {}) + action2 = Action("call", (), {}) mocker1, event1 = self.recorded[0] self.assertEquals(mocker1, self.mocker) self.assertEquals(type(event1), Event) - self.assertTrue(event1.path.matches(Path(obj, [action1]))) + self.assertTrue(event1.path.matches(Path(obj, None, [action1]))) mocker2, event2 = self.recorded[1] self.assertEquals(mocker2, self.mocker) self.assertEquals(type(event2), Event) - self.assertTrue(event2.path.matches(Path(obj, [action1, action2]))) + self.assertTrue(event2.path.matches(Path(obj, None, + [action1, action2]))) self.assertEquals(self.mocker.get_events(), [event1, event2]) def test_recording_result_path(self): - obj = self.mocker.obj() + obj = self.mocker.mock() result = obj.attr() - path = Path(obj, [Action(None, "getattr", ("attr",), {}), - Action(None, "call", (), {})]) + path = Path(obj, None, [Action("getattr", ("attr",), {}), + Action("call", (), {})]) self.assertTrue(result.__mocker_path__.matches(path)) def test_replaying_no_events(self): @@ -456,7 +596,7 @@ class MockerTest(unittest.TestCase): def verify(self): if not self.raised: self.raised = True - raise AssertionError() + raise AssertionError("An error") def run(self, path): return "result2" event1 = self.mocker.add_event(Event()) @@ -515,10 +655,10 @@ class MockerTest(unittest.TestCase): self.assertEquals(task.max, 4) def test_order(self): - mock1 = self.mocker.obj() - mock2 = self.mocker.obj() - mock3 = self.mocker.obj() - mock4 = self.mocker.obj() + mock1 = self.mocker.mock() + mock2 = self.mocker.mock() + mock3 = self.mocker.mock() + mock4 = self.mocker.mock() result1 = mock1.attr1(1) result2 = mock2.attr2(2) result3 = mock3.attr3(3) @@ -560,9 +700,9 @@ class MockerTest(unittest.TestCase): self.assertEquals(other_task_, other_task) def test_after(self): - mock1 = self.mocker.obj() - mock2 = self.mocker.obj() - mock3 = self.mocker.obj() + mock1 = self.mocker.mock() + mock2 = self.mocker.mock() + mock3 = self.mocker.mock() result1 = mock1.attr1(1) result2 = mock2.attr2(2) result3 = mock3.attr3(3) @@ -596,9 +736,9 @@ class MockerTest(unittest.TestCase): self.assertEquals(other_task_, other_task) def test_before(self): - mock1 = self.mocker.obj() - mock2 = self.mocker.obj() - mock3 = self.mocker.obj() + mock1 = self.mocker.mock() + mock2 = self.mocker.mock() + mock3 = self.mocker.mock() result1 = mock1.attr1(1) result2 = mock2.attr2(2) result3 = mock3.attr3(3) @@ -638,7 +778,7 @@ class MockerTest(unittest.TestCase): self.mocker.ordered() self.assertEquals(self.mocker.is_ordering(), True) - def test_ordered_enter_exit(self): + def test_ordered_context_manager(self): with_manager = self.mocker.ordered() self.assertEquals(self.mocker.is_ordering(), True) with_manager.__enter__() @@ -652,7 +792,7 @@ class MockerTest(unittest.TestCase): self.assertEquals(self.mocker.is_ordering(), False) def test_ordered_events(self): - mock = self.mocker.obj() + mock = self.mocker.mock() # Ensure that the state is correctly reset between # different ordered blocks. @@ -691,26 +831,39 @@ class MockerTest(unittest.TestCase): self.assertEquals(event2.get_tasks(), [task2, task4]) def test_passthrough(self): - obj = self.mocker.proxy(object()) - event1 = self.mocker.add_event(Event(Path(obj))) - event2 = self.mocker.add_event(Event(Path(obj))) + obj = object() + mock = self.mocker.proxy(obj) + event1 = self.mocker.add_event(Event(Path(mock, obj))) + event2 = self.mocker.add_event(Event(Path(mock, obj))) self.mocker.passthrough() self.assertEquals(event1.get_tasks(), []) (task,) = event2.get_tasks() - self.assertEquals(type(task), PathApplier) + self.assertEquals(type(task), PathExecuter) def test_passthrough_fails_on_unproxied(self): - obj = self.mocker.obj() - event1 = self.mocker.add_event(Event(Path(obj))) - event2 = self.mocker.add_event(Event(Path(obj))) + mock = self.mocker.mock() + event1 = self.mocker.add_event(Event(Path(mock))) + event2 = self.mocker.add_event(Event(Path(mock))) self.assertRaises(TypeError, self.mocker.passthrough) def test_on(self): - obj = self.mocker.obj() + obj = self.mocker.mock() self.mocker.on(obj.attr).result(123) self.mocker.replay() self.assertEquals(obj.attr, 123) + def test_patch(self): + class C(object): pass + mock = self.mocker.patch(C) + self.assertEquals(type(C.__mocker_mock__), Mock) + self.assertTrue(C.__mocker_mock__ is mock) + self.assertTrue(mock.__mocker_object__ is C) + self.assertEquals(type(mock.__mocker_patcher__), Patcher) + self.assertEquals(mock.__mocker_passthrough__, True) + (event,) = self.mocker.get_events() + (task,) = event.get_tasks() + self.assertTrue(task is mock.__mocker_patcher__) + class ActionTest(unittest.TestCase): @@ -720,32 +873,77 @@ class ActionTest(unittest.TestCase): def test_create(self): objects = [object() for i in range(4)] action = Action(*objects) - self.assertEquals(action.path, objects[0]) - self.assertEquals(action.kind, objects[1]) - self.assertEquals(action.args, objects[2]) - self.assertEquals(action.kwargs, objects[3]) + self.assertEquals(action.kind, objects[0]) + self.assertEquals(action.args, objects[1]) + self.assertEquals(action.kwargs, objects[2]) + self.assertEquals(action.path, objects[3]) - def test_apply_getattr(self): + def test_execute_getattr(self): class C(object): pass obj = C() obj.x = C() - action = Action(None, "getattr", ("x",), {}) - self.assertEquals(action.apply(obj), obj.x) + action = Action("getattr", ("x",), {}) + self.assertEquals(action.execute(obj), obj.x) - def test_apply_call(self): + def test_execute_call(self): obj = lambda a, b: a+b - action = Action(None, "call", (1,), {"b": 2}) - self.assertEquals(action.apply(obj), 3) + action = Action("call", (1,), {"b": 2}) + self.assertEquals(action.execute(obj), 3) - def test_apply_caching(self): + def test_execute_caching(self): values = iter(range(10)) obj = lambda: values.next() - action = Action(None, "call", (), {}) - self.assertEquals(action.apply(obj), 0) - self.assertEquals(action.apply(obj), 0) + action = Action("call", (), {}) + self.assertEquals(action.execute(obj), 0) + self.assertEquals(action.execute(obj), 0) obj = lambda: values.next() - self.assertEquals(action.apply(obj), 1) + self.assertEquals(action.execute(obj), 1) + + def test_equals(self): + obj1 = object() + obj2 = object() + + self.assertEquals(Action("kind", (), {}, obj1), + Action("kind", (), {}, obj2)) + self.assertNotEquals(Action("kind", (), {}, obj1), + Action("dnik", (), {}, obj2)) + self.assertNotEquals(Action("kind", (), {}, obj1), + Action("kind", (1,), {}, obj2)) + self.assertNotEquals(Action("kind", (), {}, obj1), + Action("kind", (), {"a": 1}, obj2)) + self.assertNotEquals(Action("kind", (ANY,), {}, obj1), + Action("kind", (1,), {}, obj2)) + self.assertEquals(Action("kind", (CONTAINS(1),), {}, obj1), + Action("kind", (CONTAINS(1),), {}, obj2)) + + def test_matches(self): + obj1 = object() + obj2 = object() + + action1 = Action("kind", (), {}, obj1) + action2 = Action("kind", (), {}, obj2) + self.assertTrue(action1.matches(action2)) + + action1 = Action("kind", (), {}, obj1) + action2 = Action("dnik", (), {}, obj2) + self.assertFalse(action1.matches(action2)) + + action1 = Action("kind", (), {}, obj1) + action2 = Action("kind", (1,), {}, obj2) + self.assertFalse(action1.matches(action2)) + + action1 = Action("kind", (), {}, obj1) + action2 = Action("kind", (), {"a": 1}, obj2) + self.assertFalse(action1.matches(action2)) + + action1 = Action("kind", (ARGS,), {}, obj1) + action2 = Action("kind", (), {}, obj2) + self.assertTrue(action1.matches(action2)) + + action1 = Action("kind", (ARGS,), {"a": 1}, obj1) + action2 = Action("kind", (), {}, obj2) + self.assertFalse(action1.matches(action2)) class PathTest(unittest.TestCase): @@ -757,31 +955,41 @@ class PathTest(unittest.TestCase): pass self.mocker = StubMocker() self.mock = Mock(self.mocker, name="obj") + self.object = object() def test_create(self): mock = object() path = Path(mock) self.assertEquals(path.root_mock, mock) + self.assertEquals(path.root_object, None) self.assertEquals(path.actions, ()) + def test_create_with_object(self): + mock = object() + path = Path(mock, self.object) + self.assertEquals(path.root_mock, mock) + self.assertEquals(path.root_object, self.object) + def test_create_with_actions(self): mock = object() - path = Path(mock, [1,2,3]) + path = Path(mock, self.object, [1,2,3]) self.assertEquals(path.root_mock, mock) + self.assertEquals(path.root_object, self.object) self.assertEquals(path.actions, (1,2,3)) def test_add(self): mock = object() - path = Path(mock, [1,2,3]) + path = Path(mock, self.object, [1,2,3]) result = path + 4 self.assertTrue(result is not path) self.assertEquals(result.root_mock, mock) + self.assertEquals(result.root_object, self.object) self.assertEquals(result.actions, (1,2,3,4)) def test_parent_path(self): path1 = Path(self.mock) - path2 = path1 + Action(path1, "getattr", ("attr",), {}) - path3 = path2 + Action(path2, "getattr", ("attr",), {}) + path2 = path1 + Action("getattr", ("attr",), {}, path1) + path3 = path2 + Action("getattr", ("attr",), {}, path2) self.assertEquals(path1.parent_path, None) self.assertEquals(path2.parent_path, path1) @@ -789,86 +997,97 @@ class PathTest(unittest.TestCase): def test_equals(self): mock = object() + obj = object() obj1 = object() obj2 = object() # Not the *same* mock. - path1 = Path([], []) - path2 = Path([], []) + path1 = Path([], obj, []) + path2 = Path([], obj, []) + self.assertNotEquals(path1, path2) + + # Not the *same* object. + path1 = Path(mock, [], []) + path2 = Path(mock, [], []) self.assertNotEquals(path1, path2) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, [Action(obj2, "kind", (), {})]) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (), {}, obj2)]) self.assertEquals(path1, path2) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, [Action(obj2, "dnik", (), {})]) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, [Action("dnik", (), {}, obj2)]) self.assertNotEquals(path1, path2) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(object(), [Action(obj2, "kind", (), {})]) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(object(), obj, [Action("kind", (), {}, obj2)]) self.assertNotEquals(path1, path2) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, [Action(obj2, "kind", (1,), {})]) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (1,), {}, obj2)]) self.assertNotEquals(path1, path2) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, [Action(obj2, "kind", (), {"a": 1})]) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (), {"a": 1}, obj2)]) self.assertNotEquals(path1, path2) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, []) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, []) self.assertNotEquals(path1, path2) - path1 = Path(mock, [Action(obj1, "kind", (ANY,), {})]) - path2 = Path(mock, [Action(obj2, "kind", (1), {})]) + path1 = Path(mock, obj, [Action("kind", (ANY,), {}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (1,), {}, obj2)]) self.assertNotEquals(path1, path2) - path1 = Path(mock, [Action(obj1, "kind", (CONTAINS(1),), {})]) - path2 = Path(mock, [Action(obj2, "kind", (CONTAINS(1),), {})]) + path1 = Path(mock, obj, [Action("kind", (CONTAINS(1),), {}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (CONTAINS(1),), {}, obj2)]) self.assertEquals(path1, path2) def test_matches(self): + obj = object() mock = object() obj1 = object() obj2 = object() # Not the *same* mock. - path1 = Path([], []) - path2 = Path([], []) + path1 = Path([], obj, []) + path2 = Path([], obj, []) self.assertFalse(path1.matches(path2)) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, [Action(obj2, "kind", (), {})]) + path1 = Path(mock, obj1, []) + path2 = Path(mock, obj2, []) + self.assertTrue(path1.matches(path2)) + + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (), {}, obj2)]) self.assertTrue(path1.matches(path2)) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, [Action(obj2, "dnik", (), {})]) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, [Action("dnik", (), {}, obj2)]) self.assertFalse(path1.matches(path2)) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(object(), [Action(obj2, "kind", (), {})]) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(object(), [Action("kind", (), {}, obj2)]) self.assertFalse(path1.matches(path2)) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, [Action(obj2, "kind", (1,), {})]) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (1,), {}, obj2)]) self.assertFalse(path1.matches(path2)) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, [Action(obj2, "kind", (), {"a": 1})]) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (), {"a": 1}, obj2)]) self.assertFalse(path1.matches(path2)) - path1 = Path(mock, [Action(obj1, "kind", (), {})]) - path2 = Path(mock, []) + path1 = Path(mock, obj, [Action("kind", (), {}, obj1)]) + path2 = Path(mock, obj, []) self.assertFalse(path1.matches(path2)) - path1 = Path(mock, [Action(obj1, "kind", (VARIOUS,), {})]) - path2 = Path(mock, [Action(obj2, "kind", (), {})]) + path1 = Path(mock, obj, [Action("kind", (ARGS,), {}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (), {}, obj2)]) self.assertTrue(path1.matches(path2)) - path1 = Path(mock, [Action(obj1, "kind", (VARIOUS,), {"a": 1})]) - path2 = Path(mock, [Action(obj2, "kind", (), {})]) + path1 = Path(mock, obj, [Action("kind", (ARGS,), {"a": 1}, obj1)]) + path2 = Path(mock, obj, [Action("kind", (), {}, obj2)]) self.assertFalse(path1.matches(path2)) def test_str(self): @@ -887,40 +1106,40 @@ class PathTest(unittest.TestCase): self.assertEquals(str(path), "named_mock") def test_str_getattr(self): - path = Path(self.mock, [Action(None, "getattr", ("attr",), {})]) + path = Path(self.mock, None, [Action("getattr", ("attr",), {})]) self.assertEquals(str(path), "obj.attr") - path += Action(None, "getattr", ("x",), {}) + path += Action("getattr", ("x",), {}) self.assertEquals(str(path), "obj.attr.x") def test_str_call(self): - path = Path(self.mock, [Action(None, "call", (), {})]) + path = Path(self.mock, None, [Action("call", (), {})]) self.assertEquals(str(path), "obj()") - path = Path(self.mock, - [Action(None, "call", (1, "2"), {"a":3,"b":"4"})]) + path = Path(self.mock, None, + [Action("call", (1, "2"), {"a": 3, "b": "4"})]) self.assertEquals(str(path), "obj(1, '2', a=3, b='4')") def test_str_getattr_call(self): - path = Path(self.mock, [Action(None, "getattr", ("x",), {}), - Action(None, "getattr", ("y",), {}), - Action(None, "call", ("z",), {})]) + path = Path(self.mock, None, [Action("getattr", ("x",), {}), + Action("getattr", ("y",), {}), + Action("call", ("z",), {})]) self.assertEquals(str(path), "obj.x.y('z')") def test_str_raise_on_unknown(self): - path = Path(self.mock, [Action(None, "unknown", (), {})]) + path = Path(self.mock, None, [Action("unknown", (), {})]) self.assertRaises(RuntimeError, str, path) - def test_apply(self): + def test_execute(self): class C(object): pass obj = C() obj.x = C() obj.x.y = lambda a, b: a+b - path = Path(self.mock, [Action(None, "getattr", ("x",), {}), - Action(None, "getattr", ("y",), {}), - Action(None, "call", (1,), {"b": 2})]) - self.assertEquals(path.apply(obj), 3) + path = Path(self.mock, None, [Action("getattr", ("x",), {}), + Action("getattr", ("y",), {}), + Action("call", (1,), {"b": 2})]) + self.assertEquals(path.execute(obj), 3) class MatchParamsTest(unittest.TestCase): @@ -935,17 +1154,26 @@ class MatchParamsTest(unittest.TestCase): self.assertEquals(repr(ANY), "ANY") def test_any_equals(self): - self.assertEquals(ANY, 1) - self.assertEquals(ANY, 42) self.assertEquals(ANY, ANY) + self.assertNotEquals(ANY, ARGS) + self.assertNotEquals(ANY, object()) + + def test_any_matches(self): + self.assertTrue(ANY.matches(1)) + self.assertTrue(ANY.matches(42)) + self.assertTrue(ANY.matches(object())) def test_various_repr(self): self.assertEquals(repr(VARIOUS), "VARIOUS") def test_various_equals(self): self.assertEquals(VARIOUS, VARIOUS) - self.assertNotEquals(VARIOUS, ANY) - self.assertNotEquals(ANY, VARIOUS) + self.assertNotEquals(VARIOUS, object()) + + def test_various_matches(self): + self.assertTrue(VARIOUS.matches(1)) + self.assertTrue(VARIOUS.matches(42)) + self.assertTrue(VARIOUS.matches(object())) def test_same_repr(self): self.assertEquals(repr(SAME("obj")), "SAME('obj')") @@ -953,96 +1181,126 @@ class MatchParamsTest(unittest.TestCase): def test_same_equals(self): l1 = [] l2 = [] - self.assertEquals(SAME(l1), l1) self.assertNotEquals(SAME(l1), l2) - self.assertEquals(SAME(l1), SAME(l1)) self.assertNotEquals(SAME(l1), SAME(l2)) - self.assertNotEquals(ANY, SAME(l1)) - self.assertNotEquals(SAME(l1), ANY) + def test_same_matches(self): + l1 = [] + l2 = [] + self.assertTrue(SAME(l1).matches(l1)) + self.assertFalse(SAME(l1).matches(l2)) + self.assertFalse(SAME(l1).matches(ANY)) def test_contains_repr(self): self.assertEquals(repr(CONTAINS("obj")), "CONTAINS('obj')") def test_contains_equals(self): - self.assertEquals(CONTAINS(1), [1]) - self.assertNotEquals(CONTAINS([1]), [1]) - self.assertEquals(CONTAINS([1]), CONTAINS([1])) self.assertNotEquals(CONTAINS(1), CONTAINS([1])) - self.assertNotEquals(ANY, CONTAINS(1)) - self.assertNotEquals(CONTAINS(1), ANY) + def test_contains_matches(self): + self.assertTrue(CONTAINS(1).matches([1])) + self.assertFalse(CONTAINS([1]).matches([1])) + self.assertFalse(CONTAINS(1).matches(object())) + + def test_contains_matches_with_contains(self): + """Can't be iterated, but has contains hook.""" + class C(object): + def __contains__(self, value): + return True + self.assertTrue(CONTAINS(1).matches(C())) def test_normal(self): - self.assertTrue(match_params((), {}, (), {})) - self.assertTrue(match_params((1, 2), {"a": 3}, (1, 2), {"a": 3})) - self.assertFalse(match_params((1,), {}, (), {})) - self.assertFalse(match_params((), {}, (1,), {})) - self.assertFalse(match_params((1, 2), {"a": 3}, (1, 2), {"a": 4})) - self.assertFalse(match_params((1, 2), {"a": 3}, (1, 3), {"a": 3})) + self.true((), {}, (), {}) + self.true((1, 2), {"a": 3}, (1, 2), {"a": 3}) + self.false((1,), {}, (), {}) + self.false((), {}, (1,), {}) + self.false((1, 2), {"a": 3}, (1, 2), {"a": 4}) + self.false((1, 2), {"a": 3}, (1, 3), {"a": 3}) def test_any(self): - self.assertTrue(match_params((1, 2), {"a": ANY}, (1, 2), {"a": 4})) - self.assertTrue(match_params((1, ANY), {"a": 3}, (1, 3), {"a": 3})) - self.assertFalse(match_params((ANY,), {}, (), {})) - - def test_various_alone(self): - self.true((VARIOUS,), {}, (), {}) - self.true((VARIOUS,), {}, (1, 2), {}) - self.true((VARIOUS,), {}, (1, 2), {"a": 2}) - self.true((VARIOUS,), {}, (), {"a": 2}) - self.true((VARIOUS,), {"a": 1}, (), {"a": 1}) - self.true((VARIOUS,), {"a": 1}, (1, 2), {"a": 1}) - self.true((VARIOUS,), {"a": 1}, (), {"a": 1, "b": 2}) - self.true((VARIOUS,), {"a": 1}, (1, 2), {"a": 1, "b": 2}) - self.false((VARIOUS,), {"a": 1}, (), {}) - - def test_various_at_start(self): - self.true((VARIOUS, 3, 4), {}, (3, 4), {}) - self.true((VARIOUS, 3, 4), {}, (1, 2, 3, 4), {}) - self.true((VARIOUS, 3, 4), {"a": 1}, (3, 4), {"a": 1}) - self.true((VARIOUS, 3, 4), {"a": 1}, (1, 2, 3, 4), {"a": 1, "b": 2}) - self.false((VARIOUS, 3, 4), {}, (), {}) - self.false((VARIOUS, 3, 4), {}, (3, 5), {}) - self.false((VARIOUS, 3, 4), {}, (5, 5), {}) - self.false((VARIOUS, 3, 4), {"a": 1}, (), {}) - self.false((VARIOUS, 3, 4), {"a": 1}, (3, 4), {}) - self.false((VARIOUS, 3, 4), {"a": 1}, (3, 4), {"b": 2}) - - def test_various_at_end(self): - self.true((1, 2, VARIOUS), {}, (1, 2), {}) - self.true((1, 2, VARIOUS), {}, (1, 2, 3, 4), {}) - self.true((1, 2, VARIOUS), {"a": 1}, (1, 2), {"a": 1}) - self.true((1, 2, VARIOUS), {"a": 1}, (1, 2, 3, 4), {"a": 1, "b": 2}) - self.false((1, 2, VARIOUS), {}, (), {}) - self.false((1, 2, VARIOUS), {}, (1, 3), {}) - self.false((1, 2, VARIOUS), {}, (3, 3), {}) - self.false((1, 2, VARIOUS), {"a": 1}, (), {}) - self.false((1, 2, VARIOUS), {"a": 1}, (1, 2), {}) - self.false((1, 2, VARIOUS), {"a": 1}, (1, 2), {"b": 2}) - - def test_various_at_middle(self): - self.true((1, VARIOUS, 4), {}, (1, 4), {}) - self.true((1, VARIOUS, 4), {}, (1, 2, 3, 4), {}) - self.true((1, VARIOUS, 4), {"a": 1}, (1, 4), {"a": 1}) - self.true((1, VARIOUS, 4), {"a": 1}, (1, 2, 3, 4), {"a": 1, "b": 2}) - self.false((1, VARIOUS, 4), {}, (), {}) - self.false((1, VARIOUS, 4), {}, (1, 5), {}) - self.false((1, VARIOUS, 4), {}, (5, 5), {}) - self.false((1, VARIOUS, 4), {"a": 1}, (), {}) - self.false((1, VARIOUS, 4), {"a": 1}, (1, 4), {}) - self.false((1, VARIOUS, 4), {"a": 1}, (1, 4), {"b": 2}) - - def test_various_multiple(self): - self.true((VARIOUS, 3, VARIOUS, 6, VARIOUS), {}, + self.true((1, 2), {"a": ANY}, (1, 2), {"a": 4}) + self.true((1, ANY), {"a": 3}, (1, 3), {"a": 3}) + self.false((ANY,), {}, (), {}) + + def test_args_alone(self): + self.true((ARGS,), {}, (), {}) + self.true((ARGS,), {}, (1, 2), {}) + self.false((ARGS,), {}, (1, 2), {"a": 2}) + self.false((ARGS,), {}, (), {"a": 2}) + self.true((ARGS,), {"a": 1}, (), {"a": 1}) + self.true((ARGS,), {"a": 1}, (1, 2), {"a": 1}) + self.false((ARGS,), {"a": 1}, (), {"a": 1, "b": 2}) + self.false((ARGS,), {"a": 1}, (1, 2), {"a": 1, "b": 2}) + self.false((ARGS,), {"a": 1}, (), {}) + + def test_kwargs_alone(self): + self.true((KWARGS,), {}, (), {}) + self.false((KWARGS,), {}, (1, 2), {}) + self.false((KWARGS,), {}, (1, 2), {"a": 2}) + self.true((KWARGS,), {}, (), {"a": 2}) + self.true((KWARGS,), {"a": 1}, (), {"a": 1}) + self.false((KWARGS,), {"a": 1}, (1, 2), {"a": 1}) + self.true((KWARGS,), {"a": 1}, (), {"a": 1, "b": 2}) + self.false((KWARGS,), {"a": 1}, (1, 2), {"a": 1, "b": 2}) + self.false((KWARGS,), {"a": 1}, (), {}) + + def test_args_kwargs(self): + self.true((ARGS, KWARGS), {}, (), {}) + self.true((ARGS, KWARGS), {}, (1, 2), {}) + self.true((ARGS, KWARGS), {}, (1, 2), {"a": 2}) + self.true((ARGS, KWARGS), {}, (), {"a": 2}) + self.true((ARGS, KWARGS), {"a": 1}, (), {"a": 1}) + self.true((ARGS, KWARGS), {"a": 1}, (1, 2), {"a": 1}) + self.true((ARGS, KWARGS), {"a": 1}, (), {"a": 1, "b": 2}) + self.true((ARGS, KWARGS), {"a": 1}, (1, 2), {"a": 1, "b": 2}) + self.false((ARGS, KWARGS), {"a": 1}, (), {}) + + def test_args_at_start(self): + self.true((ARGS, 3, 4), {}, (3, 4), {}) + self.true((ARGS, 3, 4), {}, (1, 2, 3, 4), {}) + self.true((ARGS, 3, 4), {"a": 1}, (3, 4), {"a": 1}) + self.false((ARGS, 3, 4), {"a": 1}, (1, 2, 3, 4), {"a": 1, "b": 2}) + self.false((ARGS, 3, 4), {}, (), {}) + self.false((ARGS, 3, 4), {}, (3, 5), {}) + self.false((ARGS, 3, 4), {}, (5, 5), {}) + self.false((ARGS, 3, 4), {"a": 1}, (), {}) + self.false((ARGS, 3, 4), {"a": 1}, (3, 4), {}) + self.false((ARGS, 3, 4), {"a": 1}, (3, 4), {"b": 2}) + + def test_args_at_end(self): + self.true((1, 2, ARGS), {}, (1, 2), {}) + self.true((1, 2, ARGS), {}, (1, 2, 3, 4), {}) + self.true((1, 2, ARGS), {"a": 1}, (1, 2), {"a": 1}) + self.false((1, 2, ARGS), {"a": 1}, (1, 2, 3, 4), {"a": 1, "b": 2}) + self.false((1, 2, ARGS), {}, (), {}) + self.false((1, 2, ARGS), {}, (1, 3), {}) + self.false((1, 2, ARGS), {}, (3, 3), {}) + self.false((1, 2, ARGS), {"a": 1}, (), {}) + self.false((1, 2, ARGS), {"a": 1}, (1, 2), {}) + self.false((1, 2, ARGS), {"a": 1}, (1, 2), {"b": 2}) + + def test_args_at_middle(self): + self.true((1, ARGS, 4), {}, (1, 4), {}) + self.true((1, ARGS, 4), {}, (1, 2, 3, 4), {}) + self.true((1, ARGS, 4), {"a": 1}, (1, 4), {"a": 1}) + self.false((1, ARGS, 4), {"a": 1}, (1, 2, 3, 4), {"a": 1, "b": 2}) + self.false((1, ARGS, 4), {}, (), {}) + self.false((1, ARGS, 4), {}, (1, 5), {}) + self.false((1, ARGS, 4), {}, (5, 5), {}) + self.false((1, ARGS, 4), {"a": 1}, (), {}) + self.false((1, ARGS, 4), {"a": 1}, (1, 4), {}) + self.false((1, ARGS, 4), {"a": 1}, (1, 4), {"b": 2}) + + def test_args_multiple(self): + self.true((ARGS, 3, ARGS, 6, ARGS), {}, (1, 2, 3, 4, 5, 6), {}) - self.true((VARIOUS, VARIOUS, VARIOUS), {}, (1, 2, 3, 4, 5, 6), {}) - self.true((VARIOUS, VARIOUS, VARIOUS), {}, (), {}) - self.false((VARIOUS, 3, VARIOUS, 6, VARIOUS), {}, + self.true((ARGS, ARGS, ARGS), {}, (1, 2, 3, 4, 5, 6), {}) + self.true((ARGS, ARGS, ARGS), {}, (), {}) + self.false((ARGS, 3, ARGS, 6, ARGS), {}, (1, 2, 3, 4, 5), {}) - self.false((VARIOUS, 3, VARIOUS, 6, VARIOUS), {}, + self.false((ARGS, 3, ARGS, 6, ARGS), {}, (1, 2, 4, 5, 6), {}) @@ -1055,95 +1313,150 @@ class MockTest(unittest.TestCase): def act(path): self.paths.append(path) return 42 + self.StubMocker = StubMocker self.mocker = StubMocker() - self.obj = Mock(self.mocker) - - def test_mocker(self): - self.assertEquals(self.obj.__mocker__, self.mocker) - self.assertEquals(self.obj.__mocker_name__, None) - - def test_default_path(self): - path = self.obj.__mocker_path__ - self.assertEquals(path.root_mock, self.obj) - self.assertEquals(path.actions, ()) + self.mock = Mock(self.mocker) + + def test_default_attributes(self): + self.assertEquals(self.mock.__mocker__, self.mocker) + self.assertEquals(self.mock.__mocker_path__, Path(self.mock)) + self.assertEquals(self.mock.__mocker_name__, None) + self.assertEquals(self.mock.__mocker_spec__, None) + self.assertEquals(self.mock.__mocker_type__, None) + self.assertEquals(self.mock.__mocker_object__, None) + self.assertEquals(self.mock.__mocker_passthrough__, False) + self.assertEquals(self.mock.__mocker_patcher__, None) + self.assertEquals(self.mock.__mocker_replace__, False) def test_path(self): path = object() self.assertEquals(Mock(self.mocker, path).__mocker_path__, path) def test_object(self): - obj = Mock(self.mocker, object="foo") - self.assertEquals(obj.__mocker_object__, "foo") + mock = Mock(self.mocker, object="foo") + self.assertEquals(mock.__mocker_object__, "foo") + self.assertEquals(mock.__mocker_path__.root_object, "foo") def test_passthrough(self): - obj = Mock(self.mocker, object="foo", passthrough=True) - self.assertEquals(obj.__mocker_object__, "foo") - self.assertEquals(obj.__mocker_passthrough__, True) + mock = Mock(self.mocker, object="foo", passthrough=True) + self.assertEquals(mock.__mocker_object__, "foo") + self.assertEquals(mock.__mocker_passthrough__, True) + + def test_spec(self): + C = object() + self.assertEquals(Mock(self.mocker, spec=C).__mocker_spec__, C) + + def test_type(self): + def raise_exception(self, path): + raise MatchError + self.StubMocker.act = raise_exception + class C(object): pass + mock = Mock(self.mocker, type=C) + self.assertEquals(mock.__mocker_type__, C) + self.assertEquals(mock.__class__, C) + self.assertEquals(isinstance(mock, C), True) def test_auto_naming(self): - named_obj = self.obj - named_obj.attr - another_name = named_obj - named_obj = None # Can't find this one anymore. + named_mock = self.mock + named_mock.attr + another_name = named_mock + named_mock = None # Can't find this one anymore. another_name.attr - self.assertEquals(another_name.__mocker_name__, "named_obj") + self.assertEquals(another_name.__mocker_name__, "named_mock") def test_auto_naming_on_self(self): - self.named_obj = self.obj - del self.obj - self.named_obj.attr - self.assertEquals(self.named_obj.__mocker_name__, "named_obj") + self.named_mock = self.mock + del self.mock + self.named_mock.attr + self.assertEquals(self.named_mock.__mocker_name__, "named_mock") def test_auto_naming_on_bad_self(self): self_ = self self = object() # No __dict__ - self_.named_obj = self_.obj - self_.named_obj.attr - self_.assertEquals(self_.named_obj.__mocker_name__, None) + self_.named_mock = self_.mock + self_.named_mock.attr + self_.assertEquals(self_.named_mock.__mocker_name__, None) def test_auto_naming_without_getframe(self): getframe = sys._getframe sys._getframe = None try: - self.named_obj = self.obj - self.named_obj.attr - self.assertEquals(self.named_obj.__mocker_name__, None) + self.named_mock = self.mock + self.named_mock.attr + self.assertEquals(self.named_mock.__mocker_name__, None) finally: sys._getframe = getframe def test_getattr(self): - self.assertEquals(self.obj.attr, 42) + self.assertEquals(self.mock.attr, 42) (path,) = self.paths self.assertEquals(type(path), Path) - self.assertTrue(path.parent_path is self.obj.__mocker_path__) - self.assertEquals(path, self.obj.__mocker_path__ + - Action(None, "getattr", ("attr",), {})) + self.assertTrue(path.parent_path is self.mock.__mocker_path__) + self.assertEquals(path, self.mock.__mocker_path__ + + Action("getattr", ("attr",), {})) def test_call(self): - self.obj(1, a=2) + self.mock(1, a=2) (path,) = self.paths self.assertEquals(type(path), Path) - self.assertTrue(path.parent_path is self.obj.__mocker_path__) - self.assertEquals(path, self.obj.__mocker_path__ + - Action(None, "call", (1,), {"a": 2})) + self.assertTrue(path.parent_path is self.mock.__mocker_path__) + self.assertEquals(path, self.mock.__mocker_path__ + + Action("call", (1,), {"a": 2})) def test_passthrough_on_unexpected(self): class StubMocker(object): def act(self, path): if path.actions[-1].args == ("x",): - raise UnexpectedExprError + raise MatchError return 42 class C(object): x = 123 y = 321 - obj = Mock(StubMocker(), object=C()) - self.assertRaises(UnexpectedExprError, getattr, obj, "x", 42) - self.assertEquals(obj.y, 42) + mock = Mock(StubMocker(), object=C()) + self.assertRaises(MatchError, getattr, mock, "x", 42) + self.assertEquals(mock.y, 42) + + mock = Mock(StubMocker(), passthrough=True) + self.assertRaises(MatchError, getattr, mock, "x", 42) + self.assertEquals(mock.y, 42) + + mock = Mock(StubMocker(), object=C(), passthrough=True) + self.assertEquals(mock.x, 123) + self.assertEquals(mock.y, 42) + + mock = Mock(StubMocker(), passthrough=True) + act = mock.__mocker_act__ + self.assertEquals(act("getattr", ("x",), 42, object=C()), 123) + self.assertEquals(act("getattr", ("y",), 42, object=C()), 42) + + def test_act_with_object(self): + obj = object() + self.mock.__mocker_act__("kind", object=obj) + (path,) = self.paths + self.assertEquals(type(path), Path) + self.assertTrue(path.parent_path is self.mock.__mocker_path__) + self.assertTrue(path.root_object is obj) + + def test_reraise_assertion(self): + class StubMocker(object): + def act(self, path): + message = os.linesep.join(["An", "- error", "- happened"]) + raise AssertionError(message) + mock = Mock(StubMocker()) + try: + mock.__mocker_act__("kind") + except AssertionError, e: + message = os.linesep.join(["[Mocker] Unmet expectation:", + "", + "=> An", + " - error", + " - happened", + ""]) + self.assertEquals(str(e), message) + else: + self.fail("AssertionError not raised") - obj = Mock(StubMocker(), object=C(), passthrough=True) - self.assertEquals(obj.x, 123) - self.assertEquals(obj.y, 42) class EventTest(unittest.TestCase): @@ -1220,9 +1533,32 @@ class EventTest(unittest.TestCase): self.assertEquals(self.event.run(42), False) self.assertEquals(calls, [42, 42, 42]) + def test_run_errors(self): + class MyTask(object): + def __init__(self, id, failed): + self.id = id + self.failed = failed + def run(self, path): + if self.failed: + raise AssertionError("%d failed" % self.id) + event = Event("i.am.a.path") + event.add_task(MyTask(1, True)) + event.add_task(MyTask(2, False)) + event.add_task(MyTask(3, True)) + + try: + event.run(42) + except AssertionError, e: + message = os.linesep.join(["i.am.a.path", + "- 1 failed", + "- 3 failed"]) + self.assertEquals(str(e), message) + else: + self.fail("AssertionError not raised") + def test_satisfied_false(self): def raise_error(): - raise AssertionError + raise AssertionError("An error") task1 = self.event.add_task(Task()) task2 = self.event.add_task(Task()) task2.verify = raise_error @@ -1237,13 +1573,27 @@ class EventTest(unittest.TestCase): self.assertEquals(self.event.satisfied(), True) def test_verify(self): - calls = [] - task1 = self.event.add_task(Task()) - task1.verify = lambda: calls.append(1) - task2 = self.event.add_task(Task()) - task2.verify = lambda: calls.append(2) - self.event.verify() - self.assertEquals(calls, [1, 2]) + class MyTask(object): + def __init__(self, id, failed): + self.id = id + self.failed = failed + def verify(self): + if self.failed: + raise AssertionError("%d failed" % self.id) + event = Event("i.am.a.path") + event.add_task(MyTask(1, True)) + event.add_task(MyTask(2, False)) + event.add_task(MyTask(3, True)) + + try: + event.verify() + except AssertionError, e: + message = os.linesep.join(["i.am.a.path", + "- 1 failed", + "- 3 failed"]) + self.assertEquals(str(e), message) + else: + self.fail("AssertionError not raised") def test_set_state(self): calls = [] @@ -1277,7 +1627,7 @@ class PathMatcherTest(unittest.TestCase): def setUp(self): self.mocker = CleanMocker() - self.mock = self.mocker.obj() + self.mock = self.mocker.mock() def test_is_task(self): self.assertTrue(isinstance(PathMatcher(None), Task)) @@ -1288,15 +1638,15 @@ class PathMatcherTest(unittest.TestCase): self.assertEquals(task.path, path) def test_matches(self): - path = Path(self.mock, [Action(None, "getattr", ("attr1",), {})]) + path = Path(self.mock, None, [Action("getattr", ("attr1",), {})]) task = PathMatcher(path) - action = Action(Path(self.mock), "getattr", (), {}) + action = Action("getattr", (), {}, Path(self.mock)) self.assertFalse(task.matches(action.path + action)) - action = Action(Path(self.mock), "getattr", ("attr1",), {}) + action = Action("getattr", ("attr1",), {}, Path(self.mock)) self.assertTrue(task.matches(action.path + action)) def test_recorder(self): - path = Path(self.mock, [Action(None, "call", (), {})]) + path = Path(self.mock, [Action("call", (), {})]) event = Event(path) path_matcher_recorder(self.mocker, event) (task,) = event.get_tasks() @@ -1311,8 +1661,8 @@ class RunCounterTest(unittest.TestCase): def setUp(self): self.mocker = CleanMocker() - self.mock = self.mocker.obj() - self.action = Action(Path(self.mock), "getattr", ("attr",), {}) + self.mock = self.mocker.mock() + self.action = Action("getattr", ("attr",), {}, Path(self.mock)) self.path = Path(self.mock, [self.action]) self.event = Event(self.path) @@ -1387,10 +1737,10 @@ class RunCounterTest(unittest.TestCase): may be repeated any number of times. """ path1 = Path(self.mock) - path2 = path1 + Action(path1, "getattr", ("attr",), {}) - path3 = path2 + Action(path2, "getattr", ("attr",), {}) - path4 = path3 + Action(path3, "call", (), {}) - path5 = path4 + Action(path4, "call", (), {}) + path2 = path1 + Action("getattr", ("attr",), {}, path1) + path3 = path2 + Action("getattr", ("attr",), {}, path2) + path4 = path3 + Action("call", (), {}, path3) + path5 = path4 + Action("call", (), {}, path4) event3 = self.mocker.add_event(Event(path3)) event2 = self.mocker.add_event(Event(path2)) @@ -1437,7 +1787,7 @@ class RunCounterTest(unittest.TestCase): self.mocker.add_recorder(run_counter_recorder) self.mocker.add_recorder(run_counter_removal_recorder) - obj = self.mocker.obj() + obj = self.mocker.mock() obj.x.y()() @@ -1457,8 +1807,8 @@ class MockReturnerTest(unittest.TestCase): def setUp(self): self.mocker = CleanMocker() - self.mock = self.mocker.obj() - self.action = Action(Path(self.mock), "getattr", ("attr",), {}) + self.mock = self.mocker.mock() + self.action = Action("getattr", ("attr",), {}, Path(self.mock)) self.path = Path(self.mock, [self.action]) self.event = Event(self.path) @@ -1474,9 +1824,9 @@ class MockReturnerTest(unittest.TestCase): def test_recorder(self): path1 = Path(self.mock) - path2 = path1 + Action(path1, "getattr", ("attr",), {}) - path3 = path2 + Action(path2, "getattr", ("attr",), {}) - path4 = path3 + Action(path3, "call", (), {}) + path2 = path1 + Action("getattr", ("attr",), {}, path1) + path3 = path2 + Action("getattr", ("attr",), {}, path2) + path4 = path3 + Action("call", (), {}, path3) event2 = self.mocker.add_event(Event(path2)) event3 = self.mocker.add_event(Event(path3)) @@ -1514,9 +1864,9 @@ class FunctionRunnerTest(unittest.TestCase): def setUp(self): self.mocker = CleanMocker() - self.mock = self.mocker.obj() - self.action = Action(Path(self.mock), "call", (1, 2), {"c": 3}) - self.path = Path(self.mock, [self.action]) + self.mock = self.mocker.mock() + self.action = Action("call", (1, 2), {"c": 3}, Path(self.mock)) + self.path = Path(self.mock, None, [self.action]) self.event = Event(self.path) def test_is_task(self): @@ -1528,13 +1878,13 @@ class FunctionRunnerTest(unittest.TestCase): self.assertEquals(result, "((1, 2), {'c': 3})") -class PathApplierTest(unittest.TestCase): +class PathExecuterTest(unittest.TestCase): def setUp(self): self.mocker = CleanMocker() def test_is_task(self): - self.assertTrue(isinstance(PathApplier(), Task)) + self.assertTrue(isinstance(PathExecuter(), Task)) def test_run(self): class C(object): @@ -1543,13 +1893,11 @@ class PathApplierTest(unittest.TestCase): obj.x = C() obj.x.y = lambda a, b: a+b - proxy = self.mocker.proxy(obj) + path = Path(None, obj, [Action("getattr", ("x",), {}), + Action("getattr", ("y",), {}), + Action("call", (1,), {"b": 2})]) - path = Path(proxy, [Action(None, "getattr", ("x",), {}), - Action(None, "getattr", ("y",), {}), - Action(None, "call", (1,), {"b": 2})]) - - task = PathApplier() + task = PathExecuter() self.assertEquals(task.run(path), 3) @@ -1557,8 +1905,8 @@ class OrdererTest(unittest.TestCase): def setUp(self): self.mocker = CleanMocker() - self.mock = self.mocker.obj() - self.action = Action(Path(self.mock), "call", (1, 2), {"c": 3}) + self.mock = self.mocker.mock() + self.action = Action("call", (1, 2, Path(self.mock)), {"c": 3}) self.path = Path(self.mock, [self.action]) def test_is_task(self): @@ -1605,10 +1953,10 @@ class SpecCheckerTest(unittest.TestCase): def staticnoargs(): pass self.cls = C self.mocker = CleanMocker() - self.mock = self.mocker.obj(self.cls) + self.mock = self.mocker.mock(self.cls) def path(self, *args, **kwargs): - action = Action(Path(self.mock), "call", args, kwargs) + action = Action("call", args, kwargs, Path(self.mock)) return action.path + action def good(self, method_names, args_expr): @@ -1647,9 +1995,19 @@ class SpecCheckerTest(unittest.TestCase): def test_is_task(self): self.assertTrue(isinstance(SpecChecker(self.cls.normal), Task)) + def test_error_message(self): + task = SpecChecker(self.cls.normal) + try: + task.run(self.path(1)) + except AssertionError, e: + self.assertEquals(str(e), "Specification is normal(a, b, c=3): " + "'b' not provided") + else: + self.fail("AssertionError not raised") + def test_recorder(self): self.mocker.add_recorder(spec_checker_recorder) - obj = self.mocker.obj(self.cls) + obj = self.mocker.mock(spec=self.cls) obj.noargs() getattr, call = self.mocker.get_events() self.assertEquals(getattr.get_tasks(), []) @@ -1659,7 +2017,7 @@ class SpecCheckerTest(unittest.TestCase): def test_recorder_with_unexistent_method(self): self.mocker.add_recorder(spec_checker_recorder) - obj = self.mocker.obj(self.cls) + obj = self.mocker.mock(spec=self.cls) obj.unexistent() getattr, call = self.mocker.get_events() self.assertEquals(getattr.get_tasks(), []) @@ -1669,7 +2027,7 @@ class SpecCheckerTest(unittest.TestCase): def test_recorder_second_action_isnt_call(self): self.mocker.add_recorder(spec_checker_recorder) - obj = self.mocker.obj(self.cls) + obj = self.mocker.mock(spec=self.cls) obj.noargs.x event1, event2 = self.mocker.get_events() self.assertEquals(event1.get_tasks(), []) @@ -1677,7 +2035,7 @@ class SpecCheckerTest(unittest.TestCase): def test_recorder_first_action_isnt_getattr(self): self.mocker.add_recorder(spec_checker_recorder) - obj = self.mocker.obj(self.cls) + obj = self.mocker.mock(spec=self.cls) obj("noargs").x event1, event2 = self.mocker.get_events() self.assertEquals(event1.get_tasks(), []) @@ -1685,7 +2043,7 @@ class SpecCheckerTest(unittest.TestCase): def test_recorder_more_than_two_actions(self): self.mocker.add_recorder(spec_checker_recorder) - obj = self.mocker.obj(self.cls) + obj = self.mocker.mock(spec=self.cls) obj.noargs().x event1, event2, event3 = self.mocker.get_events() self.assertEquals(len(event1.get_tasks()), 0) @@ -1742,23 +2100,23 @@ class SpecCheckerTest(unittest.TestCase): self.bad("unexistent", "") -class ProxyInstallerTest(unittest.TestCase): +class ProxyReplacerTest(unittest.TestCase): def setUp(self): self.mocker = CleanMocker() import calendar self.mock = Mock(self.mocker, object=calendar) - self.task = ProxyInstaller(self.mock) + self.task = ProxyReplacer(self.mock) def tearDown(self): self.task.set_state(RESTORE) def test_is_task(self): - self.assertTrue(isinstance(ProxyInstaller(None), Task)) + self.assertTrue(isinstance(ProxyReplacer(None), Task)) def test_mock(self): mock = object() - task = ProxyInstaller(mock) + task = ProxyReplacer(mock) self.assertEquals(task.mock, mock) def test_matches_nothing(self): @@ -1778,11 +2136,27 @@ class ProxyInstallerTest(unittest.TestCase): self.task.set_state(REPLAY) self.assertEquals(type(self.mock.__mocker_object__), ModuleType) + def test_install_protects_path(self): + self.task.set_state(REPLAY) + self.assertEquals(type(self.mock.__mocker_path__.root_object), + ModuleType) + def test_deinstall_protects_task(self): self.task.set_state(REPLAY) self.task.set_state(RESTORE) self.assertEquals(type(self.task.mock), Mock) + def test_install_protects_anything_with_mocker_replace_false(self): + class C(object): + def __init__(self): + import calendar + self.calendar = calendar + self.__mocker_replace__ = False + obj = C() + self.task.set_state(REPLAY) + self.assertEquals(type(self.mock.__mocker_path__.root_object), + ModuleType) + def test_install_on_object(self): class C(object): def __init__(self): @@ -1796,7 +2170,7 @@ class ProxyInstallerTest(unittest.TestCase): def test_install_on_submodule(self): from os import path mock = Mock(self.mocker, object=path) - task = ProxyInstaller(mock) + task = ProxyReplacer(mock) task.set_state(REPLAY) import os self.assertEquals(type(os.path), Mock) @@ -1830,12 +2204,315 @@ class ProxyInstallerTest(unittest.TestCase): def test_deinstall_from_submodule(self): from os import path mock = Mock(self.mocker, object=path) - task = ProxyInstaller(mock) + task = ProxyReplacer(mock) task.set_state(REPLAY) task.set_state(RESTORE) import os self.assertEquals(type(os.path), ModuleType) +class PatcherTest(unittest.TestCase): + + def setUp(self): + self.mocker = Mocker() + self.patcher = Patcher() + self.C = type("C", (object,), {}) + self.D = type("D", (self.C,), {}) + self.E = type("E", (), {}) + + class MockStub(object): + def __mocker_act__(self, kind, args=(), kwargs={}, object=None): + return (kind, args, kwargs, object) + + self.MockStub = MockStub + + def test_is_task(self): + self.assertTrue(isinstance(Patcher(), Task)) + + def test_matches_nothing(self): + self.assertFalse(self.patcher.matches(None)) + + def test_is_monitoring_unseen_class_kind(self): + self.assertFalse(self.patcher.is_monitoring(self.C, "kind")) + + def test_monitor_class(self): + self.patcher.monitor(self.C, "kind") + self.assertTrue(self.patcher.is_monitoring(self.C, "kind")) + + def test_monitor_subclass(self): + self.patcher.monitor(self.C, "kind") + self.assertTrue(self.patcher.is_monitoring(self.D, "kind")) + + def test_monitor_unknown_class(self): + self.patcher.monitor(self.C, "kind") + self.assertFalse(self.patcher.is_monitoring(self.E, "kind")) + + def test_is_monitoring_unseen_instance(self): + obj = self.E() + self.patcher.monitor(self.C, "kind") + self.assertFalse(self.patcher.is_monitoring(obj, "kind")) + + def test_is_monitoring_instance_explicitly_monitored(self): + obj = self.C() + self.patcher.monitor(obj, "kind") + self.assertTrue(self.patcher.is_monitoring(obj, "kind")) + + def test_is_monitoring_instance_monitored_by_class(self): + obj = self.D() + self.patcher.monitor(self.D, "kind") + self.assertTrue(self.patcher.is_monitoring(obj, "kind")) + + def test_patch_attr(self): + self.patcher.patch_attr(self.C, "attr", "patch") + self.assertEquals(self.C.__dict__.get("attr"), "patch") + + def test_patch_attr_and_restore(self): + self.patcher.patch_attr(self.C, "attr", "patch") + self.patcher.set_state(RESTORE) + self.assertTrue("attr" not in self.C.__dict__) + + def test_patch_attr_and_restore_to_original(self): + self.C.attr = "original" + self.patcher.patch_attr(self.C, "attr", "patch") + self.patcher.set_state(RESTORE) + self.assertEquals(self.C.__dict__.get("attr"), "original") + + def test_get_unpatched_attr_unpatched_undefined(self): + self.assertEquals(self.patcher.get_unpatched_attr(self.C, "attr"), + Undefined) + + def test_get_unpatched_attr_unpatched(self): + self.C.attr = "original" + self.assertEquals(self.patcher.get_unpatched_attr(self.C, "attr"), + "original") + + def test_get_unpatched_attr_defined_on_superclass(self): + self.C.attr = "original" + self.assertEquals(self.patcher.get_unpatched_attr(self.D, "attr"), + "original") + + def test_get_unpatched_attr_defined_on_superclass_patched_on_sub(self): + self.C.attr = "original" + self.patcher.patch_attr(self.D, "attr", "patch") + self.assertEquals(self.patcher.get_unpatched_attr(self.D, "attr"), + "original") + + def test_get_unpatched_attr_patched_originally_undefined(self): + self.patcher.patch_attr(self.C, "attr", "patch") + self.assertEquals(self.patcher.get_unpatched_attr(self.C, "attr"), + Undefined) + + def test_get_unpatched_attr_patched(self): + self.C.attr = "original" + self.patcher.patch_attr(self.C, "attr", "patch") + self.assertEquals(self.patcher.get_unpatched_attr(self.C, "attr"), + "original") + + def test_get_unpatched_attr_on_instance_originally_undefined(self): + self.assertEquals(self.patcher.get_unpatched_attr(self.C(), "attr"), + Undefined) + + def test_get_unpatched_attr_on_instance(self): + self.C.attr = "original" + self.assertEquals(self.patcher.get_unpatched_attr(self.D(), "attr"), + "original") + + def test_get_unpatched_attr_on_instance_defined_on_superclass(self): + self.C.attr = "original" + self.patcher.patch_attr(self.C, "attr", "patch") + self.assertEquals(self.patcher.get_unpatched_attr(self.D(), "attr"), + "original") + + def test_get_unpatched_attr_on_instance_with_descriptor(self): + self.C.attr = property(lambda self: "original") + self.patcher.patch_attr(self.C, "attr", "patch") + self.assertEquals(self.patcher.get_unpatched_attr(self.D(), "attr"), + "original") + + def test_get_unpatched_attr_on_instance_with_fake_descriptor(self): + class BadProperty(object): + def __init__(self): + # On real, __get__ must be on the class, not on the instance. + self.__get__ = lambda self, obj, cls=None: "original" + prop = BadProperty() + self.C.attr = prop + self.patcher.patch_attr(self.C, "attr", "patch") + self.assertEquals(self.patcher.get_unpatched_attr(self.D(), "attr"), + prop) + + def test_replay_with_monitored_class(self): + self.patcher.monitor(self.C, "call") + self.patcher.set_state(REPLAY) + self.assertEquals(type(self.C.__dict__["__call__"]), PatchedMethod) + + def test_replay_with_monitored_instance(self): + self.patcher.monitor(self.C(), "call") + self.patcher.set_state(REPLAY) + self.assertEquals(type(self.C.__dict__["__call__"]), PatchedMethod) + + def test_replay_getattr(self): + self.patcher.monitor(self.C, "getattr") + self.patcher.set_state(REPLAY) + self.assertEquals(type(self.C.__dict__["__getattribute__"]), + PatchedMethod) + + def test_restore(self): + self.patcher.monitor(self.C, "call") + self.patcher.set_state(REPLAY) + self.patcher.set_state(RESTORE) + self.assertTrue("__call__" not in self.C.__dict__) + + def test_restore_twice_does_nothing(self): + self.patcher.monitor(self.C, "call") + self.patcher.set_state(REPLAY) + self.patcher.set_state(RESTORE) + self.C.__call__ = "original" + self.patcher.set_state(RESTORE) + self.assertTrue(self.C.__dict__.get("__call__"), "original") + + def test_patched_call_on_instance(self): + self.patcher.monitor(self.C, "call") + obj = self.C() + obj.__mocker_mock__ = self.MockStub() + self.patcher.set_state(REPLAY) + result = obj(1, a=2) + self.assertEquals(result, ("call", (1,), {"a": 2}, obj)) + + def test_patched_call_on_class(self): + self.patcher.monitor(self.C, "call") + self.C.__mocker_mock__ = self.MockStub() + self.patcher.set_state(REPLAY) + obj = self.C() + result = obj(1, a=2) + self.assertEquals(result, ("call", (1,), {"a": 2}, obj)) + + def test_patched_call_on_class_edge_case(self): + """Only "getattr" kind should passthrough on __mocker_* arguments.""" + self.patcher.monitor(self.C, "call") + self.C.__mocker_mock__ = self.MockStub() + self.patcher.set_state(REPLAY) + obj = self.C() + result = obj("__mocker_mock__") + self.assertEquals(result, ("call", ("__mocker_mock__",), {}, obj)) + + def test_patched_getattr_on_class(self): + self.patcher.monitor(self.C, "getattr") + self.C.__mocker_mock__ = self.MockStub() + self.patcher.set_state(REPLAY) + obj = self.C() + result = obj.attr + self.assertEquals(result, ("getattr", ("attr",), {}, obj)) + + def test_patched_getattr_on_unmonitored_object(self): + obj1 = self.C() + obj1.__mocker_mock__ = self.MockStub() + self.patcher.monitor(obj1, "getattr") + obj2 = self.C() + obj2.attr = "original" + self.patcher.set_state(REPLAY) + self.assertEquals(obj1.attr, ("getattr", ("attr",), {}, obj1)) + self.assertEquals(obj2.attr, "original") + + def test_patched_getattr_on_different_instances(self): + def build_getattr(original): + def __getattribute__(self, name): + if name == "attr": + return original + return object.__getattribute__(self, name) + return __getattribute__ + self.C.__getattribute__ = build_getattr("originalC") + self.D.__getattribute__ = build_getattr("originalD") + + class MockStub(object): + def __init__(self, id): + self.id = id + def __mocker_act__(self, kind, args=(), kwargs={}, object=None): + return self.id + + obj1, obj2, obj3, obj4, obj5, obj6 = [self.C() for i in range(6)] + obj7, obj8, obj9 = [self.D() for i in range(3)] + + obj2.__mocker_mock__ = MockStub(2) + self.patcher.monitor(obj2, "getattr") + obj5.__mocker_mock__ = MockStub(5) + self.patcher.monitor(obj5, "getattr") + obj8.__mocker_mock__ = MockStub(8) + self.patcher.monitor(obj8, "getattr") + + self.patcher.set_state(REPLAY) + self.assertEquals(obj1.attr, "originalC") + self.assertEquals(obj2.attr, 2) + self.assertEquals(obj3.attr, "originalC") + self.assertEquals(obj4.attr, "originalC") + self.assertEquals(obj5.attr, 5) + self.assertEquals(obj6.attr, "originalC") + self.assertEquals(obj7.attr, "originalD") + self.assertEquals(obj8.attr, 8) + self.assertEquals(obj9.attr, "originalD") + + def test_execute_getattr(self): + class C(object): + def __getattribute__(self, attr): + if attr == "attr": + return "original" + action = Action("getattr", ("attr",), {}) + obj = C() + self.patcher.monitor(obj, "getattr") + self.patcher.set_state(REPLAY) + self.assertEquals(self.patcher.execute(action, obj), "original") + + def test_execute_getattr_on_unexistent(self): + action = Action("getattr", ("attr",), {}) + obj = self.C() + self.patcher.monitor(obj, "getattr") + self.patcher.set_state(REPLAY) + self.assertRaises(AttributeError, self.patcher.execute, action, obj) + + def test_execute_call(self): + class C(object): + def __call__(self, *args, **kwargs): + return (args, kwargs) + action = Action("call", (1,), {"a": 2}) + obj = C() + self.patcher.monitor(obj, "call") + self.patcher.set_state(REPLAY) + self.assertEquals(self.patcher.execute(action, obj), ((1,), {"a": 2})) + + def test_recorder_class_getattr(self): + mock = self.mocker.patch(self.C) + mock.method() + self.mocker.result("mocked") + self.mocker.replay() + self.assertEquals(self.C().method(), "mocked") + self.assertRaises(AssertionError, self.C().method) + + def test_recorder_instance_getattr(self): + self.C.attr = "original" + obj1 = self.C() + obj2 = self.C() + mock = self.mocker.patch(obj1) + mock.attr + self.mocker.result("mocked") + self.mocker.replay() + self.assertEquals(obj1.attr, "mocked") + self.assertRaises(AssertionError, getattr, obj1, "attr") + self.assertEquals(obj2.attr, "original") + self.assertRaises(AttributeError, getattr, obj1, "unexistent") + + def test_recorder_passthrough(self): + class C(object): + def __init__(self): + self.result = "original" # Value on object's dictionary. + def method(self): + return self.result + mock = self.mocker.patch(C) + mock.method() + self.mocker.passthrough() + self.mocker.replay() + obj = C() + self.assertEquals(obj.method(), "original") + self.assertRaises(AssertionError, obj.method) + + if __name__ == "__main__": unittest.main() |