Merge remote-tracking branch 'upstream/master' into features

This commit is contained in:
Bruno Oliveira 2017-12-11 22:18:50 -02:00
commit f8f1a52ea0
14 changed files with 202 additions and 220 deletions

View File

@ -157,10 +157,10 @@ Bug Fixes
<https://github.com/pytest-dev/pytest/issues/2124>`_) <https://github.com/pytest-dev/pytest/issues/2124>`_)
- If an exception happens while loading a plugin, pytest no longer hides the - If an exception happens while loading a plugin, pytest no longer hides the
original traceback. In python2 it will show the original traceback with a new original traceback. In Python 2 it will show the original traceback with a new
message that explains in which plugin. In python3 it will show 2 canonized message that explains in which plugin. In Python 3 it will show 2 canonized
exceptions, the original exception while loading the plugin in addition to an exceptions, the original exception while loading the plugin in addition to an
exception that PyTest throws about loading a plugin. (`#2491 exception that pytest throws about loading a plugin. (`#2491
<https://github.com/pytest-dev/pytest/issues/2491>`_) <https://github.com/pytest-dev/pytest/issues/2491>`_)
- ``capsys`` and ``capfd`` can now be used by other fixtures. (`#2709 - ``capsys`` and ``capfd`` can now be used by other fixtures. (`#2709

View File

@ -1,19 +1,15 @@
from __future__ import absolute_import, division, generators, print_function from __future__ import absolute_import, division, generators, print_function
import ast
from ast import PyCF_ONLY_AST as _AST_FLAG
from bisect import bisect_right from bisect import bisect_right
import sys import sys
import six import six
import inspect import inspect
import tokenize import tokenize
import py import py
cpy_compile = compile
try: cpy_compile = compile
import _ast
from _ast import PyCF_ONLY_AST as _AST_FLAG
except ImportError:
_AST_FLAG = 0
_ast = None
class Source(object): class Source(object):
@ -209,7 +205,7 @@ def compile_(source, filename=None, mode='exec', flags=generators.compiler_flag,
retrieval of the source code for the code object retrieval of the source code for the code object
and any recursively created code objects. and any recursively created code objects.
""" """
if _ast is not None and isinstance(source, _ast.AST): if isinstance(source, ast.AST):
# XXX should Source support having AST? # XXX should Source support having AST?
return cpy_compile(source, filename, mode, flags, dont_inherit) return cpy_compile(source, filename, mode, flags, dont_inherit)
_genframe = sys._getframe(1) # the caller _genframe = sys._getframe(1) # the caller
@ -322,7 +318,7 @@ def get_statement_startend2(lineno, node):
# AST's line numbers start indexing at 1 # AST's line numbers start indexing at 1
values = [] values = []
for x in ast.walk(node): for x in ast.walk(node):
if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler): if isinstance(x, ast.stmt) or isinstance(x, ast.ExceptHandler):
values.append(x.lineno - 1) values.append(x.lineno - 1)
for name in "finalbody", "orelse": for name in "finalbody", "orelse":
val = getattr(x, name, None) val = getattr(x, name, None)

View File

@ -1,7 +1,6 @@
"""Rewrite assertion AST to produce nice error messages""" """Rewrite assertion AST to produce nice error messages"""
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import ast import ast
import _ast
import errno import errno
import itertools import itertools
import imp import imp
@ -914,7 +913,7 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Compare(self, comp): def visit_Compare(self, comp):
self.push_format_context() self.push_format_context()
left_res, left_expl = self.visit(comp.left) left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (_ast.Compare, _ast.BoolOp)): if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = "({0})".format(left_expl) left_expl = "({0})".format(left_expl)
res_variables = [self.variable() for i in range(len(comp.ops))] res_variables = [self.variable() for i in range(len(comp.ops))]
load_names = [ast.Name(v, ast.Load()) for v in res_variables] load_names = [ast.Name(v, ast.Load()) for v in res_variables]
@ -925,7 +924,7 @@ class AssertionRewriter(ast.NodeVisitor):
results = [left_res] results = [left_res]
for i, op, next_operand in it: for i, op, next_operand in it:
next_res, next_expl = self.visit(next_operand) next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (_ast.Compare, _ast.BoolOp)): if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = "({0})".format(next_expl) next_expl = "({0})".format(next_expl)
results.append(next_res) results.append(next_res)
sym = binop_map[op.__class__] sym = binop_map[op.__class__]

View File

@ -5,11 +5,7 @@ import pprint
import _pytest._code import _pytest._code
import py import py
import six import six
try: from collections import Sequence
from collections import Sequence
except ImportError:
Sequence = list
u = six.text_type u = six.text_type
@ -113,7 +109,7 @@ def assertrepr_compare(config, op, left, right):
summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr)) summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr))
def issequence(x): def issequence(x):
return (isinstance(x, (list, tuple, Sequence)) and not isinstance(x, basestring)) return isinstance(x, Sequence) and not isinstance(x, basestring)
def istext(x): def istext(x):
return isinstance(x, basestring) return isinstance(x, basestring)

View File

@ -1,4 +1,4 @@
""" (disabled by default) support for testing pytest and pytest plugins. """ """(disabled by default) support for testing pytest and pytest plugins."""
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import codecs import codecs
@ -27,13 +27,12 @@ PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace("$py.cla
def pytest_addoption(parser): def pytest_addoption(parser):
# group = parser.getgroup("pytester", "pytester (self-tests) options")
parser.addoption('--lsof', parser.addoption('--lsof',
action="store_true", dest="lsof", default=False, action="store_true", dest="lsof", default=False,
help=("run FD checks if lsof is available")) help=("run FD checks if lsof is available"))
parser.addoption('--runpytest', default="inprocess", dest="runpytest", parser.addoption('--runpytest', default="inprocess", dest="runpytest",
choices=("inprocess", "subprocess", ), choices=("inprocess", "subprocess"),
help=("run pytest sub runs in tests using an 'inprocess' " help=("run pytest sub runs in tests using an 'inprocess' "
"or 'subprocess' (python -m main) method")) "or 'subprocess' (python -m main) method"))
@ -76,8 +75,8 @@ class LsofFdLeakChecker(object):
try: try:
py.process.cmdexec("lsof -v") py.process.cmdexec("lsof -v")
except (py.process.cmdexec.Error, UnicodeDecodeError): except (py.process.cmdexec.Error, UnicodeDecodeError):
# cmdexec may raise UnicodeDecodeError on Windows systems # cmdexec may raise UnicodeDecodeError on Windows systems with
# with locale other than english: # locale other than English:
# https://bitbucket.org/pytest-dev/py/issues/66 # https://bitbucket.org/pytest-dev/py/issues/66
return False return False
else: else:
@ -132,7 +131,7 @@ def getexecutable(name, cache={}):
if "2.5.2" in err: if "2.5.2" in err:
executable = None # http://bugs.jython.org/issue1790 executable = None # http://bugs.jython.org/issue1790
elif popen.returncode != 0: elif popen.returncode != 0:
# Handle pyenv's 127. # handle pyenv's 127
executable = None executable = None
cache[name] = executable cache[name] = executable
return executable return executable
@ -157,9 +156,10 @@ def anypython(request):
@pytest.fixture @pytest.fixture
def _pytest(request): def _pytest(request):
""" Return a helper which offers a gethookrecorder(hook) """Return a helper which offers a gethookrecorder(hook) method which
method which returns a HookRecorder instance which helps returns a HookRecorder instance which helps to make assertions about called
to make assertions about called hooks. hooks.
""" """
return PytestArg(request) return PytestArg(request)
@ -193,8 +193,8 @@ class ParsedCall:
class HookRecorder: class HookRecorder:
"""Record all hooks called in a plugin manager. """Record all hooks called in a plugin manager.
This wraps all the hook calls in the plugin manager, recording This wraps all the hook calls in the plugin manager, recording each call
each call before propagating the normal calls. before propagating the normal calls.
""" """
@ -262,7 +262,7 @@ class HookRecorder:
def matchreport(self, inamepart="", def matchreport(self, inamepart="",
names="pytest_runtest_logreport pytest_collectreport", when=None): names="pytest_runtest_logreport pytest_collectreport", when=None):
""" return a testreport whose dotted import path matches """ """return a testreport whose dotted import path matches"""
values = [] values = []
for rep in self.getreports(names=names): for rep in self.getreports(names=names):
try: try:
@ -341,14 +341,14 @@ class RunResult:
Attributes: Attributes:
:ret: The return value. :ret: the return value
:outlines: List of lines captured from stdout. :outlines: list of lines captured from stdout
:errlines: List of lines captures from stderr. :errlines: list of lines captures from stderr
:stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to :stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to
reconstruct stdout or the commonly used reconstruct stdout or the commonly used ``stdout.fnmatch_lines()``
``stdout.fnmatch_lines()`` method. method
:stderrr: :py:class:`LineMatcher` of stderr. :stderrr: :py:class:`LineMatcher` of stderr
:duration: Duration in seconds. :duration: duration in seconds
""" """
@ -361,8 +361,10 @@ class RunResult:
self.duration = duration self.duration = duration
def parseoutcomes(self): def parseoutcomes(self):
""" Return a dictionary of outcomestring->num from parsing """Return a dictionary of outcomestring->num from parsing the terminal
the terminal output that the test process produced.""" output that the test process produced.
"""
for line in reversed(self.outlines): for line in reversed(self.outlines):
if 'seconds' in line: if 'seconds' in line:
outcomes = rex_outcome.findall(line) outcomes = rex_outcome.findall(line)
@ -374,8 +376,10 @@ class RunResult:
raise ValueError("Pytest terminal report not found") raise ValueError("Pytest terminal report not found")
def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0): def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0):
""" assert that the specified outcomes appear with the respective """Assert that the specified outcomes appear with the respective
numbers (0 means it didn't occur) in the text output from a test run.""" numbers (0 means it didn't occur) in the text output from a test run.
"""
d = self.parseoutcomes() d = self.parseoutcomes()
obtained = { obtained = {
'passed': d.get('passed', 0), 'passed': d.get('passed', 0),
@ -389,21 +393,18 @@ class RunResult:
class Testdir: class Testdir:
"""Temporary test directory with tools to test/run pytest itself. """Temporary test directory with tools to test/run pytest itself.
This is based on the ``tmpdir`` fixture but provides a number of This is based on the ``tmpdir`` fixture but provides a number of methods
methods which aid with testing pytest itself. Unless which aid with testing pytest itself. Unless :py:meth:`chdir` is used all
:py:meth:`chdir` is used all methods will use :py:attr:`tmpdir` as methods will use :py:attr:`tmpdir` as their current working directory.
current working directory.
Attributes: Attributes:
:tmpdir: The :py:class:`py.path.local` instance of the temporary :tmpdir: The :py:class:`py.path.local` instance of the temporary directory.
directory.
:plugins: A list of plugins to use with :py:meth:`parseconfig` and :plugins: A list of plugins to use with :py:meth:`parseconfig` and
:py:meth:`runpytest`. Initially this is an empty list but :py:meth:`runpytest`. Initially this is an empty list but plugins can
plugins can be added to the list. The type of items to add to be added to the list. The type of items to add to the list depends on
the list depend on the method which uses them so refer to them the method using them so refer to them for details.
for details.
""" """
@ -429,10 +430,9 @@ class Testdir:
def finalize(self): def finalize(self):
"""Clean up global state artifacts. """Clean up global state artifacts.
Some methods modify the global interpreter state and this Some methods modify the global interpreter state and this tries to
tries to clean this up. It does not remove the temporary clean this up. It does not remove the temporary directory however so
directory however so it can be looked at after the test run it can be looked at after the test run has finished.
has finished.
""" """
sys.path[:], sys.meta_path[:] = self._savesyspath sys.path[:], sys.meta_path[:] = self._savesyspath
@ -495,17 +495,15 @@ class Testdir:
def makefile(self, ext, *args, **kwargs): def makefile(self, ext, *args, **kwargs):
"""Create a new file in the testdir. """Create a new file in the testdir.
ext: The extension the file should use, including the dot. ext: The extension the file should use, including the dot, e.g. `.py`.
E.g. ".py".
args: All args will be treated as strings and joined using args: All args will be treated as strings and joined using newlines.
newlines. The result will be written as contents to the The result will be written as contents to the file. The name of the
file. The name of the file will be based on the test file will be based on the test function requesting this fixture.
function requesting this fixture.
E.g. "testdir.makefile('.txt', 'line1', 'line2')" E.g. "testdir.makefile('.txt', 'line1', 'line2')"
kwargs: Each keyword is the name of a file, while the value of kwargs: Each keyword is the name of a file, while the value of it will
it will be written as contents of the file. be written as contents of the file.
E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')" E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')"
""" """
@ -535,14 +533,16 @@ class Testdir:
def syspathinsert(self, path=None): def syspathinsert(self, path=None):
"""Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`. """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`.
This is undone automatically after the test. This is undone automatically when this object dies at the end of each
test.
""" """
if path is None: if path is None:
path = self.tmpdir path = self.tmpdir
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
# a call to syspathinsert() usually means that the caller # a call to syspathinsert() usually means that the caller wants to
# wants to import some dynamically created files. # import some dynamically created files, thus with python3 we
# with python3 we thus invalidate import caches. # invalidate its import caches
self._possibly_invalidate_import_caches() self._possibly_invalidate_import_caches()
def _possibly_invalidate_import_caches(self): def _possibly_invalidate_import_caches(self):
@ -562,8 +562,8 @@ class Testdir:
def mkpydir(self, name): def mkpydir(self, name):
"""Create a new python package. """Create a new python package.
This creates a (sub)directory with an empty ``__init__.py`` This creates a (sub)directory with an empty ``__init__.py`` file so it
file so that is recognised as a python package. gets recognised as a python package.
""" """
p = self.mkdir(name) p = self.mkdir(name)
@ -576,10 +576,10 @@ class Testdir:
"""Return the collection node of a file. """Return the collection node of a file.
:param config: :py:class:`_pytest.config.Config` instance, see :param config: :py:class:`_pytest.config.Config` instance, see
:py:meth:`parseconfig` and :py:meth:`parseconfigure` to :py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the
create the configuration. configuration
:param arg: A :py:class:`py.path.local` instance of the file. :param arg: a :py:class:`py.path.local` instance of the file
""" """
session = Session(config) session = Session(config)
@ -593,11 +593,10 @@ class Testdir:
def getpathnode(self, path): def getpathnode(self, path):
"""Return the collection node of a file. """Return the collection node of a file.
This is like :py:meth:`getnode` but uses This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
:py:meth:`parseconfigure` to create the (configured) pytest create the (configured) pytest Config instance.
Config instance.
:param path: A :py:class:`py.path.local` instance of the file. :param path: a :py:class:`py.path.local` instance of the file
""" """
config = self.parseconfigure(path) config = self.parseconfigure(path)
@ -611,8 +610,8 @@ class Testdir:
def genitems(self, colitems): def genitems(self, colitems):
"""Generate all test items from a collection node. """Generate all test items from a collection node.
This recurses into the collection node and returns a list of This recurses into the collection node and returns a list of all the
all the test items contained within. test items contained within.
""" """
session = colitems[0].session session = colitems[0].session
@ -624,10 +623,10 @@ class Testdir:
def runitem(self, source): def runitem(self, source):
"""Run the "test_func" Item. """Run the "test_func" Item.
The calling test instance (the class which contains the test The calling test instance (class containing the test method) must
method) must provide a ``.getrunner()`` method which should provide a ``.getrunner()`` method which should return a runner which
return a runner which can run the test protocol for a single can run the test protocol for a single item, e.g.
item, like e.g. :py:func:`_pytest.runner.runtestprotocol`. :py:func:`_pytest.runner.runtestprotocol`.
""" """
# used from runner functional tests # used from runner functional tests
@ -641,14 +640,14 @@ class Testdir:
"""Run a test module in process using ``pytest.main()``. """Run a test module in process using ``pytest.main()``.
This run writes "source" into a temporary file and runs This run writes "source" into a temporary file and runs
``pytest.main()`` on it, returning a :py:class:`HookRecorder` ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
instance for the result. for the result.
:param source: The source code of the test module. :param source: the source code of the test module
:param cmdlineargs: Any extra command line arguments to use. :param cmdlineargs: any extra command line arguments to use
:return: :py:class:`HookRecorder` instance of the result. :return: :py:class:`HookRecorder` instance of the result
""" """
p = self.makepyfile(source) p = self.makepyfile(source)
@ -658,13 +657,9 @@ class Testdir:
def inline_genitems(self, *args): def inline_genitems(self, *args):
"""Run ``pytest.main(['--collectonly'])`` in-process. """Run ``pytest.main(['--collectonly'])`` in-process.
Returns a tuple of the collected items and a Runs the :py:func:`pytest.main` function to run all of pytest inside
:py:class:`HookRecorder` instance. the test process itself like :py:meth:`inline_run`, but returns a
tuple of the collected items and a :py:class:`HookRecorder` instance.
This runs the :py:func:`pytest.main` function to run all of
pytest inside the test process itself like
:py:meth:`inline_run`. However the return value is a tuple of
the collection items and a :py:class:`HookRecorder` instance.
""" """
rec = self.inline_run("--collect-only", *args) rec = self.inline_run("--collect-only", *args)
@ -674,24 +669,24 @@ class Testdir:
def inline_run(self, *args, **kwargs): def inline_run(self, *args, **kwargs):
"""Run ``pytest.main()`` in-process, returning a HookRecorder. """Run ``pytest.main()`` in-process, returning a HookRecorder.
This runs the :py:func:`pytest.main` function to run all of Runs the :py:func:`pytest.main` function to run all of pytest inside
pytest inside the test process itself. This means it can the test process itself. This means it can return a
return a :py:class:`HookRecorder` instance which gives more :py:class:`HookRecorder` instance which gives more detailed results
detailed results from then run then can be done by matching from that run than can be done by matching stdout/stderr from
stdout/stderr from :py:meth:`runpytest`. :py:meth:`runpytest`.
:param args: Any command line arguments to pass to :param args: command line arguments to pass to :py:func:`pytest.main`
:py:func:`pytest.main`.
:param plugin: (keyword-only) Extra plugin instances the :param plugin: (keyword-only) extra plugin instances the
``pytest.main()`` instance should use. ``pytest.main()`` instance should use
:return: a :py:class:`HookRecorder` instance
:return: A :py:class:`HookRecorder` instance.
""" """
# When running py.test inline any plugins active in the main # When running py.test inline any plugins active in the main test
# test process are already imported. So this disables the # process are already imported. So this disables the warning which
# warning which will trigger to say they can no longer be # will trigger to say they can no longer be rewritten, which is fine as
# rewritten, which is fine as they are already rewritten. # they have already been rewritten.
orig_warn = AssertionRewritingHook._warn_already_imported orig_warn = AssertionRewritingHook._warn_already_imported
def revert(): def revert():
@ -717,8 +712,8 @@ class Testdir:
pass pass
reprec.ret = ret reprec.ret = ret
# typically we reraise keyboard interrupts from the child run # typically we reraise keyboard interrupts from the child run because
# because it's our user requesting interruption of the testing # it's our user requesting interruption of the testing
if ret == 2 and not kwargs.get("no_reraise_ctrlc"): if ret == 2 and not kwargs.get("no_reraise_ctrlc"):
calls = reprec.getcalls("pytest_keyboard_interrupt") calls = reprec.getcalls("pytest_keyboard_interrupt")
if calls and calls[-1].excinfo.type == KeyboardInterrupt: if calls and calls[-1].excinfo.type == KeyboardInterrupt:
@ -726,8 +721,10 @@ class Testdir:
return reprec return reprec
def runpytest_inprocess(self, *args, **kwargs): def runpytest_inprocess(self, *args, **kwargs):
""" Return result of running pytest in-process, providing a similar """Return result of running pytest in-process, providing a similar
interface to what self.runpytest() provides. """ interface to what self.runpytest() provides.
"""
if kwargs.get("syspathinsert"): if kwargs.get("syspathinsert"):
self.syspathinsert() self.syspathinsert()
now = time.time() now = time.time()
@ -759,7 +756,7 @@ class Testdir:
return res return res
def runpytest(self, *args, **kwargs): def runpytest(self, *args, **kwargs):
""" Run pytest inline or in a subprocess, depending on the command line """Run pytest inline or in a subprocess, depending on the command line
option "--runpytest" and return a :py:class:`RunResult`. option "--runpytest" and return a :py:class:`RunResult`.
""" """
@ -780,13 +777,13 @@ class Testdir:
def parseconfig(self, *args): def parseconfig(self, *args):
"""Return a new pytest Config instance from given commandline args. """Return a new pytest Config instance from given commandline args.
This invokes the pytest bootstrapping code in _pytest.config This invokes the pytest bootstrapping code in _pytest.config to create
to create a new :py:class:`_pytest.core.PluginManager` and a new :py:class:`_pytest.core.PluginManager` and call the
call the pytest_cmdline_parse hook to create new pytest_cmdline_parse hook to create a new
:py:class:`_pytest.config.Config` instance. :py:class:`_pytest.config.Config` instance.
If :py:attr:`plugins` has been populated they should be plugin If :py:attr:`plugins` has been populated they should be plugin modules
modules which will be registered with the PluginManager. to be registered with the PluginManager.
""" """
args = self._ensure_basetemp(args) args = self._ensure_basetemp(args)
@ -802,9 +799,8 @@ class Testdir:
def parseconfigure(self, *args): def parseconfigure(self, *args):
"""Return a new pytest configured Config instance. """Return a new pytest configured Config instance.
This returns a new :py:class:`_pytest.config.Config` instance This returns a new :py:class:`_pytest.config.Config` instance like
like :py:meth:`parseconfig`, but also calls the :py:meth:`parseconfig`, but also calls the pytest_configure hook.
pytest_configure hook.
""" """
config = self.parseconfig(*args) config = self.parseconfig(*args)
@ -815,14 +811,14 @@ class Testdir:
def getitem(self, source, funcname="test_func"): def getitem(self, source, funcname="test_func"):
"""Return the test item for a test function. """Return the test item for a test function.
This writes the source to a python file and runs pytest's This writes the source to a python file and runs pytest's collection on
collection on the resulting module, returning the test item the resulting module, returning the test item for the requested
for the requested function name. function name.
:param source: The module source. :param source: the module source
:param funcname: The name of the test function for which the :param funcname: the name of the test function for which to return a
Item must be returned. test item
""" """
items = self.getitems(source) items = self.getitems(source)
@ -835,9 +831,8 @@ class Testdir:
def getitems(self, source): def getitems(self, source):
"""Return all test items collected from the module. """Return all test items collected from the module.
This writes the source to a python file and runs pytest's This writes the source to a python file and runs pytest's collection on
collection on the resulting module, returning all test items the resulting module, returning all test items contained within.
contained within.
""" """
modcol = self.getmodulecol(source) modcol = self.getmodulecol(source)
@ -846,17 +841,17 @@ class Testdir:
def getmodulecol(self, source, configargs=(), withinit=False): def getmodulecol(self, source, configargs=(), withinit=False):
"""Return the module collection node for ``source``. """Return the module collection node for ``source``.
This writes ``source`` to a file using :py:meth:`makepyfile` This writes ``source`` to a file using :py:meth:`makepyfile` and then
and then runs the pytest collection on it, returning the runs the pytest collection on it, returning the collection node for the
collection node for the test module. test module.
:param source: The source code of the module to collect. :param source: the source code of the module to collect
:param configargs: Any extra arguments to pass to :param configargs: any extra arguments to pass to
:py:meth:`parseconfigure`. :py:meth:`parseconfigure`
:param withinit: Whether to also write a ``__init__.py`` file :param withinit: whether to also write an ``__init__.py`` file to the
to the temporary directory to ensure it is a package. same directory to ensure it is a package
""" """
kw = {self.request.function.__name__: Source(source).strip()} kw = {self.request.function.__name__: Source(source).strip()}
@ -871,13 +866,12 @@ class Testdir:
def collect_by_name(self, modcol, name): def collect_by_name(self, modcol, name):
"""Return the collection node for name from the module collection. """Return the collection node for name from the module collection.
This will search a module collection node for a collection This will search a module collection node for a collection node
node matching the given name. matching the given name.
:param modcol: A module collection node, see :param modcol: a module collection node; see :py:meth:`getmodulecol`
:py:meth:`getmodulecol`.
:param name: The name of the node to return. :param name: the name of the node to return
""" """
if modcol not in self._mod_collections: if modcol not in self._mod_collections:
@ -889,8 +883,8 @@ class Testdir:
def popen(self, cmdargs, stdout, stderr, **kw): def popen(self, cmdargs, stdout, stderr, **kw):
"""Invoke subprocess.Popen. """Invoke subprocess.Popen.
This calls subprocess.Popen making sure the current working This calls subprocess.Popen making sure the current working directory
directory is the PYTHONPATH. is in the PYTHONPATH.
You probably want to use :py:meth:`run` instead. You probably want to use :py:meth:`run` instead.
@ -908,8 +902,7 @@ class Testdir:
def run(self, *cmdargs): def run(self, *cmdargs):
"""Run a command with arguments. """Run a command with arguments.
Run a process using subprocess.Popen saving the stdout and Run a process using subprocess.Popen saving the stdout and stderr.
stderr.
Returns a :py:class:`RunResult`. Returns a :py:class:`RunResult`.
@ -952,14 +945,15 @@ class Testdir:
print("couldn't print to %s because of encoding" % (fp,)) print("couldn't print to %s because of encoding" % (fp,))
def _getpytestargs(self): def _getpytestargs(self):
# we cannot use "(sys.executable,script)" # we cannot use `(sys.executable, script)` because on Windows the
# because on windows the script is e.g. a pytest.exe # script is e.g. `pytest.exe`
return (sys.executable, PYTEST_FULLPATH) # noqa return (sys.executable, PYTEST_FULLPATH) # noqa
def runpython(self, script): def runpython(self, script):
"""Run a python script using sys.executable as interpreter. """Run a python script using sys.executable as interpreter.
Returns a :py:class:`RunResult`. Returns a :py:class:`RunResult`.
""" """
return self.run(sys.executable, script) return self.run(sys.executable, script)
@ -970,25 +964,18 @@ class Testdir:
def runpytest_subprocess(self, *args, **kwargs): def runpytest_subprocess(self, *args, **kwargs):
"""Run pytest as a subprocess with given arguments. """Run pytest as a subprocess with given arguments.
Any plugins added to the :py:attr:`plugins` list will added Any plugins added to the :py:attr:`plugins` list will added using the
using the ``-p`` command line option. Addtionally ``-p`` command line option. Additionally ``--basetemp`` is used put
``--basetemp`` is used put any temporary files and directories any temporary files and directories in a numbered directory prefixed
in a numbered directory prefixed with "runpytest-" so they do with "runpytest-" so they do not conflict with the normal numbered
not conflict with the normal numberd pytest location for pytest location for temporary files and directories.
temporary files and directories.
Returns a :py:class:`RunResult`. Returns a :py:class:`RunResult`.
""" """
p = py.path.local.make_numbered_dir(prefix="runpytest-", p = py.path.local.make_numbered_dir(prefix="runpytest-",
keep=None, rootdir=self.tmpdir) keep=None, rootdir=self.tmpdir)
args = ('--basetemp=%s' % p, ) + args args = ('--basetemp=%s' % p,) + args
# for x in args:
# if '--confcutdir' in str(x):
# break
# else:
# pass
# args = ('--confcutdir=.',) + args
plugins = [x for x in self.plugins if isinstance(x, str)] plugins = [x for x in self.plugins if isinstance(x, str)]
if plugins: if plugins:
args = ('-p', plugins[0]) + args args = ('-p', plugins[0]) + args
@ -998,8 +985,8 @@ class Testdir:
def spawn_pytest(self, string, expect_timeout=10.0): def spawn_pytest(self, string, expect_timeout=10.0):
"""Run pytest using pexpect. """Run pytest using pexpect.
This makes sure to use the right pytest and sets up the This makes sure to use the right pytest and sets up the temporary
temporary directory locations. directory locations.
The pexpect child is returned. The pexpect child is returned.
@ -1013,6 +1000,7 @@ class Testdir:
"""Run a command using pexpect. """Run a command using pexpect.
The pexpect child is returned. The pexpect child is returned.
""" """
pexpect = pytest.importorskip("pexpect", "3.0") pexpect = pytest.importorskip("pexpect", "3.0")
if hasattr(sys, 'pypy_version_info') and '64' in platform.machine(): if hasattr(sys, 'pypy_version_info') and '64' in platform.machine():
@ -1039,8 +1027,10 @@ class LineComp:
self.stringio = py.io.TextIO() self.stringio = py.io.TextIO()
def assert_contains_lines(self, lines2): def assert_contains_lines(self, lines2):
""" assert that lines2 are contained (linearly) in lines1. """Assert that lines2 are contained (linearly) in lines1.
return a list of extralines found.
Return a list of extralines found.
""" """
__tracebackhide__ = True __tracebackhide__ = True
val = self.stringio.getvalue() val = self.stringio.getvalue()
@ -1056,8 +1046,8 @@ class LineMatcher:
This is a convenience class to test large texts like the output of This is a convenience class to test large texts like the output of
commands. commands.
The constructor takes a list of lines without their trailing The constructor takes a list of lines without their trailing newlines, i.e.
newlines, i.e. ``text.splitlines()``. ``text.splitlines()``.
""" """
@ -1077,18 +1067,19 @@ class LineMatcher:
return lines2 return lines2
def fnmatch_lines_random(self, lines2): def fnmatch_lines_random(self, lines2):
"""Check lines exist in the output using ``fnmatch.fnmatch``, in any order. """Check lines exist in the output using in any order.
Lines are checked using ``fnmatch.fnmatch``. The argument is a list of
lines which have to occur in the output, in any order.
The argument is a list of lines which have to occur in the
output, in any order.
""" """
self._match_lines_random(lines2, fnmatch) self._match_lines_random(lines2, fnmatch)
def re_match_lines_random(self, lines2): def re_match_lines_random(self, lines2):
"""Check lines exist in the output using ``re.match``, in any order. """Check lines exist in the output using ``re.match``, in any order.
The argument is a list of lines which have to occur in the The argument is a list of lines which have to occur in the output, in
output, in any order. any order.
""" """
self._match_lines_random(lines2, lambda name, pat: re.match(pat, name)) self._match_lines_random(lines2, lambda name, pat: re.match(pat, name))
@ -1096,8 +1087,8 @@ class LineMatcher:
def _match_lines_random(self, lines2, match_func): def _match_lines_random(self, lines2, match_func):
"""Check lines exist in the output. """Check lines exist in the output.
The argument is a list of lines which have to occur in the The argument is a list of lines which have to occur in the output, in
output, in any order. Each line can contain glob whildcards. any order. Each line can contain glob whildcards.
""" """
lines2 = self._getlines(lines2) lines2 = self._getlines(lines2)
@ -1114,6 +1105,7 @@ class LineMatcher:
"""Return all lines following the given line in the text. """Return all lines following the given line in the text.
The given line can contain glob wildcards. The given line can contain glob wildcards.
""" """
for i, line in enumerate(self.lines): for i, line in enumerate(self.lines):
if fnline == line or fnmatch(line, fnline): if fnline == line or fnmatch(line, fnline):
@ -1130,10 +1122,9 @@ class LineMatcher:
def fnmatch_lines(self, lines2): def fnmatch_lines(self, lines2):
"""Search captured text for matching lines using ``fnmatch.fnmatch``. """Search captured text for matching lines using ``fnmatch.fnmatch``.
The argument is a list of lines which have to match and can The argument is a list of lines which have to match and can use glob
use glob wildcards. If they do not match a pytest.fail() is wildcards. If they do not match a pytest.fail() is called. The
called. The matches and non-matches are also printed on matches and non-matches are also printed on stdout.
stdout.
""" """
self._match_lines(lines2, fnmatch, 'fnmatch') self._match_lines(lines2, fnmatch, 'fnmatch')
@ -1144,21 +1135,22 @@ class LineMatcher:
The argument is a list of lines which have to match using ``re.match``. The argument is a list of lines which have to match using ``re.match``.
If they do not match a pytest.fail() is called. If they do not match a pytest.fail() is called.
The matches and non-matches are also printed on The matches and non-matches are also printed on stdout.
stdout.
""" """
self._match_lines(lines2, lambda name, pat: re.match(pat, name), 're.match') self._match_lines(lines2, lambda name, pat: re.match(pat, name), 're.match')
def _match_lines(self, lines2, match_func, match_nickname): def _match_lines(self, lines2, match_func, match_nickname):
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
:param list[str] lines2: list of string patterns to match. The actual format depends on :param list[str] lines2: list of string patterns to match. The actual
``match_func``. format depends on ``match_func``
:param match_func: a callable ``match_func(line, pattern)`` where line is the captured :param match_func: a callable ``match_func(line, pattern)`` where line
line from stdout/stderr and pattern is the matching pattern. is the captured line from stdout/stderr and pattern is the matching
pattern
:param str match_nickname: the nickname for the match function that
will be logged to stdout when a match occurs
:param str match_nickname: the nickname for the match function that will be logged
to stdout when a match occurs.
""" """
lines2 = self._getlines(lines2) lines2 = self._getlines(lines2)
lines1 = self.lines[:] lines1 = self.lines[:]

1
changelog/3015.trivial Normal file
View File

@ -0,0 +1 @@
Code cleanup.

1
changelog/3018.trivial Normal file
View File

@ -0,0 +1 @@
Clean up code by replacing imports and references of `_ast` to `ast`.

View File

@ -92,14 +92,14 @@ an example test function that performs some output related checks:
.. code-block:: python .. code-block:: python
def test_myoutput(capsys): # or use "capfd" for fd-level def test_myoutput(capsys): # or use "capfd" for fd-level
print ("hello") print("hello")
sys.stderr.write("world\n") sys.stderr.write("world\n")
out, err = capsys.readouterr() captured = capsys.readouterr()
assert out == "hello\n" assert captured.out == "hello\n"
assert err == "world\n" assert captured.err == "world\n"
print ("next") print("next")
out, err = capsys.readouterr() captured = capsys.readouterr()
assert out == "next\n" assert captured.out == "next\n"
The ``readouterr()`` call snapshots the output so far - The ``readouterr()`` call snapshots the output so far -
and capturing will be continued. After the test and capturing will be continued. After the test
@ -117,6 +117,10 @@ system level output streams (FD1 and FD2).
.. versionadded:: 3.3 .. versionadded:: 3.3
The return value from ``readouterr`` changed to a ``namedtuple`` with two attributes, ``out`` and ``err``.
.. versionadded:: 3.3
If the code under test writes non-textual data, you can capture this using If the code under test writes non-textual data, you can capture this using
the ``capsysbinary`` fixture which instead returns ``bytes`` from the ``capsysbinary`` fixture which instead returns ``bytes`` from
the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only

View File

@ -215,8 +215,8 @@ class TestGeneralUsage(object):
assert not result.ret assert not result.ret
def test_issue109_sibling_conftests_not_loaded(self, testdir): def test_issue109_sibling_conftests_not_loaded(self, testdir):
sub1 = testdir.tmpdir.mkdir("sub1") sub1 = testdir.mkdir("sub1")
sub2 = testdir.tmpdir.mkdir("sub2") sub2 = testdir.mkdir("sub2")
sub1.join("conftest.py").write("assert 0") sub1.join("conftest.py").write("assert 0")
result = testdir.runpytest(sub2) result = testdir.runpytest(sub2)
assert result.ret == EXIT_NOTESTSCOLLECTED assert result.ret == EXIT_NOTESTSCOLLECTED
@ -603,11 +603,11 @@ class TestInvocationVariants(object):
# The structure of the test directory is now: # The structure of the test directory is now:
# . # .
# ├── hello # ├── hello
# │   └── ns_pkg # │ └── ns_pkg
# │   ├── __init__.py # │ ├── __init__.py
# │   └── hello # │ └── hello
# │   ├── __init__.py # │ ├── __init__.py
# │   └── test_hello.py # │ └── test_hello.py
# └── world # └── world
# └── ns_pkg # └── ns_pkg
# ├── __init__.py # ├── __init__.py
@ -624,10 +624,9 @@ class TestInvocationVariants(object):
for p in search_path: for p in search_path:
monkeypatch.syspath_prepend(p) monkeypatch.syspath_prepend(p)
os.chdir('world')
# mixed module and filenames: # mixed module and filenames:
os.chdir('world')
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world") result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world")
testdir.chdir()
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*test_hello.py::test_hello*PASSED*", "*test_hello.py::test_hello*PASSED*",
@ -638,6 +637,7 @@ class TestInvocationVariants(object):
]) ])
# specify tests within a module # specify tests within a module
testdir.chdir()
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.world.test_world::test_other") result = testdir.runpytest("--pyargs", "-v", "ns_pkg.world.test_world::test_other")
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([

View File

@ -8,13 +8,10 @@ import _pytest._code
import py import py
import pytest import pytest
from _pytest._code import Source from _pytest._code import Source
from _pytest._code.source import _ast from _pytest._code.source import ast
if _ast is not None:
astonly = pytest.mark.nothing
else:
astonly = pytest.mark.xfail("True", reason="only works with AST-compile")
astonly = pytest.mark.nothing
failsonjython = pytest.mark.xfail("sys.platform.startswith('java')") failsonjython = pytest.mark.xfail("sys.platform.startswith('java')")

View File

@ -84,7 +84,7 @@ def test_conftest_in_nonpkg_with_init(tmpdir):
def test_doubledash_considered(testdir): def test_doubledash_considered(testdir):
conf = testdir.mkdir("--option") conf = testdir.mkdir("--option")
conf.join("conftest.py").ensure() conf.ensure("conftest.py")
conftest = PytestPluginManager() conftest = PytestPluginManager()
conftest_setinitial(conftest, [conf.basename, conf.basename]) conftest_setinitial(conftest, [conf.basename, conf.basename])
values = conftest._getconftestmodules(conf) values = conftest._getconftestmodules(conf)
@ -270,11 +270,7 @@ def test_conftest_found_with_double_dash(testdir):
parser.addoption("--hello-world", action="store_true") parser.addoption("--hello-world", action="store_true")
""")) """))
p = sub.join("test_hello.py") p = sub.join("test_hello.py")
p.write(py.std.textwrap.dedent(""" p.write("def test_hello(): pass")
import pytest
def test_hello(found):
assert found == 1
"""))
result = testdir.runpytest(str(p) + "::test_hello", "-h") result = testdir.runpytest(str(p) + "::test_hello", "-h")
result.stdout.fnmatch_lines(""" result.stdout.fnmatch_lines("""
*--hello-world* *--hello-world*

View File

@ -338,7 +338,7 @@ class TestPDB(object):
self.flush(child) self.flush(child)
def test_pdb_collection_failure_is_shown(self, testdir): def test_pdb_collection_failure_is_shown(self, testdir):
p1 = testdir.makepyfile("""xxx """) p1 = testdir.makepyfile("xxx")
result = testdir.runpytest_subprocess("--pdb", p1) result = testdir.runpytest_subprocess("--pdb", p1)
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*NameError*xxx*", "*NameError*xxx*",

View File

@ -141,7 +141,7 @@ def test_inline_run_clean_modules(testdir):
assert result2.ret == EXIT_TESTSFAILED assert result2.ret == EXIT_TESTSFAILED
def test_assert_outcomes_after_pytest_erro(testdir): def test_assert_outcomes_after_pytest_error(testdir):
testdir.makepyfile("def test_foo(): assert True") testdir.makepyfile("def test_foo(): assert True")
result = testdir.runpytest('--unexpected-argument') result = testdir.runpytest('--unexpected-argument')

View File

@ -138,7 +138,7 @@ commands =
basepython = python basepython = python
usedevelop = True usedevelop = True
skipsdist = True skipsdist = True
# ensure the given pyargs cant mean anytrhing else # ensure the given pyargs can't mean anything else
changedir = doc/ changedir = doc/
deps = deps =
PyYAML PyYAML