33
|
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
|
|
16 from __future__ import (absolute_import, unicode_literals)
|
|
17
|
|
18 import unittest
|
|
19 import doctest
|
|
20 import logging
|
|
21 import os
|
|
22 import sys
|
|
23 import operator
|
|
24 import textwrap
|
|
25 from copy import deepcopy
|
|
26 import functools
|
|
27
|
|
28 try:
|
|
29 # Python 2 has both ‘str’ (bytes) and ‘unicode’ (text).
|
|
30 basestring = basestring
|
|
31 unicode = unicode
|
|
32 except NameError:
|
|
33 # Python 3 names the Unicode data type ‘str’.
|
|
34 basestring = str
|
|
35 unicode = str
|
|
36
|
|
37 import testscenarios
|
|
38 import testtools.testcase
|
|
39
|
|
40
|
|
41 test_dir = os.path.dirname(os.path.abspath(__file__))
|
|
42 parent_dir = os.path.dirname(test_dir)
|
|
43 if not test_dir in sys.path:
|
|
44 sys.path.insert(1, test_dir)
|
|
45 if not parent_dir in sys.path:
|
|
46 sys.path.insert(1, parent_dir)
|
|
47
|
|
48 # Disable all but the most critical logging messages.
|
|
49 logging.disable(logging.CRITICAL)
|
|
50
|
|
51
|
|
52 def 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
|
|
105 def 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
|
|
140 class 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
|
|
268 class TestCaseWithScenarios(testscenarios.WithScenarios, TestCase):
|
|
269 """ Test cases run per scenario. """
|
|
270
|
|
271
|
|
272 class 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
|
|
285 def 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 :
|