Merge pull request #1647 from sallner/features

Add new options to report fixture setup and teardown
This commit is contained in:
holger krekel 2016-06-25 16:38:37 +02:00 committed by GitHub
commit 13a188fe37
11 changed files with 405 additions and 28 deletions

View File

@ -31,6 +31,7 @@ Christopher Gilling
Daniel Grana Daniel Grana
Daniel Hahler Daniel Hahler
Daniel Nuri Daniel Nuri
Danielle Jenkins
Dave Hunt Dave Hunt
David Díaz-Barquero David Díaz-Barquero
David Mohr David Mohr
@ -96,6 +97,7 @@ Ross Lawley
Russel Winder Russel Winder
Ryan Wooden Ryan Wooden
Samuele Pedroni Samuele Pedroni
Steffen Allner
Stephan Obermann Stephan Obermann
Tareq Alayan Tareq Alayan
Simon Gomizelj Simon Gomizelj
@ -104,6 +106,7 @@ Stefan Farmbauer
Thomas Grainger Thomas Grainger
Tom Viner Tom Viner
Trevor Bekolay Trevor Bekolay
Vasily Kuznetsov
Wouter van Ackooy Wouter van Ackooy
Bernard Pratz Bernard Pratz
Stefan Zimmermann Stefan Zimmermann

View File

@ -68,6 +68,18 @@
`#1629`_. Thanks `@obestwalter`_ and `@davehunt`_ for the complete PR `#1629`_. Thanks `@obestwalter`_ and `@davehunt`_ for the complete PR
(`#1633`_) (`#1633`_)
* New cli flags: (1) ``--setup-plan`` performs normal collection and reports
the potential setup and teardown, does not execute any fixtures and tests (2)
``--setup-only`` performs normal collection, executes setup and teardown of
fixtures and reports them. Thanks `@d6e`_, `@kvas-it`_, `@sallner`_
and `@omarkohl`_ for the PR.
* Added two new hooks: ``pytest_fixture_setup`` which executes the fixture
setup and ``pytest_fixture_post_finalizer`` which is called after the fixture's
finalizer and has access to the fixture's result cache.
Thanks `@d6e`_, `@sallner`_
**Changes** **Changes**
* Fixtures marked with ``@pytest.fixture`` can now use ``yield`` statements exactly like * Fixtures marked with ``@pytest.fixture`` can now use ``yield`` statements exactly like
@ -170,6 +182,9 @@
.. _@olegpidsadnyi: https://github.com/olegpidsadnyi .. _@olegpidsadnyi: https://github.com/olegpidsadnyi
.. _@obestwalter: https://github.com/obestwalter .. _@obestwalter: https://github.com/obestwalter
.. _@davehunt: https://github.com/davehunt .. _@davehunt: https://github.com/davehunt
.. _@sallner: https://github.com/sallner
.. _@d6e: https://github.com/d6e
.. _@kvas-it: https://github.com/kvas-it
.. _#1421: https://github.com/pytest-dev/pytest/issues/1421 .. _#1421: https://github.com/pytest-dev/pytest/issues/1421
.. _#1426: https://github.com/pytest-dev/pytest/issues/1426 .. _#1426: https://github.com/pytest-dev/pytest/issues/1426

View File

@ -65,7 +65,7 @@ _preinit = []
default_plugins = ( default_plugins = (
"mark main terminal runner python pdb unittest capture skipping " "mark main terminal runner python pdb unittest capture skipping "
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript "
"junitxml resultlog doctest cacheprovider").split() "junitxml resultlog doctest cacheprovider setuponly setupplan").split()
builtin_plugins = set(default_plugins) builtin_plugins = set(default_plugins)
builtin_plugins.add("pytester") builtin_plugins.add("pytester")

View File

@ -218,6 +218,19 @@ def pytest_runtest_logreport(report):
""" process a test setup/call/teardown report relating to """ process a test setup/call/teardown report relating to
the respective phase of executing a test. """ the respective phase of executing a test. """
# -------------------------------------------------------------------------
# Fixture related hooks
# -------------------------------------------------------------------------
@hookspec(firstresult=True)
def pytest_fixture_setup(fixturedef, request):
""" performs fixture setup execution. """
def pytest_fixture_post_finalizer(fixturedef):
""" called after fixture teardown, but before the cache is cleared so
the fixture result cache ``fixturedef.cached_result`` can
still be accessed."""
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# test session related hooks # test session related hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------

View File

@ -2481,6 +2481,8 @@ class FixtureDef:
func = self._finalizer.pop() func = self._finalizer.pop()
func() func()
finally: finally:
ihook = self._fixturemanager.session.ihook
ihook.pytest_fixture_post_finalizer(fixturedef=self)
# even if finalization fails, we invalidate # even if finalization fails, we invalidate
# the cached fixture value # the cached fixture value
if hasattr(self, "cached_result"): if hasattr(self, "cached_result"):
@ -2489,12 +2491,8 @@ class FixtureDef:
def execute(self, request): def execute(self, request):
# get required arguments and register our own finish() # get required arguments and register our own finish()
# with their finalization # with their finalization
kwargs = {}
for argname in self.argnames: for argname in self.argnames:
fixturedef = request._get_active_fixturedef(argname) fixturedef = request._get_active_fixturedef(argname)
result, arg_cache_key, exc = fixturedef.cached_result
request._check_scope(argname, request.scope, fixturedef.scope)
kwargs[argname] = result
if argname != "request": if argname != "request":
fixturedef.addfinalizer(self.finish) fixturedef.addfinalizer(self.finish)
@ -2512,33 +2510,44 @@ class FixtureDef:
self.finish() self.finish()
assert not hasattr(self, "cached_result") assert not hasattr(self, "cached_result")
fixturefunc = self.func ihook = self._fixturemanager.session.ihook
ihook.pytest_fixture_setup(fixturedef=self, request=request)
if self.unittest:
if request.instance is not None:
# bind the unbound method to the TestCase instance
fixturefunc = self.func.__get__(request.instance)
else:
# the fixture function needs to be bound to the actual
# request.instance so that code working with "self" behaves
# as expected.
if request.instance is not None:
fixturefunc = getimfunc(self.func)
if fixturefunc != self.func:
fixturefunc = fixturefunc.__get__(request.instance)
try:
result = call_fixture_func(fixturefunc, request, kwargs)
except Exception:
self.cached_result = (None, my_cache_key, sys.exc_info())
raise
self.cached_result = (result, my_cache_key, None)
return result
def __repr__(self): def __repr__(self):
return ("<FixtureDef name=%r scope=%r baseid=%r >" % return ("<FixtureDef name=%r scope=%r baseid=%r >" %
(self.argname, self.scope, self.baseid)) (self.argname, self.scope, self.baseid))
def pytest_fixture_setup(fixturedef, request):
""" Execution of fixture setup. """
kwargs = {}
for argname in fixturedef.argnames:
fixdef = request._get_active_fixturedef(argname)
result, arg_cache_key, exc = fixdef.cached_result
request._check_scope(argname, request.scope, fixdef.scope)
kwargs[argname] = result
fixturefunc = fixturedef.func
if fixturedef.unittest:
if request.instance is not None:
# bind the unbound method to the TestCase instance
fixturefunc = fixturedef.func.__get__(request.instance)
else:
# the fixture function needs to be bound to the actual
# request.instance so that code working with "fixturedef" behaves
# as expected.
if request.instance is not None:
fixturefunc = getimfunc(fixturedef.func)
if fixturefunc != fixturedef.func:
fixturefunc = fixturefunc.__get__(request.instance)
my_cache_key = request.param_index
try:
result = call_fixture_func(fixturefunc, request, kwargs)
except Exception:
fixturedef.cached_result = (None, my_cache_key, sys.exc_info())
raise
fixturedef.cached_result = (result, my_cache_key, None)
return result
def num_mock_patch_args(function): def num_mock_patch_args(function):
""" return number of arguments used up by mock arguments (if any) """ """ return number of arguments used up by mock arguments (if any) """
patchings = getattr(function, "patchings", None) patchings = getattr(function, "patchings", None)

View File

@ -73,6 +73,9 @@ def runtestprotocol(item, log=True, nextitem=None):
rep = call_and_report(item, "setup", log) rep = call_and_report(item, "setup", log)
reports = [rep] reports = [rep]
if rep.passed: if rep.passed:
if item.config.option.setuponly or item.config.option.setupplan:
show_test_item(item)
else:
reports.append(call_and_report(item, "call", log)) reports.append(call_and_report(item, "call", log))
reports.append(call_and_report(item, "teardown", log, reports.append(call_and_report(item, "teardown", log,
nextitem=nextitem)) nextitem=nextitem))
@ -83,6 +86,16 @@ def runtestprotocol(item, log=True, nextitem=None):
item.funcargs = None item.funcargs = None
return reports return reports
def show_test_item(item):
"""Show test function, parameters and the fixtures of the test item."""
tw = item.config.get_terminal_writer()
tw.line()
tw.write(' ' * 8)
tw.write(item._nodeid)
used_fixtures = sorted(item._fixtureinfo.name2fixturedefs.keys())
if used_fixtures:
tw.write(' (fixtures used: {0})'.format(', '.join(used_fixtures)))
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
item.session._setupstate.prepare(item) item.session._setupstate.prepare(item)

59
_pytest/setuponly.py Normal file
View File

@ -0,0 +1,59 @@
import pytest
import sys
def pytest_addoption(parser):
group = parser.getgroup("debugconfig")
group.addoption('--setuponly', '--setup-only', action="store_true",
help="only setup fixtures, don't execute the tests.")
@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(fixturedef, request):
yield
config = request.config
if config.option.setuponly:
if hasattr(request, 'param'):
# Save the fixture parameter so ._show_fixture_action() can
# display it now and during the teardown (in .finish()).
if fixturedef.ids:
if callable(fixturedef.ids):
fixturedef.cached_param = fixturedef.ids(request.param)
else:
fixturedef.cached_param = fixturedef.ids[request.param_index]
else:
fixturedef.cached_param = request.param
_show_fixture_action(fixturedef, 'SETUP')
def pytest_fixture_post_finalizer(fixturedef):
if hasattr(fixturedef, "cached_result"):
config = fixturedef._fixturemanager.config
if config.option.setuponly:
_show_fixture_action(fixturedef, 'TEARDOWN')
if hasattr(fixturedef, "cached_param"):
del fixturedef.cached_param
def _show_fixture_action(fixturedef, msg):
config = fixturedef._fixturemanager.config
capman = config.pluginmanager.getplugin('capturemanager')
if capman:
out, err = capman.suspendcapture()
tw = config.get_terminal_writer()
tw.line()
tw.write(' ' * 2 * fixturedef.scopenum)
tw.write('{step} {scope} {fixture}'.format(
step=msg.ljust(8), # align the output to TEARDOWN
scope=fixturedef.scope[0].upper(),
fixture=fixturedef.argname))
if msg == 'SETUP':
deps = sorted(arg for arg in fixturedef.argnames if arg != 'request')
if deps:
tw.write(' (fixtures used: {0})'.format(', '.join(deps)))
if hasattr(fixturedef, 'cached_param'):
tw.write('[{0}]'.format(fixturedef.cached_param))
if capman:
capman.resumecapture()
sys.stdout.write(out)
sys.stderr.write(err)

19
_pytest/setupplan.py Normal file
View File

@ -0,0 +1,19 @@
import pytest
def pytest_addoption(parser):
group = parser.getgroup("debugconfig")
group.addoption('--setupplan', '--setup-plan', action="store_true",
help="show what fixtures and tests would be executed but don't"
" execute anything.")
@pytest.hookimpl(tryfirst=True)
def pytest_fixture_setup(fixturedef, request):
# Will return a dummy fixture if the setuponly option is provided.
if request.config.option.setupplan:
fixturedef.cached_result = (None, None, None)
return fixturedef.cached_result
@pytest.hookimpl(tryfirst=True)
def pytest_cmdline_main(config):
if config.option.setupplan:
config.option.setuponly = True

View File

@ -498,6 +498,8 @@ Session related reporting hooks:
.. autofunction:: pytest_report_header .. autofunction:: pytest_report_header
.. autofunction:: pytest_report_teststatus .. autofunction:: pytest_report_teststatus
.. autofunction:: pytest_terminal_summary .. autofunction:: pytest_terminal_summary
.. autofunction:: pytest_fixture_setup
.. autofunction:: pytest_fixture_post_finalizer
And here is the central hook for reporting about And here is the central hook for reporting about
test execution: test execution:
@ -554,6 +556,10 @@ Reference of objects involved in hooks
:members: :members:
:show-inheritance: :show-inheritance:
.. autoclass:: _pytest.python.FixtureDef()
:members:
:show-inheritance:
.. autoclass:: _pytest.runner.CallInfo() .. autoclass:: _pytest.runner.CallInfo()
:members: :members:

View File

@ -0,0 +1,221 @@
import pytest
@pytest.fixture(params=['--setup-only', '--setup-plan'], scope='module')
def mode(request):
return request.param
def test_show_only_active_fixtures(testdir, mode):
p = testdir.makepyfile('''
import pytest
@pytest.fixture
def _arg0():
"""hidden arg0 fixture"""
@pytest.fixture
def arg1():
"""arg1 docstring"""
def test_arg1(arg1):
pass
''')
result = testdir.runpytest(mode, p)
assert result.ret == 0
result.stdout.fnmatch_lines([
'*SETUP F arg1*',
'*test_arg1 (fixtures used: arg1)',
'*TEARDOWN F arg1*',
])
assert "_arg0" not in result.stdout.str()
def test_show_different_scopes(testdir, mode):
p = testdir.makepyfile('''
import pytest
@pytest.fixture
def arg_function():
"""function scoped fixture"""
@pytest.fixture(scope='session')
def arg_session():
"""session scoped fixture"""
def test_arg1(arg_session, arg_function):
pass
''')
result = testdir.runpytest(mode, p)
assert result.ret == 0
result.stdout.fnmatch_lines([
'SETUP S arg_session*',
'*SETUP F arg_function*',
'*test_arg1 (fixtures used: arg_function, arg_session)',
'*TEARDOWN F arg_function*',
'TEARDOWN S arg_session*',
])
def test_show_nested_fixtures(testdir, mode):
testdir.makeconftest('''
import pytest
@pytest.fixture(scope='session')
def arg_same():
"""session scoped fixture"""
''')
p = testdir.makepyfile('''
import pytest
@pytest.fixture(scope='function')
def arg_same(arg_same):
"""function scoped fixture"""
def test_arg1(arg_same):
pass
''')
result = testdir.runpytest(mode, p)
assert result.ret == 0
result.stdout.fnmatch_lines([
'SETUP S arg_same*',
'*SETUP F arg_same (fixtures used: arg_same)*',
'*test_arg1 (fixtures used: arg_same)',
'*TEARDOWN F arg_same*',
'TEARDOWN S arg_same*',
])
def test_show_fixtures_with_autouse(testdir, mode):
p = testdir.makepyfile('''
import pytest
@pytest.fixture
def arg_function():
"""function scoped fixture"""
@pytest.fixture(scope='session', autouse=True)
def arg_session():
"""session scoped fixture"""
def test_arg1(arg_function):
pass
''')
result = testdir.runpytest(mode, p)
assert result.ret == 0
result.stdout.fnmatch_lines([
'SETUP S arg_session*',
'*SETUP F arg_function*',
'*test_arg1 (fixtures used: arg_function, arg_session)',
])
def test_show_fixtures_with_parameters(testdir, mode):
testdir.makeconftest('''
import pytest
@pytest.fixture(scope='session', params=['foo', 'bar'])
def arg_same():
"""session scoped fixture"""
''')
p = testdir.makepyfile('''
import pytest
@pytest.fixture(scope='function')
def arg_other(arg_same):
"""function scoped fixture"""
def test_arg1(arg_other):
pass
''')
result = testdir.runpytest(mode, p)
assert result.ret == 0
result.stdout.fnmatch_lines([
'SETUP S arg_same?foo?',
'TEARDOWN S arg_same?foo?',
'SETUP S arg_same?bar?',
'TEARDOWN S arg_same?bar?',
])
def test_show_fixtures_with_parameter_ids(testdir, mode):
testdir.makeconftest('''
import pytest
@pytest.fixture(
scope='session', params=['foo', 'bar'], ids=['spam', 'ham'])
def arg_same():
"""session scoped fixture"""
''')
p = testdir.makepyfile('''
import pytest
@pytest.fixture(scope='function')
def arg_other(arg_same):
"""function scoped fixture"""
def test_arg1(arg_other):
pass
''')
result = testdir.runpytest(mode, p)
assert result.ret == 0
result.stdout.fnmatch_lines([
'SETUP S arg_same?spam?',
'SETUP S arg_same?ham?',
])
def test_show_fixtures_with_parameter_ids_function(testdir, mode):
p = testdir.makepyfile('''
import pytest
@pytest.fixture(params=['foo', 'bar'], ids=lambda p: p.upper())
def foobar():
pass
def test_foobar(foobar):
pass
''')
result = testdir.runpytest(mode, p)
assert result.ret == 0
result.stdout.fnmatch_lines([
'*SETUP F foobar?FOO?',
'*SETUP F foobar?BAR?',
])
def test_dynamic_fixture_request(testdir):
p = testdir.makepyfile('''
import pytest
@pytest.fixture()
def dynamically_requested_fixture():
pass
@pytest.fixture()
def dependent_fixture(request):
request.getfuncargvalue('dynamically_requested_fixture')
def test_dyn(dependent_fixture):
pass
''')
result = testdir.runpytest('--setup-only', p)
assert result.ret == 0
result.stdout.fnmatch_lines([
'*SETUP F dynamically_requested_fixture',
'*TEARDOWN F dynamically_requested_fixture'
])
def test_capturing(testdir):
p = testdir.makepyfile('''
import pytest, sys
@pytest.fixture()
def one():
sys.stdout.write('this should be captured')
sys.stderr.write('this should also be captured')
@pytest.fixture()
def two(one):
assert 0
def test_capturing(two):
pass
''')
result = testdir.runpytest('--setup-only', p)
result.stdout.fnmatch_lines([
'this should be captured',
'this should also be captured'
])

View File

@ -0,0 +1,19 @@
def test_show_fixtures_and_test(testdir):
""" Verifies that fixtures are not executed. """
p = testdir.makepyfile('''
import pytest
@pytest.fixture
def arg():
assert False
def test_arg(arg):
assert False
''')
result = testdir.runpytest("--setup-plan", p)
assert result.ret == 0
result.stdout.fnmatch_lines([
'*SETUP F arg*',
'*test_arg (fixtures used: arg)',
'*TEARDOWN F arg*',
])