2 # -*- coding: utf-8 -*-
3 # Extends unittest with support for pytest-style fixtures.
5 # Copyright (c) 2018 Peter Wu <peter@lekensteyn.nl>
7 # SPDX-License-Identifier: (GPL-2.0-or-later OR MIT)
16 _use_native_pytest = False
20 global _use_native_pytest, pytest
23 _use_native_pytest = True
26 def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
28 When running under pytest, this is the same as the pytest.fixture decorator.
29 See https://docs.pytest.org/en/latest/reference.html#pytest-fixture
31 if _use_native_pytest:
32 # XXX sorting of fixtures based on scope does not work, see
33 # https://github.com/pytest-dev/pytest/issues/4143#issuecomment-431794076
34 # When ran under pytest, use native functionality.
35 return pytest.fixture(scope, params, autouse, ids, name)
36 init_fallback_fixtures_once()
37 return _fallback.fixture(scope, params, autouse, ids, name)
40 def _fixture_wrapper(test_fn, params):
41 @functools.wraps(test_fn)
43 if not _use_native_pytest:
44 self._fixture_request.function = getattr(self, test_fn.__name__)
45 self._fixture_request.fillfixtures(params)
46 fixtures = [self._fixture_request.getfixturevalue(n) for n in params]
47 test_fn(self, *fixtures)
51 def uses_fixtures(cls):
52 """Enables use of fixtures within test methods of unittest.TestCase."""
53 assert issubclass(cls, unittest.TestCase)
56 func = getattr(cls, name)
57 if not name.startswith('test') or not callable(func):
59 params = inspect.getfullargspec(func).args[1:]
60 # Unconditionally overwrite methods in case usefixtures marks exist.
61 setattr(cls, name, _fixture_wrapper(func, params))
63 if _use_native_pytest:
64 # Make request object to _fixture_wrapper
65 @pytest.fixture(autouse=True)
66 def __inject_request(self, request):
67 self._fixture_request = request
68 cls.__inject_request = __inject_request
70 _patch_unittest_testcase_class(cls)
75 def mark_usefixtures(*args):
76 """Add the given fixtures to every test method."""
77 if _use_native_pytest:
78 return pytest.mark.usefixtures(*args)
81 cls._fixtures_prepend = list(args)
86 # Begin fallback functionality when pytest is not available.
88 # - session-scoped fixtures (for cmd_tshark)
89 # - function-scoped fixtures (for tmpfile)
90 # - teardown (via yield keyword in fixture)
91 # - sorting of scopes (session before function)
92 # - fixtures that depend on other fixtures (requires sorting)
93 # - marking classes with @pytest.mark.usefixtures("fixture")
94 # Not supported (yet) due to lack of need for it:
96 # - parameterized fixtures (@pytest.fixture(params=...))
97 # - class-scoped fixtures
98 # - (overriding) fixtures on various levels (e.g. conftest, module, class)
101 class _FixtureSpec(object):
102 def __init__(self, name, scope, func):
106 self.params = inspect.getfullargspec(func).args
107 if inspect.ismethod(self.params):
108 self.params = self.params[1:] # skip self
111 return '<_FixtureSpec name=%s scope=%s params=%r>' % \
112 (self.name, self.scope, self.params)
115 class _FixturesManager(object):
116 '''Records collected fixtures when pytest is unavailable.'''
118 # supported scopes, in execution order.
119 SCOPES = ('session', 'function')
121 def _add_fixture(self, scope, autouse, name, func):
122 name = name or func.__name__
123 if name in self.fixtures:
124 raise NotImplementedError('overriding fixtures is not supported')
125 self.fixtures[name] = _FixtureSpec(name, scope, func)
128 def fixture(self, scope, params, autouse, ids, name):
130 raise NotImplementedError('params is not supported')
132 raise NotImplementedError('ids is not supported')
134 raise NotImplementedError('autouse is not supported yet')
137 # used as decorator, pass through the original function
138 self._add_fixture('function', autouse, name, scope)
140 assert scope in self.SCOPES, 'unsupported scope'
141 # invoked with arguments, should return a decorator
142 return lambda func: self._add_fixture(scope, autouse, name, func)
144 def lookup(self, name):
145 return self.fixtures.get(name)
147 def resolve_fixtures(self, fixtures):
148 '''Find all dependencies for the requested list of fixtures.'''
149 unresolved = fixtures.copy()
150 resolved_keys, resolved = [], []
152 param = unresolved.pop(0)
153 if param in resolved:
155 spec = self.lookup(param)
157 if param == 'request':
159 raise RuntimeError("Fixture '%s' not found" % (param,))
160 unresolved += spec.params
161 resolved_keys.append(param)
162 resolved.append(spec)
163 # Return fixtures, sorted by their scope
164 resolved.sort(key=lambda spec: self.SCOPES.index(spec.scope))
168 class _ExecutionScope(object):
169 '''Store execution/teardown state for a scope.'''
171 def __init__(self, scope, parent):
177 def _find_scope(self, scope):
179 while context.scope != scope:
180 context = context.parent
183 def execute(self, spec, test_fn):
184 '''Execute a fixture and cache the result.'''
185 context = self._find_scope(spec.scope)
186 if spec.name in context.cache:
189 value, cleanup = self._execute_one(spec, test_fn)
192 value, cleanup, exc = None, None, sys.exc_info()[1]
193 context.cache[spec.name] = value, exc
195 context.finalizers.append(cleanup)
199 def cached_result(self, spec):
200 '''Obtain the cached result for a previously executed fixture.'''
201 entry = self._find_scope(spec.scope).cache.get(spec.name)
209 def _execute_one(self, spec, test_fn):
210 # A fixture can only execute in the same or earlier scopes
211 context_scope_index = _FixturesManager.SCOPES.index(self.scope)
212 fixture_scope_index = _FixturesManager.SCOPES.index(spec.scope)
213 assert fixture_scope_index <= context_scope_index
215 # Do not invoke destroy, it is taken care of by the main request.
216 subrequest = _FixtureRequest(self)
217 subrequest.function = test_fn
218 subrequest.fillfixtures(spec.params)
219 fixtures = (subrequest.getfixturevalue(n) for n in spec.params)
220 value = spec.func(*fixtures) # Execute fixture
222 value = spec.func() # Execute fixture
223 if not inspect.isgenerator(value):
226 @functools.wraps(value)
230 except StopIteration:
233 raise RuntimeError('%s yielded more than once!' % (spec.name,))
234 return next(value), cleanup
238 for cleanup in self.finalizers:
242 exceptions.append(sys.exc_info()[1])
244 self.finalizers.clear()
249 class _FixtureRequest(object):
251 Holds state during a single test execution. See
252 https://docs.pytest.org/en/latest/reference.html#request
255 def __init__(self, context):
256 self._context = context
257 self._fixtures_prepend = [] # fixtures added via usefixtures
258 # XXX is there any need for .module or .cls?
259 self.function = None # test function, set before execution.
261 def fillfixtures(self, params):
262 params = self._fixtures_prepend + params
263 specs = _fallback.resolve_fixtures(params)
265 self._context.execute(spec, self.function)
267 def getfixturevalue(self, argname):
268 spec = _fallback.lookup(argname)
270 assert argname == 'request'
272 value, ok = self._context.cached_result(spec)
274 # If getfixturevalue is called directly from a setUp function, the
275 # fixture value might not have computed before, so evaluate it now.
276 # As the test function is not available, use None.
277 self._context.execute(spec, test_fn=None)
278 value, ok = self._context.cached_result(spec)
279 assert ok, 'Failed to execute fixture %s' % (spec,)
283 self._context.destroy()
285 def addfinalizer(self, finalizer):
286 self._context.finalizers.append(finalizer)
290 return self.function.__self__
294 '''The pytest config object associated with this request.'''
298 def _patch_unittest_testcase_class(cls):
300 Patch the setUp and tearDown methods of the unittest.TestCase such that the
301 fixtures are properly setup and destroyed.
305 assert _session_context, 'must call create_session() first!'
306 function_context = _ExecutionScope('function', _session_context)
307 req = _FixtureRequest(function_context)
308 req._fixtures_prepend = getattr(self, '_fixtures_prepend', [])
309 self._fixture_request = req
314 self._orig_tearDown()
316 self._fixture_request.destroy()
317 # Only the leaf test case class should be decorated!
318 assert not hasattr(cls, '_orig_setUp')
319 assert not hasattr(cls, '_orig_tearDown')
320 cls._orig_setUp, cls.setUp = cls.setUp, setUp
321 cls._orig_tearDown, cls.tearDown = cls.tearDown, tearDown
324 class _Config(object):
325 def __init__(self, args):
326 assert isinstance(args, argparse.Namespace)
329 def getoption(self, name, default):
330 '''Partial emulation for pytest Config.getoption.'''
331 name = name.lstrip('-').replace('-', '_')
332 return getattr(self.args, name, default)
336 _session_context = None
340 def init_fallback_fixtures_once():
342 assert not _use_native_pytest
345 _fallback = _FixturesManager()
346 # Register standard fixtures here as needed
349 def create_session(args=None):
350 '''Start a test session where args is from argparse.'''
351 global _session_context, _config
352 assert not _use_native_pytest
353 _session_context = _ExecutionScope('session', None)
355 args = argparse.Namespace()
356 _config = _Config(args)
359 def destroy_session():
360 global _session_context
361 assert not _use_native_pytest
362 _session_context = None
366 '''Skip the executing test with the given message.'''
367 if _use_native_pytest:
370 raise unittest.SkipTest(msg)