File: testutil.py

package info (click to toggle)
dupeguru 4.3.1-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,604 kB
  • sloc: python: 16,846; ansic: 424; makefile: 123
file content (191 lines) | stat: -rw-r--r-- 6,339 bytes parent folder | download | duplicates (3)
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