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