1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
|
# Created By: Virgil Dupras
# Created On: 2010-11-14
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
import pytest
def eq_(a, b, msg=None):
__tracebackhide__ = True
assert a == b, msg or "{!r} != {!r}".format(a, b)
def callcounter():
def f(*args, **kwargs):
f.callcount += 1
f.callcount = 0
return f
class CallLogger:
"""This is a dummy object that logs all calls made to it.
It is used to simulate the GUI layer.
"""
def __init__(self):
self.calls = []
def __getattr__(self, func_name):
def func(*args, **kw):
self.calls.append(func_name)
return func
def clear_calls(self):
del self.calls[:]
def check_gui_calls(self, expected, verify_order=False):
"""Checks that the expected calls have been made to 'self', then clears the log.
`expected` is an iterable of strings representing method names.
If `verify_order` is True, the order of the calls matters.
"""
__tracebackhide__ = True
if verify_order:
eq_(self.calls, expected)
else:
eq_(set(self.calls), set(expected))
self.clear_calls()
def check_gui_calls_partial(self, expected=None, not_expected=None, verify_order=False):
"""Checks that the expected calls have been made to 'self', then clears the log.
`expected` is an iterable of strings representing method names. Order doesn't matter.
Moreover, if calls have been made that are not in expected, no failure occur.
`not_expected` can be used for a more explicit check (rather than calling `check_gui_calls`
with an empty `expected`) to assert that calls have *not* been made.
"""
__tracebackhide__ = True
if expected is not None:
not_called = set(expected) - set(self.calls)
assert not not_called, f"These calls haven't been made: {not_called}"
if verify_order:
max_index = 0
for call in expected:
index = self.calls.index(call)
if index < max_index:
raise AssertionError(f"The call {call} hasn't been made in the correct order")
max_index = index
if not_expected is not None:
called = set(not_expected) & set(self.calls)
assert not called, f"These calls shouldn't have been made: {called}"
self.clear_calls()
class TestApp:
def __init__(self):
self._call_loggers = []
def clear_gui_calls(self):
for logger in self._call_loggers:
logger.clear_calls()
def make_logger(self, logger=None):
if logger is None:
logger = CallLogger()
self._call_loggers.append(logger)
return logger
def make_gui(self, name, class_, view=None, parent=None, holder=None):
if view is None:
view = self.make_logger()
if parent is None:
# The attribute "default_parent" has to be set for this to work correctly
parent = self.default_parent
if holder is None:
holder = self
setattr(holder, f"{name}_gui", view)
gui = class_(parent)
gui.view = view
setattr(holder, name, gui)
return gui
# To use @with_app, you have to import app in your conftest.py file.
def with_app(setupfunc):
def decorator(func):
func.setupfunc = setupfunc
return func
return decorator
@pytest.fixture
def app(request):
setupfunc = request.function.setupfunc
if hasattr(setupfunc, "__code__"):
argnames = setupfunc.__code__.co_varnames[: setupfunc.__code__.co_argcount]
def getarg(name):
if name == "self":
return request.function.__self__
else:
return request.getfixturevalue(name)
args = [getarg(argname) for argname in argnames]
else:
args = []
app = setupfunc(*args)
return app
def _unify_args(func, args, kwargs, args_to_ignore=None):
"""Unify args and kwargs in the same dictionary.
The result is kwargs with args added to it. func.func_code.co_varnames is used to determine
under what key each elements of arg will be mapped in kwargs.
if you want some arguments not to be in the results, supply a list of arg names in
args_to_ignore.
if f is a function that takes *args, func_code.co_varnames is empty, so args will be put
under 'args' in kwargs.
def foo(bar, baz)
_unifyArgs(foo, (42,), {'baz': 23}) --> {'bar': 42, 'baz': 23}
_unifyArgs(foo, (42,), {'baz': 23}, ['bar']) --> {'baz': 23}
"""
result = kwargs.copy()
if hasattr(func, "__code__"): # built-in functions don't have func_code
args = list(args)
if getattr(func, "__self__", None) is not None: # bound method, we have to add self to args list
args = [func.__self__] + args
defaults = list(func.__defaults__) if func.__defaults__ is not None else []
arg_count = func.__code__.co_argcount
arg_names = list(func.__code__.co_varnames)
if len(args) < arg_count: # We have default values
required_arg_count = arg_count - len(args)
args = args + defaults[-required_arg_count:]
for arg_name, arg in zip(arg_names, args):
# setdefault is used because if the arg is already in kwargs, we don't want to use default values
result.setdefault(arg_name, arg)
else:
# 'func' has a *args argument
result["args"] = args
if args_to_ignore:
for kw in args_to_ignore:
del result[kw]
return result
def log_calls(func):
"""Logs all func calls' arguments under func.calls.
func.calls is a list of _unify_args() result (dict).
Mostly used for unit testing.
"""
def wrapper(*args, **kwargs):
unified_args = _unify_args(func, args, kwargs)
wrapper.calls.append(unified_args)
return func(*args, **kwargs)
wrapper.calls = []
return wrapper
|