1fc46f261SDan Klein# -*- coding: utf-8 -*-
2fc46f261SDan Klein
3fc46f261SDan Klein# test/scaffold.py
4fc46f261SDan Klein# Part of ‘python-daemon’, an implementation of PEP 3143.
5fc46f261SDan Klein#
6fc46f261SDan Klein# Copyright © 2007–2015 Ben Finney <ben+python@benfinney.id.au>
7fc46f261SDan Klein#
8fc46f261SDan Klein# This is free software: you may copy, modify, and/or distribute this work
9fc46f261SDan Klein# under the terms of the Apache License, version 2.0 as published by the
10fc46f261SDan Klein# Apache Software Foundation.
11fc46f261SDan Klein# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details.
12fc46f261SDan Klein
13fc46f261SDan Klein""" Scaffolding for unit test modules.
14fc46f261SDan Klein    """
15fc46f261SDan Klein
16fc46f261SDan Kleinfrom __future__ import (absolute_import, unicode_literals)
17fc46f261SDan Klein
18fc46f261SDan Kleinimport unittest
19fc46f261SDan Kleinimport doctest
20fc46f261SDan Kleinimport logging
21fc46f261SDan Kleinimport os
22fc46f261SDan Kleinimport sys
23fc46f261SDan Kleinimport operator
24fc46f261SDan Kleinimport textwrap
25fc46f261SDan Kleinfrom copy import deepcopy
26fc46f261SDan Kleinimport functools
27fc46f261SDan Klein
28fc46f261SDan Kleintry:
29fc46f261SDan Klein    # Python 2 has both ‘str’ (bytes) and ‘unicode’ (text).
30fc46f261SDan Klein    basestring = basestring
31fc46f261SDan Klein    unicode = unicode
32fc46f261SDan Kleinexcept NameError:
33fc46f261SDan Klein    # Python 3 names the Unicode data type ‘str’.
34fc46f261SDan Klein    basestring = str
35fc46f261SDan Klein    unicode = str
36fc46f261SDan Klein
37fc46f261SDan Kleinimport testscenarios
38fc46f261SDan Kleinimport testtools.testcase
39fc46f261SDan Klein
40fc46f261SDan Klein
41fc46f261SDan Kleintest_dir = os.path.dirname(os.path.abspath(__file__))
42fc46f261SDan Kleinparent_dir = os.path.dirname(test_dir)
43fc46f261SDan Kleinif not test_dir in sys.path:
44fc46f261SDan Klein    sys.path.insert(1, test_dir)
45fc46f261SDan Kleinif not parent_dir in sys.path:
46fc46f261SDan Klein    sys.path.insert(1, parent_dir)
47fc46f261SDan Klein
48fc46f261SDan Klein# Disable all but the most critical logging messages.
49fc46f261SDan Kleinlogging.disable(logging.CRITICAL)
50fc46f261SDan Klein
51fc46f261SDan Klein
52fc46f261SDan Kleindef get_function_signature(func):
53fc46f261SDan Klein    """ Get the function signature as a mapping of attributes.
54fc46f261SDan Klein
55fc46f261SDan Klein        :param func: The function object to interrogate.
56fc46f261SDan Klein        :return: A mapping of the components of a function signature.
57fc46f261SDan Klein
58fc46f261SDan Klein        The signature is constructed as a mapping:
59fc46f261SDan Klein
60fc46f261SDan Klein        * 'name': The function's defined name.
61fc46f261SDan Klein        * 'arg_count': The number of arguments expected by the function.
62fc46f261SDan Klein        * 'arg_names': A sequence of the argument names, as strings.
63fc46f261SDan Klein        * 'arg_defaults': A sequence of the default values for the arguments.
64fc46f261SDan Klein        * 'va_args': The name bound to remaining positional arguments.
65fc46f261SDan Klein        * 'va_kw_args': The name bound to remaining keyword arguments.
66fc46f261SDan Klein
67fc46f261SDan Klein        """
68fc46f261SDan Klein    try:
69fc46f261SDan Klein        # Python 3 function attributes.
70fc46f261SDan Klein        func_code = func.__code__
71fc46f261SDan Klein        func_defaults = func.__defaults__
72fc46f261SDan Klein    except AttributeError:
73fc46f261SDan Klein        # Python 2 function attributes.
74fc46f261SDan Klein        func_code = func.func_code
75fc46f261SDan Klein        func_defaults = func.func_defaults
76fc46f261SDan Klein
77fc46f261SDan Klein    arg_count = func_code.co_argcount
78fc46f261SDan Klein    arg_names = func_code.co_varnames[:arg_count]
79fc46f261SDan Klein
80fc46f261SDan Klein    arg_defaults = {}
81fc46f261SDan Klein    if func_defaults is not None:
82fc46f261SDan Klein        arg_defaults = dict(
83fc46f261SDan Klein                (name, value)
84fc46f261SDan Klein                for (name, value) in
85fc46f261SDan Klein                    zip(arg_names[::-1], func_defaults[::-1]))
86fc46f261SDan Klein
87fc46f261SDan Klein    signature = {
88fc46f261SDan Klein            'name': func.__name__,
89fc46f261SDan Klein            'arg_count': arg_count,
90fc46f261SDan Klein            'arg_names': arg_names,
91fc46f261SDan Klein            'arg_defaults': arg_defaults,
92fc46f261SDan Klein            }
93fc46f261SDan Klein
94fc46f261SDan Klein    non_pos_names = list(func_code.co_varnames[arg_count:])
95fc46f261SDan Klein    COLLECTS_ARBITRARY_POSITIONAL_ARGS = 0x04
96fc46f261SDan Klein    if func_code.co_flags & COLLECTS_ARBITRARY_POSITIONAL_ARGS:
97fc46f261SDan Klein        signature['var_args'] = non_pos_names.pop(0)
98fc46f261SDan Klein    COLLECTS_ARBITRARY_KEYWORD_ARGS = 0x08
99fc46f261SDan Klein    if func_code.co_flags & COLLECTS_ARBITRARY_KEYWORD_ARGS:
100fc46f261SDan Klein        signature['var_kw_args'] = non_pos_names.pop(0)
101fc46f261SDan Klein
102fc46f261SDan Klein    return signature
103fc46f261SDan Klein
104fc46f261SDan Klein
105fc46f261SDan Kleindef format_function_signature(func):
106fc46f261SDan Klein    """ Format the function signature as printable text.
107fc46f261SDan Klein
108fc46f261SDan Klein        :param func: The function object to interrogate.
109fc46f261SDan Klein        :return: A formatted text representation of the function signature.
110fc46f261SDan Klein
111fc46f261SDan Klein        The signature is rendered a text; for example::
112fc46f261SDan Klein
113fc46f261SDan Klein            foo(spam, eggs, ham=True, beans=None, *args, **kwargs)
114fc46f261SDan Klein
115fc46f261SDan Klein        """
116fc46f261SDan Klein    signature = get_function_signature(func)
117fc46f261SDan Klein
118fc46f261SDan Klein    args_text = []
119fc46f261SDan Klein    for arg_name in signature['arg_names']:
120fc46f261SDan Klein        if arg_name in signature['arg_defaults']:
121fc46f261SDan Klein            arg_text = "{name}={value!r}".format(
122fc46f261SDan Klein                    name=arg_name, value=signature['arg_defaults'][arg_name])
123fc46f261SDan Klein        else:
124fc46f261SDan Klein            arg_text = "{name}".format(
125fc46f261SDan Klein                    name=arg_name)
126fc46f261SDan Klein        args_text.append(arg_text)
127fc46f261SDan Klein    if 'var_args' in signature:
128fc46f261SDan Klein        args_text.append("*{var_args}".format(signature))
129fc46f261SDan Klein    if 'var_kw_args' in signature:
130fc46f261SDan Klein        args_text.append("**{var_kw_args}".format(signature))
131fc46f261SDan Klein    signature_args_text = ", ".join(args_text)
132fc46f261SDan Klein
133fc46f261SDan Klein    func_name = signature['name']
134fc46f261SDan Klein    signature_text = "{name}({args})".format(
135fc46f261SDan Klein            name=func_name, args=signature_args_text)
136fc46f261SDan Klein
137fc46f261SDan Klein    return signature_text
138fc46f261SDan Klein
139fc46f261SDan Klein
140fc46f261SDan Kleinclass TestCase(testtools.testcase.TestCase):
141fc46f261SDan Klein    """ Test case behaviour. """
142fc46f261SDan Klein
143fc46f261SDan Klein    def failUnlessOutputCheckerMatch(self, want, got, msg=None):
144fc46f261SDan Klein        """ Fail unless the specified string matches the expected.
145fc46f261SDan Klein
146fc46f261SDan Klein            :param want: The desired output pattern.
147fc46f261SDan Klein            :param got: The actual text to match.
148fc46f261SDan Klein            :param msg: A message to prefix on the failure message.
149fc46f261SDan Klein            :return: ``None``.
150fc46f261SDan Klein            :raises self.failureException: If the text does not match.
151fc46f261SDan Klein
152fc46f261SDan Klein            Fail the test unless ``want`` matches ``got``, as determined by
153fc46f261SDan Klein            a ``doctest.OutputChecker`` instance. This is not an equality
154fc46f261SDan Klein            check, but a pattern match according to the ``OutputChecker``
155fc46f261SDan Klein            rules.
156fc46f261SDan Klein
157fc46f261SDan Klein            """
158fc46f261SDan Klein        checker = doctest.OutputChecker()
159fc46f261SDan Klein        want = textwrap.dedent(want)
160fc46f261SDan Klein        source = ""
161fc46f261SDan Klein        example = doctest.Example(source, want)
162fc46f261SDan Klein        got = textwrap.dedent(got)
163fc46f261SDan Klein        checker_optionflags = functools.reduce(operator.or_, [
164fc46f261SDan Klein                doctest.ELLIPSIS,
165fc46f261SDan Klein                ])
166fc46f261SDan Klein        if not checker.check_output(want, got, checker_optionflags):
167fc46f261SDan Klein            if msg is None:
168fc46f261SDan Klein                diff = checker.output_difference(
169fc46f261SDan Klein                        example, got, checker_optionflags)
170fc46f261SDan Klein                msg = "\n".join([
171fc46f261SDan Klein                        "Output received did not match expected output",
172fc46f261SDan Klein                        "{diff}",
173fc46f261SDan Klein                        ]).format(
174fc46f261SDan Klein                            diff=diff)
175fc46f261SDan Klein            raise self.failureException(msg)
176fc46f261SDan Klein
177fc46f261SDan Klein    assertOutputCheckerMatch = failUnlessOutputCheckerMatch
178fc46f261SDan Klein
179fc46f261SDan Klein    def failUnlessFunctionInTraceback(self, traceback, function, msg=None):
180fc46f261SDan Klein        """ Fail if the function is not in the traceback.
181fc46f261SDan Klein
182fc46f261SDan Klein            :param traceback: The traceback object to interrogate.
183fc46f261SDan Klein            :param function: The function object to match.
184fc46f261SDan Klein            :param msg: A message to prefix on the failure message.
185fc46f261SDan Klein            :return: ``None``.
186fc46f261SDan Klein
187fc46f261SDan Klein            :raises self.failureException: If the function is not in the
188fc46f261SDan Klein                traceback.
189fc46f261SDan Klein
190fc46f261SDan Klein            Fail the test if the function ``function`` is not at any of the
191fc46f261SDan Klein            levels in the traceback object ``traceback``.
192fc46f261SDan Klein
193fc46f261SDan Klein            """
194fc46f261SDan Klein        func_in_traceback = False
195fc46f261SDan Klein        expected_code = function.func_code
196fc46f261SDan Klein        current_traceback = traceback
197fc46f261SDan Klein        while current_traceback is not None:
198fc46f261SDan Klein            if expected_code is current_traceback.tb_frame.f_code:
199fc46f261SDan Klein                func_in_traceback = True
200fc46f261SDan Klein                break
201fc46f261SDan Klein            current_traceback = current_traceback.tb_next
202fc46f261SDan Klein
203fc46f261SDan Klein        if not func_in_traceback:
204fc46f261SDan Klein            if msg is None:
205fc46f261SDan Klein                msg = (
206fc46f261SDan Klein                        "Traceback did not lead to original function"
207fc46f261SDan Klein                        " {function}"
208fc46f261SDan Klein                        ).format(
209fc46f261SDan Klein                            function=function)
210fc46f261SDan Klein            raise self.failureException(msg)
211fc46f261SDan Klein
212fc46f261SDan Klein    assertFunctionInTraceback = failUnlessFunctionInTraceback
213fc46f261SDan Klein
214fc46f261SDan Klein    def failUnlessFunctionSignatureMatch(self, first, second, msg=None):
215fc46f261SDan Klein        """ Fail if the function signatures do not match.
216fc46f261SDan Klein
217fc46f261SDan Klein            :param first: The first function to compare.
218fc46f261SDan Klein            :param second: The second function to compare.
219fc46f261SDan Klein            :param msg: A message to prefix to the failure message.
220fc46f261SDan Klein            :return: ``None``.
221fc46f261SDan Klein
222fc46f261SDan Klein            :raises self.failureException: If the function signatures do
223fc46f261SDan Klein                not match.
224fc46f261SDan Klein
225fc46f261SDan Klein            Fail the test if the function signature does not match between
226fc46f261SDan Klein            the ``first`` function and the ``second`` function.
227fc46f261SDan Klein
228fc46f261SDan Klein            The function signature includes:
229fc46f261SDan Klein
230fc46f261SDan Klein            * function name,
231fc46f261SDan Klein
232fc46f261SDan Klein            * count of named parameters,
233fc46f261SDan Klein
234fc46f261SDan Klein            * sequence of named parameters,
235fc46f261SDan Klein
236fc46f261SDan Klein            * default values of named parameters,
237fc46f261SDan Klein
238fc46f261SDan Klein            * collector for arbitrary positional arguments,
239fc46f261SDan Klein
240fc46f261SDan Klein            * collector for arbitrary keyword arguments.
241fc46f261SDan Klein
242fc46f261SDan Klein            """
243fc46f261SDan Klein        first_signature = get_function_signature(first)
244fc46f261SDan Klein        second_signature = get_function_signature(second)
245fc46f261SDan Klein
246fc46f261SDan Klein        if first_signature != second_signature:
247fc46f261SDan Klein            if msg is None:
248fc46f261SDan Klein                first_signature_text = format_function_signature(first)
249fc46f261SDan Klein                second_signature_text = format_function_signature(second)
250fc46f261SDan Klein                msg = (textwrap.dedent("""\
251fc46f261SDan Klein                        Function signatures do not match:
252fc46f261SDan Klein                            {first!r} != {second!r}
253fc46f261SDan Klein                        Expected:
254fc46f261SDan Klein                            {first_text}
255fc46f261SDan Klein                        Got:
256fc46f261SDan Klein                            {second_text}""")
257fc46f261SDan Klein                        ).format(
258fc46f261SDan Klein                            first=first_signature,
259fc46f261SDan Klein                            first_text=first_signature_text,
260fc46f261SDan Klein                            second=second_signature,
261fc46f261SDan Klein                            second_text=second_signature_text,
262fc46f261SDan Klein                            )
263fc46f261SDan Klein            raise self.failureException(msg)
264fc46f261SDan Klein
265fc46f261SDan Klein    assertFunctionSignatureMatch = failUnlessFunctionSignatureMatch
266fc46f261SDan Klein
267fc46f261SDan Klein
268fc46f261SDan Kleinclass TestCaseWithScenarios(testscenarios.WithScenarios, TestCase):
269fc46f261SDan Klein    """ Test cases run per scenario. """
270fc46f261SDan Klein
271fc46f261SDan Klein
272fc46f261SDan Kleinclass Exception_TestCase(TestCaseWithScenarios):
273fc46f261SDan Klein    """ Test cases for exception classes. """
274fc46f261SDan Klein
275fc46f261SDan Klein    def test_exception_instance(self):
276fc46f261SDan Klein        """ Exception instance should be created. """
277fc46f261SDan Klein        self.assertIsNot(self.instance, None)
278fc46f261SDan Klein
279fc46f261SDan Klein    def test_exception_types(self):
280fc46f261SDan Klein        """ Exception instance should match expected types. """
281fc46f261SDan Klein        for match_type in self.types:
282fc46f261SDan Klein            self.assertIsInstance(self.instance, match_type)
283fc46f261SDan Klein
284fc46f261SDan Klein
285fc46f261SDan Kleindef make_exception_scenarios(scenarios):
286fc46f261SDan Klein    """ Make test scenarios for exception classes.
287fc46f261SDan Klein
288fc46f261SDan Klein        :param scenarios: Sequence of scenarios.
289fc46f261SDan Klein        :return: List of scenarios with additional mapping entries.
290fc46f261SDan Klein
291fc46f261SDan Klein        Use this with `testscenarios` to adapt `Exception_TestCase`_ for
292fc46f261SDan Klein        any exceptions that need testing.
293fc46f261SDan Klein
294fc46f261SDan Klein        Each scenario is a tuple (`name`, `map`) where `map` is a mapping
295fc46f261SDan Klein        of attributes to be applied to each test case. Attributes map must
296fc46f261SDan Klein        contain items for:
297fc46f261SDan Klein
298fc46f261SDan Klein            :key exc_type:
299fc46f261SDan Klein                The exception type to be tested.
300fc46f261SDan Klein            :key min_args:
301fc46f261SDan Klein                The minimum argument count for the exception instance
302fc46f261SDan Klein                initialiser.
303fc46f261SDan Klein            :key types:
304fc46f261SDan Klein                Sequence of types that should be superclasses of each
305fc46f261SDan Klein                instance of the exception type.
306fc46f261SDan Klein
307fc46f261SDan Klein        """
308fc46f261SDan Klein    updated_scenarios = deepcopy(scenarios)
309fc46f261SDan Klein    for (name, scenario) in updated_scenarios:
310fc46f261SDan Klein        args = (None,) * scenario['min_args']
311fc46f261SDan Klein        scenario['args'] = args
312fc46f261SDan Klein        instance = scenario['exc_type'](*args)
313fc46f261SDan Klein        scenario['instance'] = instance
314fc46f261SDan Klein
315fc46f261SDan Klein    return updated_scenarios
316fc46f261SDan Klein
317fc46f261SDan Klein
318fc46f261SDan Klein# Local variables:
319fc46f261SDan Klein# coding: utf-8
320fc46f261SDan Klein# mode: python
321fc46f261SDan Klein# End:
322fc46f261SDan Klein# vim: fileencoding=utf-8 filetype=python :
323