Skip to content

Commit 13a188f

Browse files
authored
Merge pull request #1647 from sallner/features
Add new options to report fixture setup and teardown
2 parents 2af3a7a + 32ca5cd commit 13a188f

File tree

11 files changed

+405
-28
lines changed

11 files changed

+405
-28
lines changed

AUTHORS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Christopher Gilling
3131
Daniel Grana
3232
Daniel Hahler
3333
Daniel Nuri
34+
Danielle Jenkins
3435
Dave Hunt
3536
David Díaz-Barquero
3637
David Mohr
@@ -96,6 +97,7 @@ Ross Lawley
9697
Russel Winder
9798
Ryan Wooden
9899
Samuele Pedroni
100+
Steffen Allner
99101
Stephan Obermann
100102
Tareq Alayan
101103
Simon Gomizelj
@@ -104,6 +106,7 @@ Stefan Farmbauer
104106
Thomas Grainger
105107
Tom Viner
106108
Trevor Bekolay
109+
Vasily Kuznetsov
107110
Wouter van Ackooy
108111
Bernard Pratz
109112
Stefan Zimmermann

CHANGELOG.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@
6868
`#1629`_. Thanks `@obestwalter`_ and `@davehunt`_ for the complete PR
6969
(`#1633`_)
7070

71+
* New cli flags: (1) ``--setup-plan`` performs normal collection and reports
72+
the potential setup and teardown, does not execute any fixtures and tests (2)
73+
``--setup-only`` performs normal collection, executes setup and teardown of
74+
fixtures and reports them. Thanks `@d6e`_, `@kvas-it`_, `@sallner`_
75+
and `@omarkohl`_ for the PR.
76+
77+
* Added two new hooks: ``pytest_fixture_setup`` which executes the fixture
78+
setup and ``pytest_fixture_post_finalizer`` which is called after the fixture's
79+
finalizer and has access to the fixture's result cache.
80+
Thanks `@d6e`_, `@sallner`_
81+
82+
7183
**Changes**
7284

7385
* Fixtures marked with ``@pytest.fixture`` can now use ``yield`` statements exactly like
@@ -170,6 +182,9 @@
170182
.. _@olegpidsadnyi: https://github.com/olegpidsadnyi
171183
.. _@obestwalter: https://github.com/obestwalter
172184
.. _@davehunt: https://github.com/davehunt
185+
.. _@sallner: https://github.com/sallner
186+
.. _@d6e: https://github.com/d6e
187+
.. _@kvas-it: https://github.com/kvas-it
173188

174189
.. _#1421: https://github.com/pytest-dev/pytest/issues/1421
175190
.. _#1426: https://github.com/pytest-dev/pytest/issues/1426

_pytest/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class UsageError(Exception):
6565
default_plugins = (
6666
"mark main terminal runner python pdb unittest capture skipping "
6767
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript "
68-
"junitxml resultlog doctest cacheprovider").split()
68+
"junitxml resultlog doctest cacheprovider setuponly setupplan").split()
6969

7070
builtin_plugins = set(default_plugins)
7171
builtin_plugins.add("pytester")

_pytest/hookspec.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,19 @@ def pytest_runtest_logreport(report):
218218
""" process a test setup/call/teardown report relating to
219219
the respective phase of executing a test. """
220220

221+
# -------------------------------------------------------------------------
222+
# Fixture related hooks
223+
# -------------------------------------------------------------------------
224+
225+
@hookspec(firstresult=True)
226+
def pytest_fixture_setup(fixturedef, request):
227+
""" performs fixture setup execution. """
228+
229+
def pytest_fixture_post_finalizer(fixturedef):
230+
""" called after fixture teardown, but before the cache is cleared so
231+
the fixture result cache ``fixturedef.cached_result`` can
232+
still be accessed."""
233+
221234
# -------------------------------------------------------------------------
222235
# test session related hooks
223236
# -------------------------------------------------------------------------

_pytest/python.py

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2481,6 +2481,8 @@ def finish(self):
24812481
func = self._finalizer.pop()
24822482
func()
24832483
finally:
2484+
ihook = self._fixturemanager.session.ihook
2485+
ihook.pytest_fixture_post_finalizer(fixturedef=self)
24842486
# even if finalization fails, we invalidate
24852487
# the cached fixture value
24862488
if hasattr(self, "cached_result"):
@@ -2489,12 +2491,8 @@ def finish(self):
24892491
def execute(self, request):
24902492
# get required arguments and register our own finish()
24912493
# with their finalization
2492-
kwargs = {}
24932494
for argname in self.argnames:
24942495
fixturedef = request._get_active_fixturedef(argname)
2495-
result, arg_cache_key, exc = fixturedef.cached_result
2496-
request._check_scope(argname, request.scope, fixturedef.scope)
2497-
kwargs[argname] = result
24982496
if argname != "request":
24992497
fixturedef.addfinalizer(self.finish)
25002498

@@ -2512,33 +2510,44 @@ def execute(self, request):
25122510
self.finish()
25132511
assert not hasattr(self, "cached_result")
25142512

2515-
fixturefunc = self.func
2516-
2517-
if self.unittest:
2518-
if request.instance is not None:
2519-
# bind the unbound method to the TestCase instance
2520-
fixturefunc = self.func.__get__(request.instance)
2521-
else:
2522-
# the fixture function needs to be bound to the actual
2523-
# request.instance so that code working with "self" behaves
2524-
# as expected.
2525-
if request.instance is not None:
2526-
fixturefunc = getimfunc(self.func)
2527-
if fixturefunc != self.func:
2528-
fixturefunc = fixturefunc.__get__(request.instance)
2529-
2530-
try:
2531-
result = call_fixture_func(fixturefunc, request, kwargs)
2532-
except Exception:
2533-
self.cached_result = (None, my_cache_key, sys.exc_info())
2534-
raise
2535-
self.cached_result = (result, my_cache_key, None)
2536-
return result
2513+
ihook = self._fixturemanager.session.ihook
2514+
ihook.pytest_fixture_setup(fixturedef=self, request=request)
25372515

25382516
def __repr__(self):
25392517
return ("<FixtureDef name=%r scope=%r baseid=%r >" %
25402518
(self.argname, self.scope, self.baseid))
25412519

2520+
def pytest_fixture_setup(fixturedef, request):
2521+
""" Execution of fixture setup. """
2522+
kwargs = {}
2523+
for argname in fixturedef.argnames:
2524+
fixdef = request._get_active_fixturedef(argname)
2525+
result, arg_cache_key, exc = fixdef.cached_result
2526+
request._check_scope(argname, request.scope, fixdef.scope)
2527+
kwargs[argname] = result
2528+
2529+
fixturefunc = fixturedef.func
2530+
if fixturedef.unittest:
2531+
if request.instance is not None:
2532+
# bind the unbound method to the TestCase instance
2533+
fixturefunc = fixturedef.func.__get__(request.instance)
2534+
else:
2535+
# the fixture function needs to be bound to the actual
2536+
# request.instance so that code working with "fixturedef" behaves
2537+
# as expected.
2538+
if request.instance is not None:
2539+
fixturefunc = getimfunc(fixturedef.func)
2540+
if fixturefunc != fixturedef.func:
2541+
fixturefunc = fixturefunc.__get__(request.instance)
2542+
my_cache_key = request.param_index
2543+
try:
2544+
result = call_fixture_func(fixturefunc, request, kwargs)
2545+
except Exception:
2546+
fixturedef.cached_result = (None, my_cache_key, sys.exc_info())
2547+
raise
2548+
fixturedef.cached_result = (result, my_cache_key, None)
2549+
return result
2550+
25422551
def num_mock_patch_args(function):
25432552
""" return number of arguments used up by mock arguments (if any) """
25442553
patchings = getattr(function, "patchings", None)

_pytest/runner.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ def runtestprotocol(item, log=True, nextitem=None):
7373
rep = call_and_report(item, "setup", log)
7474
reports = [rep]
7575
if rep.passed:
76-
reports.append(call_and_report(item, "call", log))
76+
if item.config.option.setuponly or item.config.option.setupplan:
77+
show_test_item(item)
78+
else:
79+
reports.append(call_and_report(item, "call", log))
7780
reports.append(call_and_report(item, "teardown", log,
7881
nextitem=nextitem))
7982
# after all teardown hooks have been called
@@ -83,6 +86,16 @@ def runtestprotocol(item, log=True, nextitem=None):
8386
item.funcargs = None
8487
return reports
8588

89+
def show_test_item(item):
90+
"""Show test function, parameters and the fixtures of the test item."""
91+
tw = item.config.get_terminal_writer()
92+
tw.line()
93+
tw.write(' ' * 8)
94+
tw.write(item._nodeid)
95+
used_fixtures = sorted(item._fixtureinfo.name2fixturedefs.keys())
96+
if used_fixtures:
97+
tw.write(' (fixtures used: {0})'.format(', '.join(used_fixtures)))
98+
8699
def pytest_runtest_setup(item):
87100
item.session._setupstate.prepare(item)
88101

_pytest/setuponly.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import pytest
2+
import sys
3+
4+
def pytest_addoption(parser):
5+
group = parser.getgroup("debugconfig")
6+
group.addoption('--setuponly', '--setup-only', action="store_true",
7+
help="only setup fixtures, don't execute the tests.")
8+
9+
@pytest.hookimpl(hookwrapper=True)
10+
def pytest_fixture_setup(fixturedef, request):
11+
yield
12+
config = request.config
13+
if config.option.setuponly:
14+
if hasattr(request, 'param'):
15+
# Save the fixture parameter so ._show_fixture_action() can
16+
# display it now and during the teardown (in .finish()).
17+
if fixturedef.ids:
18+
if callable(fixturedef.ids):
19+
fixturedef.cached_param = fixturedef.ids(request.param)
20+
else:
21+
fixturedef.cached_param = fixturedef.ids[request.param_index]
22+
else:
23+
fixturedef.cached_param = request.param
24+
_show_fixture_action(fixturedef, 'SETUP')
25+
26+
def pytest_fixture_post_finalizer(fixturedef):
27+
if hasattr(fixturedef, "cached_result"):
28+
config = fixturedef._fixturemanager.config
29+
if config.option.setuponly:
30+
_show_fixture_action(fixturedef, 'TEARDOWN')
31+
if hasattr(fixturedef, "cached_param"):
32+
del fixturedef.cached_param
33+
34+
def _show_fixture_action(fixturedef, msg):
35+
config = fixturedef._fixturemanager.config
36+
capman = config.pluginmanager.getplugin('capturemanager')
37+
if capman:
38+
out, err = capman.suspendcapture()
39+
40+
tw = config.get_terminal_writer()
41+
tw.line()
42+
tw.write(' ' * 2 * fixturedef.scopenum)
43+
tw.write('{step} {scope} {fixture}'.format(
44+
step=msg.ljust(8), # align the output to TEARDOWN
45+
scope=fixturedef.scope[0].upper(),
46+
fixture=fixturedef.argname))
47+
48+
if msg == 'SETUP':
49+
deps = sorted(arg for arg in fixturedef.argnames if arg != 'request')
50+
if deps:
51+
tw.write(' (fixtures used: {0})'.format(', '.join(deps)))
52+
53+
if hasattr(fixturedef, 'cached_param'):
54+
tw.write('[{0}]'.format(fixturedef.cached_param))
55+
56+
if capman:
57+
capman.resumecapture()
58+
sys.stdout.write(out)
59+
sys.stderr.write(err)

_pytest/setupplan.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
3+
def pytest_addoption(parser):
4+
group = parser.getgroup("debugconfig")
5+
group.addoption('--setupplan', '--setup-plan', action="store_true",
6+
help="show what fixtures and tests would be executed but don't"
7+
" execute anything.")
8+
9+
@pytest.hookimpl(tryfirst=True)
10+
def pytest_fixture_setup(fixturedef, request):
11+
# Will return a dummy fixture if the setuponly option is provided.
12+
if request.config.option.setupplan:
13+
fixturedef.cached_result = (None, None, None)
14+
return fixturedef.cached_result
15+
16+
@pytest.hookimpl(tryfirst=True)
17+
def pytest_cmdline_main(config):
18+
if config.option.setupplan:
19+
config.option.setuponly = True

doc/en/writing_plugins.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,8 @@ Session related reporting hooks:
498498
.. autofunction:: pytest_report_header
499499
.. autofunction:: pytest_report_teststatus
500500
.. autofunction:: pytest_terminal_summary
501+
.. autofunction:: pytest_fixture_setup
502+
.. autofunction:: pytest_fixture_post_finalizer
501503

502504
And here is the central hook for reporting about
503505
test execution:
@@ -554,6 +556,10 @@ Reference of objects involved in hooks
554556
:members:
555557
:show-inheritance:
556558

559+
.. autoclass:: _pytest.python.FixtureDef()
560+
:members:
561+
:show-inheritance:
562+
557563
.. autoclass:: _pytest.runner.CallInfo()
558564
:members:
559565

0 commit comments

Comments
 (0)