Merge remote-tracking branch 'upstream/master' into fix-flake8-errors

This commit is contained in:
Bruno Oliveira 2017-07-19 17:57:30 -03:00
commit d44565f385
7 changed files with 174 additions and 287 deletions

View File

@ -105,6 +105,7 @@ Marcin Bachry
Mark Abramowitz
Markus Unterwaditzer
Martijn Faassen
Martin Altmayer
Martin K. Scherer
Martin Prusse
Mathieu Clabaut

View File

@ -1,254 +0,0 @@
Sorting per-resource
-----------------------------
for any given set of items:
- collect items per session-scoped parametrized funcarg
- re-order until items no parametrizations are mixed
examples:
test()
test1(s1)
test1(s2)
test2()
test3(s1)
test3(s2)
gets sorted to:
test()
test2()
test1(s1)
test3(s1)
test1(s2)
test3(s2)
the new @setup functions
--------------------------------------
Consider a given @setup-marked function::
@pytest.mark.setup(maxscope=SCOPE)
def mysetup(request, arg1, arg2, ...)
...
request.addfinalizer(fin)
...
then FUNCARGSET denotes the set of (arg1, arg2, ...) funcargs and
all of its dependent funcargs. The mysetup function will execute
for any matching test item once per scope.
The scope is determined as the minimum scope of all scopes of the args
in FUNCARGSET and the given "maxscope".
If mysetup has been called and no finalizers have been called it is
called "active".
Furthermore the following rules apply:
- if an arg value in FUNCARGSET is about to be torn down, the
mysetup-registered finalizers will execute as well.
- There will never be two active mysetup invocations.
Example 1, session scope::
@pytest.mark.funcarg(scope="session", params=[1,2])
def db(request):
request.addfinalizer(db_finalize)
@pytest.mark.setup
def mysetup(request, db):
request.addfinalizer(mysetup_finalize)
...
And a given test module:
def test_something():
...
def test_otherthing():
pass
Here is what happens::
db(request) executes with request.param == 1
mysetup(request, db) executes
test_something() executes
test_otherthing() executes
mysetup_finalize() executes
db_finalize() executes
db(request) executes with request.param == 2
mysetup(request, db) executes
test_something() executes
test_otherthing() executes
mysetup_finalize() executes
db_finalize() executes
Example 2, session/function scope::
@pytest.mark.funcarg(scope="session", params=[1,2])
def db(request):
request.addfinalizer(db_finalize)
@pytest.mark.setup(scope="function")
def mysetup(request, db):
...
request.addfinalizer(mysetup_finalize)
...
And a given test module:
def test_something():
...
def test_otherthing():
pass
Here is what happens::
db(request) executes with request.param == 1
mysetup(request, db) executes
test_something() executes
mysetup_finalize() executes
mysetup(request, db) executes
test_otherthing() executes
mysetup_finalize() executes
db_finalize() executes
db(request) executes with request.param == 2
mysetup(request, db) executes
test_something() executes
mysetup_finalize() executes
mysetup(request, db) executes
test_otherthing() executes
mysetup_finalize() executes
db_finalize() executes
Example 3 - funcargs session-mix
----------------------------------------
Similar with funcargs, an example::
@pytest.mark.funcarg(scope="session", params=[1,2])
def db(request):
request.addfinalizer(db_finalize)
@pytest.mark.funcarg(scope="function")
def table(request, db):
...
request.addfinalizer(table_finalize)
...
And a given test module:
def test_something(table):
...
def test_otherthing(table):
pass
def test_thirdthing():
pass
Here is what happens::
db(request) executes with param == 1
table(request, db)
test_something(table)
table_finalize()
table(request, db)
test_otherthing(table)
table_finalize()
db_finalize
db(request) executes with param == 2
table(request, db)
test_something(table)
table_finalize()
table(request, db)
test_otherthing(table)
table_finalize()
db_finalize
test_thirdthing()
Data structures
--------------------
pytest internally maintains a dict of active funcargs with cache, param,
finalizer, (scopeitem?) information:
active_funcargs = dict()
if a parametrized "db" is activated:
active_funcargs["db"] = FuncargInfo(dbvalue, paramindex,
FuncargFinalize(...), scopeitem)
if a test is torn down and the next test requires a differently
parametrized "db":
for argname in item.callspec.params:
if argname in active_funcargs:
funcarginfo = active_funcargs[argname]
if funcarginfo.param != item.callspec.params[argname]:
funcarginfo.callfinalizer()
del node2funcarg[funcarginfo.scopeitem]
del active_funcargs[argname]
nodes_to_be_torn_down = ...
for node in nodes_to_be_torn_down:
if node in node2funcarg:
argname = node2funcarg[node]
active_funcargs[argname].callfinalizer()
del node2funcarg[node]
del active_funcargs[argname]
if a test is setup requiring a "db" funcarg:
if "db" in active_funcargs:
return active_funcargs["db"][0]
funcarginfo = setup_funcarg()
active_funcargs["db"] = funcarginfo
node2funcarg[funcarginfo.scopeitem] = "db"
Implementation plan for resources
------------------------------------------
1. Revert FuncargRequest to the old form, unmerge item/request
(done)
2. make funcarg factories be discovered at collection time
3. Introduce funcarg marker
4. Introduce funcarg scope parameter
5. Introduce funcarg parametrize parameter
6. make setup functions be discovered at collection time
7. (Introduce a pytest_fixture_protocol/setup_funcargs hook)
methods and data structures
--------------------------------
A FuncarcManager holds all information about funcarg definitions
including parametrization and scope definitions. It implements
a pytest_generate_tests hook which performs parametrization as appropriate.
as a simple example, let's consider a tree where a test function requires
a "abc" funcarg and its factory defines it as parametrized and scoped
for Modules. When collections hits the function item, it creates
the metafunc object, and calls funcargdb.pytest_generate_tests(metafunc)
which looks up available funcarg factories and their scope and parametrization.
This information is equivalent to what can be provided today directly
at the function site and it should thus be relatively straight forward
to implement the additional way of defining parametrization/scoping.
conftest loading:
each funcarg-factory will populate the session.funcargmanager
When a test item is collected, it grows a dictionary
(funcargname2factorycalllist). A factory lookup is performed
for each required funcarg. The resulting factory call is stored
with the item. If a function is parametrized multiple items are
created with respective factory calls. Else if a factory is parametrized
multiple items and calls to the factory function are created as well.
At setup time, an item populates a funcargs mapping, mapping names
to values. If a value is funcarg factories are queried for a given item
test functions and setup functions are put in a class
which looks up required funcarg factories.

View File

@ -6,6 +6,7 @@ import inspect
import sys
import os
import collections
from textwrap import dedent
from itertools import count
import math
@ -994,14 +995,12 @@ def _show_fixtures_per_test(config, session):
funcargspec = argname
tw.line(funcargspec, green=True)
INDENT = ' {0}'
fixture_doc = fixture_def.func.__doc__
if fixture_doc:
for line in fixture_doc.strip().split('\n'):
tw.line(INDENT.format(line.strip()))
write_docstring(tw, fixture_doc)
else:
tw.line(INDENT.format('no docstring available'), red=True)
tw.line(' no docstring available', red=True)
def write_item(item):
name2fixturedefs = item._fixtureinfo.name2fixturedefs
@ -1075,13 +1074,28 @@ def _showfixtures_main(config, session):
loc = getlocation(fixturedef.func, curdir)
doc = fixturedef.func.__doc__ or ""
if doc:
for line in doc.strip().split("\n"):
tw.line(" " + line.strip())
write_docstring(tw, doc)
else:
tw.line(" %s: no docstring available" % (loc,),
red=True)
def write_docstring(tw, doc):
INDENT = " "
doc = doc.rstrip()
if "\n" in doc:
firstline, rest = doc.split("\n", 1)
else:
firstline, rest = doc, ""
if firstline.strip():
tw.line(INDENT + firstline.strip())
if rest:
for line in dedent(rest).split("\n"):
tw.write(INDENT + line + "\n")
# builtin pytest.raises helper
def raises(expected_exception, *args, **kwargs):

1
changelog/2574.bugfix Normal file
View File

@ -0,0 +1 @@
The options --fixtures and --fixtures-per-test will now keep indentation within docstrings.

1
changelog/971.doc Normal file
View File

@ -0,0 +1 @@
Extend documentation for testing plugin code with the ``pytester`` plugin.

View File

@ -122,8 +122,8 @@ to extend and add functionality.
for authoring plugins.
The template provides an excellent starting point with a working plugin,
tests running with tox, comprehensive README and
entry-pointy already pre-configured.
tests running with tox, a comprehensive README file as well as a
pre-configured entry-point.
Also consider :ref:`contributing your plugin to pytest-dev<submitplugin>`
once it has some happy users other than yourself.
@ -286,34 +286,101 @@ the ``--trace-config`` option.
Testing plugins
---------------
pytest comes with some facilities that you can enable for testing your
plugin. Given that you have an installed plugin you can enable the
:py:class:`testdir <_pytest.pytester.Testdir>` fixture via specifying a
command line option to include the pytester plugin (``-p pytester``) or
by putting ``pytest_plugins = "pytester"`` into your test or
``conftest.py`` file. You then will have a ``testdir`` fixture which you
can use like this::
pytest comes with a plugin named ``pytester`` that helps you write tests for
your plugin code. The plugin is disabled by default, so you will have to enable
it before you can use it.
# content of test_myplugin.py
You can do so by adding the following line to a ``conftest.py`` file in your
testing directory:
pytest_plugins = "pytester" # to get testdir fixture
.. code-block:: python
def test_myplugin(testdir):
# content of conftest.py
pytest_plugins = ["pytester"]
Alternatively you can invoke pytest with the ``-p pytester`` command line
option.
This will allow you to use the :py:class:`testdir <_pytest.pytester.Testdir>`
fixture for testing your plugin code.
Let's demonstrate what you can do with the plugin with an example. Imagine we
developed a plugin that provides a fixture ``hello`` which yields a function
and we can invoke this function with one optional parameter. It will return a
string value of ``Hello World!`` if we do not supply a value or ``Hello
{value}!`` if we do supply a string value.
.. code-block:: python
# -*- coding: utf-8 -*-
import pytest
def pytest_addoption(parser):
group = parser.getgroup('helloworld')
group.addoption(
'--name',
action='store',
dest='name',
default='World',
help='Default "name" for hello().'
)
@pytest.fixture
def hello(request):
name = request.config.option.name
def _hello(name=None):
if not name:
name = request.config.option.name
return "Hello {name}!".format(name=name)
return _hello
Now the ``testdir`` fixture provides a convenient API for creating temporary
``conftest.py`` files and test files. It also allows us to run the tests and
return a result object, with which we can assert the tests' outcomes.
.. code-block:: python
def test_hello(testdir):
"""Make sure that our plugin works."""
# create a temporary conftest.py file
testdir.makeconftest("""
import pytest
@pytest.fixture(params=[
"Brianna",
"Andreas",
"Floris",
])
def name(request):
return request.param
""")
# create a temporary pytest test file
testdir.makepyfile("""
def test_example():
pass
""")
result = testdir.runpytest("--verbose")
result.stdout.fnmatch_lines("""
test_example*
def test_hello_default(hello):
assert hello() == "Hello World!"
def test_hello_name(hello, name):
assert hello(name) == "Hello {0}!".format(name)
""")
Note that by default ``testdir.runpytest()`` will perform a pytest
in-process. You can pass the command line option ``--runpytest=subprocess``
to have it happen in a subprocess.
# run all tests with pytest
result = testdir.runpytest()
# check that all 4 tests passed
result.assert_outcomes(passed=4)
For more information about the result object that ``runpytest()`` returns, and
the methods that it provides please check out the :py:class:`RunResult
<_pytest.pytester.RunResult>` documentation.
Also see the :py:class:`RunResult <_pytest.pytester.RunResult>` for more
methods of the result object that you get from a call to ``runpytest``.
.. _`writinghooks`:

View File

@ -2716,7 +2716,7 @@ class TestShowFixtures(object):
""")
def test_show_fixtures_trimmed_doc(self, testdir):
p = testdir.makepyfile('''
p = testdir.makepyfile(dedent('''
import pytest
@pytest.fixture
def arg1():
@ -2732,9 +2732,9 @@ class TestShowFixtures(object):
line2
"""
''')
'''))
result = testdir.runpytest("--fixtures", p)
result.stdout.fnmatch_lines("""
result.stdout.fnmatch_lines(dedent("""
* fixtures defined from test_show_fixtures_trimmed_doc *
arg2
line1
@ -2743,7 +2743,64 @@ class TestShowFixtures(object):
line1
line2
""")
"""))
def test_show_fixtures_indented_doc(self, testdir):
p = testdir.makepyfile(dedent('''
import pytest
@pytest.fixture
def fixture1():
"""
line1
indented line
"""
'''))
result = testdir.runpytest("--fixtures", p)
result.stdout.fnmatch_lines(dedent("""
* fixtures defined from test_show_fixtures_indented_doc *
fixture1
line1
indented line
"""))
def test_show_fixtures_indented_doc_first_line_unindented(self, testdir):
p = testdir.makepyfile(dedent('''
import pytest
@pytest.fixture
def fixture1():
"""line1
line2
indented line
"""
'''))
result = testdir.runpytest("--fixtures", p)
result.stdout.fnmatch_lines(dedent("""
* fixtures defined from test_show_fixtures_indented_doc_first_line_unindented *
fixture1
line1
line2
indented line
"""))
def test_show_fixtures_indented_in_class(self, testdir):
p = testdir.makepyfile(dedent('''
import pytest
class TestClass:
@pytest.fixture
def fixture1():
"""line1
line2
indented line
"""
'''))
result = testdir.runpytest("--fixtures", p)
result.stdout.fnmatch_lines(dedent("""
* fixtures defined from test_show_fixtures_indented_in_class *
fixture1
line1
line2
indented line
"""))
def test_show_fixtures_different_files(self, testdir):
"""