summaryrefslogtreecommitdiff
path: root/mocker.py
diff options
context:
space:
mode:
authorGustavo Niemeyer <gustavo@niemeyer.net>2010-09-18 15:11:38 -0300
committerGustavo Niemeyer <gustavo@niemeyer.net>2010-09-18 15:11:38 -0300
commit21f0fa2da7fcece8f8ed3125333189e666c66077 (patch)
tree674bc7edc882d538646c19d53b451100e7430a3a /mocker.py
parentdec6aee3dbe210865dcd6d0a183f49261a90a021 (diff)
downloadmocker-21f0fa2da7fcece8f8ed3125333189e666c66077.tar.gz
Tasks now have a may_run_user_code() method, which is used for tasks which
can potentially run unknown code or throw unknown exceptions. This is used by the Event.run() method to prevent running unknown logic when the event as a whole is already known to have failed.
Diffstat (limited to 'mocker.py')
-rw-r--r--mocker.py65
1 files changed, 50 insertions, 15 deletions
diff --git a/mocker.py b/mocker.py
index a453ab5..eee6c64 100644
--- a/mocker.py
+++ b/mocker.py
@@ -381,6 +381,9 @@ class MockerTestCase(unittest.TestCase):
assertMethodsMatch = failUnlessMethodsMatch
assertRaises = failUnlessRaises
+ # XXX Add assertIsInstance() and assertIsSubclass, and
+ # extend assertRaises() with new logic from unittest.
+
# The following are missing in Python < 2.4.
assertTrue = unittest.TestCase.failUnless
assertFalse = unittest.TestCase.failIf
@@ -754,7 +757,7 @@ class MockerBase(object):
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
+ This method is part of the interface between the mocker
and mock objects.
"""
if self._recording:
@@ -873,7 +876,7 @@ class MockerBase(object):
for task in event.get_tasks():
if isinstance(task, RunCounter):
event.remove_task(task)
- event.add_task(RunCounter(min, max))
+ event.prepend_task(RunCounter(min, max))
def is_ordering(self):
"""Return true if all events are being ordered.
@@ -1559,13 +1562,25 @@ class Event(object):
self._has_run = False
def add_task(self, task):
- """Add a new task to this taks."""
+ """Add a new task to this task."""
self._tasks.append(task)
return task
+ def prepend_task(self, task):
+ """Add a task at the front of the list."""
+ self._tasks.insert(0, task)
+ return task
+
def remove_task(self, task):
self._tasks.remove(task)
+ def replace_task(self, old_task, new_task):
+ """Replace old_task with new_task, in the same position."""
+ for i in range(len(self._tasks)):
+ if self._tasks[i] is old_task:
+ self._tasks[i] = new_task
+ return new_task
+
def get_tasks(self):
return self._tasks[:]
@@ -1606,16 +1621,21 @@ class Event(object):
result = None
errors = []
for task in self._tasks:
- 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 not errors or not task.may_run_user_code():
+ 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:
+ # XXX That's actually a bit weird. What if a call() really
+ # returned None? This would improperly change the semantic
+ # of this process without any good reason. Test that with two
+ # call()s in sequence.
+ if task_result is not None:
+ result = task_result
if errors:
message = [str(self.path)]
if str(path) != message[0]:
@@ -1700,6 +1720,15 @@ class Task(object):
"""Return false if running this task would certainly raise an error."""
return True
+ def may_run_user_code(self):
+ """Return true if there's a chance this task may run custom code.
+
+ Whenever errors are detected, running user code should be avoided,
+ because the situation is already known to be incorrect, and any
+ errors in the user code are side effects rather than the cause.
+ """
+ return False
+
def run(self, path):
"""Perform the task item, considering that the given action happened.
"""
@@ -1796,7 +1825,9 @@ class ImplicitRunCounter(RunCounter):
def run_counter_recorder(mocker, event):
"""Any event may be repeated once, unless disabled by default."""
if event.path.root_mock.__mocker_count__:
- event.add_task(ImplicitRunCounter(1))
+ # Rather than appending the task, we prepend it so that the
+ # issue is raised before any other side-effects happen.
+ event.prepend_task(ImplicitRunCounter(1))
Mocker.add_recorder(run_counter_recorder)
@@ -1852,6 +1883,9 @@ class FunctionRunner(Task):
self._func = func
self._with_root_object = with_root_object
+ def may_run_user_code(self):
+ return True
+
def run(self, path):
action = path.actions[-1]
if self._with_root_object:
@@ -2149,7 +2183,8 @@ class PatchedMethod(object):
# At least with __getattribute__, Python seems to use *both* the
# descriptor API and also call the class attribute directly. It
# looks like an interpreter bug, or at least an undocumented
- # inconsistency.
+ # inconsistency. Coverage tests may show this uncovered, because
+ # it depends on the Python version.
return self.__get__(obj)(*args, **kwargs)