Merge pull request #3041 from segevfiner/capture-no-disable-progress

Use classic console output when -s is used
This commit is contained in:
Bruno Oliveira 2017-12-16 12:34:34 -02:00 committed by GitHub
commit d87279115d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 322 additions and 76 deletions

View File

@ -74,6 +74,7 @@ Grig Gheorghiu
Grigorii Eremeev (budulianin) Grigorii Eremeev (budulianin)
Guido Wesdorp Guido Wesdorp
Harald Armin Massa Harald Armin Massa
Henk-Jaap Wagenaar
Hugo van Kemenade Hugo van Kemenade
Hui Wang (coldnight) Hui Wang (coldnight)
Ian Bicking Ian Bicking

View File

@ -179,11 +179,13 @@ class AssertionRewritingHook(object):
The named module or package as well as any nested modules will The named module or package as well as any nested modules will
be rewritten on import. be rewritten on import.
""" """
already_imported = set(names).intersection(set(sys.modules)) already_imported = (set(names)
if already_imported: .intersection(sys.modules)
for name in already_imported: .difference(self._rewritten_names))
if name not in self._rewritten_names: for name in already_imported:
self._warn_already_imported(name) if not AssertionRewriter.is_rewrite_disabled(
sys.modules[name].__doc__ or ""):
self._warn_already_imported(name)
self._must_rewrite.update(names) self._must_rewrite.update(names)
def _warn_already_imported(self, name): def _warn_already_imported(self, name):
@ -635,7 +637,8 @@ class AssertionRewriter(ast.NodeVisitor):
not isinstance(field, ast.expr)): not isinstance(field, ast.expr)):
nodes.append(field) nodes.append(field)
def is_rewrite_disabled(self, docstring): @staticmethod
def is_rewrite_disabled(docstring):
return "PYTEST_DONT_REWRITE" in docstring return "PYTEST_DONT_REWRITE" in docstring
def variable(self): def variable(self):

View File

@ -267,7 +267,6 @@ class FixtureRequest(FuncargnamesCompatAttr):
self.fixturename = None self.fixturename = None
#: Scope string, one of "function", "class", "module", "session" #: Scope string, one of "function", "class", "module", "session"
self.scope = "function" self.scope = "function"
self._fixture_values = {} # argname -> fixture value
self._fixture_defs = {} # argname -> FixtureDef self._fixture_defs = {} # argname -> FixtureDef
fixtureinfo = pyfuncitem._fixtureinfo fixtureinfo = pyfuncitem._fixtureinfo
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
@ -450,8 +449,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
raise raise
# remove indent to prevent the python3 exception # remove indent to prevent the python3 exception
# from leaking into the call # from leaking into the call
result = self._getfixturevalue(fixturedef) self._compute_fixture_value(fixturedef)
self._fixture_values[argname] = result
self._fixture_defs[argname] = fixturedef self._fixture_defs[argname] = fixturedef
return fixturedef return fixturedef
@ -466,7 +464,14 @@ class FixtureRequest(FuncargnamesCompatAttr):
values.append(fixturedef) values.append(fixturedef)
current = current._parent_request current = current._parent_request
def _getfixturevalue(self, fixturedef): def _compute_fixture_value(self, fixturedef):
"""
Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
force the FixtureDef object to throw away any previous results and compute a new fixture value, which
will be stored into the FixtureDef object itself.
:param FixtureDef fixturedef:
"""
# prepare a subrequest object before calling fixture function # prepare a subrequest object before calling fixture function
# (latter managed by fixturedef) # (latter managed by fixturedef)
argname = fixturedef.argname argname = fixturedef.argname
@ -515,12 +520,11 @@ class FixtureRequest(FuncargnamesCompatAttr):
exc_clear() exc_clear()
try: try:
# call the fixture function # call the fixture function
val = fixturedef.execute(request=subrequest) fixturedef.execute(request=subrequest)
finally: finally:
# if fixture function failed it might have registered finalizers # if fixture function failed it might have registered finalizers
self.session._setupstate.addfinalizer(functools.partial(fixturedef.finish, request=subrequest), self.session._setupstate.addfinalizer(functools.partial(fixturedef.finish, request=subrequest),
subrequest.node) subrequest.node)
return val
def _check_scope(self, argname, invoking_scope, requested_scope): def _check_scope(self, argname, invoking_scope, requested_scope):
if argname == "request": if argname == "request":
@ -573,7 +577,6 @@ class SubRequest(FixtureRequest):
self.scope = scope self.scope = scope
self._fixturedef = fixturedef self._fixturedef = fixturedef
self._pyfuncitem = request._pyfuncitem self._pyfuncitem = request._pyfuncitem
self._fixture_values = request._fixture_values
self._fixture_defs = request._fixture_defs self._fixture_defs = request._fixture_defs
self._arg2fixturedefs = request._arg2fixturedefs self._arg2fixturedefs = request._arg2fixturedefs
self._arg2index = request._arg2index self._arg2index = request._arg2index

View File

@ -12,22 +12,31 @@ hookspec = HookspecMarker("pytest")
@hookspec(historic=True) @hookspec(historic=True)
def pytest_addhooks(pluginmanager): def pytest_addhooks(pluginmanager):
"""called at plugin registration time to allow adding new hooks via a call to """called at plugin registration time to allow adding new hooks via a call to
pluginmanager.add_hookspecs(module_or_class, prefix).""" ``pluginmanager.add_hookspecs(module_or_class, prefix)``.
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
"""
@hookspec(historic=True) @hookspec(historic=True)
def pytest_namespace(): def pytest_namespace():
""" """
DEPRECATED: this hook causes direct monkeypatching on pytest, its use is strongly discouraged (**Deprecated**) this hook causes direct monkeypatching on pytest, its use is strongly discouraged
return dict of name->object to be made globally available in return dict of name->object to be made globally available in
the pytest namespace. This hook is called at plugin registration the pytest namespace.
time.
This hook is called at plugin registration time.
""" """
@hookspec(historic=True) @hookspec(historic=True)
def pytest_plugin_registered(plugin, manager): def pytest_plugin_registered(plugin, manager):
""" a new pytest plugin got registered. """ """ a new pytest plugin got registered.
:param plugin: the plugin module or instance
:param _pytest.config.PytestPluginManager manager: pytest plugin manager
"""
@hookspec(historic=True) @hookspec(historic=True)
@ -41,7 +50,7 @@ def pytest_addoption(parser):
files situated at the tests root directory due to how pytest files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`. :ref:`discovers plugins during startup <pluginorder>`.
:arg parser: To add command line options, call :arg _pytest.config.Parser parser: To add command line options, call
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`. :py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
To add ini-file values call :py:func:`parser.addini(...) To add ini-file values call :py:func:`parser.addini(...)
<_pytest.config.Parser.addini>`. <_pytest.config.Parser.addini>`.
@ -56,8 +65,7 @@ def pytest_addoption(parser):
a value read from an ini-style file. a value read from an ini-style file.
The config object is passed around on many internal objects via the ``.config`` The config object is passed around on many internal objects via the ``.config``
attribute or can be retrieved as the ``pytestconfig`` fixture or accessed attribute or can be retrieved as the ``pytestconfig`` fixture.
via (deprecated) ``pytest.config``.
""" """
@ -72,8 +80,7 @@ def pytest_configure(config):
After that, the hook is called for other conftest files as they are After that, the hook is called for other conftest files as they are
imported. imported.
:arg config: pytest config object :arg _pytest.config.Config config: pytest config object
:type config: _pytest.config.Config
""" """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@ -87,11 +94,22 @@ def pytest_configure(config):
def pytest_cmdline_parse(pluginmanager, args): def pytest_cmdline_parse(pluginmanager, args):
"""return initialized config object, parsing the specified args. """return initialized config object, parsing the specified args.
Stops at first non-None result, see :ref:`firstresult` """ Stops at first non-None result, see :ref:`firstresult`
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
:param list[str] args: list of arguments passed on the command line
"""
def pytest_cmdline_preparse(config, args): def pytest_cmdline_preparse(config, args):
"""(deprecated) modify command line arguments before option parsing. """ """(**Deprecated**) modify command line arguments before option parsing.
This hook is considered deprecated and will be removed in a future pytest version. Consider
using :func:`pytest_load_initial_conftests` instead.
:param _pytest.config.Config config: pytest config object
:param list[str] args: list of arguments passed on the command line
"""
@hookspec(firstresult=True) @hookspec(firstresult=True)
@ -99,12 +117,20 @@ def pytest_cmdline_main(config):
""" called for performing the main command line action. The default """ called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop. implementation will invoke the configure hooks and runtest_mainloop.
Stops at first non-None result, see :ref:`firstresult` """ Stops at first non-None result, see :ref:`firstresult`
:param _pytest.config.Config config: pytest config object
"""
def pytest_load_initial_conftests(early_config, parser, args): def pytest_load_initial_conftests(early_config, parser, args):
""" implements the loading of initial conftest files ahead """ implements the loading of initial conftest files ahead
of command line option parsing. """ of command line option parsing.
:param _pytest.config.Config early_config: pytest config object
:param list[str] args: list of arguments passed on the command line
:param _pytest.config.Parser parser: to add command line options
"""
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@ -113,18 +139,29 @@ def pytest_load_initial_conftests(early_config, parser, args):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_collection(session): def pytest_collection(session):
""" perform the collection protocol for the given session. """Perform the collection protocol for the given session.
Stops at first non-None result, see :ref:`firstresult` """ Stops at first non-None result, see :ref:`firstresult`.
:param _pytest.main.Session session: the pytest session object
"""
def pytest_collection_modifyitems(session, config, items): def pytest_collection_modifyitems(session, config, items):
""" called after collection has been performed, may filter or re-order """ called after collection has been performed, may filter or re-order
the items in-place.""" the items in-place.
:param _pytest.main.Session session: the pytest session object
:param _pytest.config.Config config: pytest config object
:param List[_pytest.main.Item] items: list of item objects
"""
def pytest_collection_finish(session): def pytest_collection_finish(session):
""" called after collection has been performed and modified. """ """ called after collection has been performed and modified.
:param _pytest.main.Session session: the pytest session object
"""
@hookspec(firstresult=True) @hookspec(firstresult=True)
@ -134,6 +171,9 @@ def pytest_ignore_collect(path, config):
more specific hooks. more specific hooks.
Stops at first non-None result, see :ref:`firstresult` Stops at first non-None result, see :ref:`firstresult`
:param str path: the path to analyze
:param _pytest.config.Config config: pytest config object
""" """
@ -141,12 +181,18 @@ def pytest_ignore_collect(path, config):
def pytest_collect_directory(path, parent): def pytest_collect_directory(path, parent):
""" called before traversing a directory for collection files. """ called before traversing a directory for collection files.
Stops at first non-None result, see :ref:`firstresult` """ Stops at first non-None result, see :ref:`firstresult`
:param str path: the path to analyze
"""
def pytest_collect_file(path, parent): def pytest_collect_file(path, parent):
""" return collection Node or None for the given path. Any new node """ return collection Node or None for the given path. Any new node
needs to have the specified ``parent`` as a parent.""" needs to have the specified ``parent`` as a parent.
:param str path: the path to collect
"""
# logging hooks for collection # logging hooks for collection
@ -212,7 +258,12 @@ def pytest_make_parametrize_id(config, val, argname):
by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
The parameter name is available as ``argname``, if required. The parameter name is available as ``argname``, if required.
Stops at first non-None result, see :ref:`firstresult` """ Stops at first non-None result, see :ref:`firstresult`
:param _pytest.config.Config config: pytest config object
:param val: the parametrized value
:param str argname: the automatic parameter name produced by pytest
"""
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# generic runtest related hooks # generic runtest related hooks
@ -224,11 +275,14 @@ def pytest_runtestloop(session):
""" called for performing the main runtest loop """ called for performing the main runtest loop
(after collection finished). (after collection finished).
Stops at first non-None result, see :ref:`firstresult` """ Stops at first non-None result, see :ref:`firstresult`
:param _pytest.main.Session session: the pytest session object
"""
def pytest_itemstart(item, node): def pytest_itemstart(item, node):
""" (deprecated, use pytest_runtest_logstart). """ """(**Deprecated**) use pytest_runtest_logstart. """
@hookspec(firstresult=True) @hookspec(firstresult=True)
@ -307,15 +361,25 @@ def pytest_fixture_post_finalizer(fixturedef, request):
def pytest_sessionstart(session): def pytest_sessionstart(session):
""" before session.main() is called. """ """ before session.main() is called.
:param _pytest.main.Session session: the pytest session object
"""
def pytest_sessionfinish(session, exitstatus): def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """ """ whole test run finishes.
:param _pytest.main.Session session: the pytest session object
:param int exitstatus: the status which pytest will return to the system
"""
def pytest_unconfigure(config): def pytest_unconfigure(config):
""" called before test process is exited. """ """ called before test process is exited.
:param _pytest.config.Config config: pytest config object
"""
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@ -329,6 +393,8 @@ def pytest_assertrepr_compare(config, op, left, right):
of strings. The strings will be joined by newlines but any newlines of strings. The strings will be joined by newlines but any newlines
*in* a string will be escaped. Note that all but the first line will *in* a string will be escaped. Note that all but the first line will
be indented slightly, the intention is for the first line to be a summary. be indented slightly, the intention is for the first line to be a summary.
:param _pytest.config.Config config: pytest config object
""" """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@ -339,7 +405,7 @@ def pytest_assertrepr_compare(config, op, left, right):
def pytest_report_header(config, startdir): def pytest_report_header(config, startdir):
""" return a string or list of strings to be displayed as header info for terminal reporting. """ return a string or list of strings to be displayed as header info for terminal reporting.
:param config: the pytest config object. :param _pytest.config.Config config: pytest config object
:param startdir: py.path object with the starting dir :param startdir: py.path object with the starting dir
.. note:: .. note::
@ -358,7 +424,7 @@ def pytest_report_collectionfinish(config, startdir, items):
This strings will be displayed after the standard "collected X items" message. This strings will be displayed after the standard "collected X items" message.
:param config: the pytest config object. :param _pytest.config.Config config: pytest config object
:param startdir: py.path object with the starting dir :param startdir: py.path object with the starting dir
:param items: list of pytest items that are going to be executed; this list should not be modified. :param items: list of pytest items that are going to be executed; this list should not be modified.
""" """
@ -418,6 +484,5 @@ def pytest_enter_pdb(config):
""" called upon pdb.set_trace(), can be used by plugins to take special """ called upon pdb.set_trace(), can be used by plugins to take special
action just before the python debugger enters in interactive mode. action just before the python debugger enters in interactive mode.
:arg config: pytest config object :param _pytest.config.Config config: pytest config object
:type config: _pytest.config.Config
""" """

View File

@ -79,39 +79,28 @@ def pytest_addoption(parser):
@contextmanager @contextmanager
def logging_using_handler(handler, logger=None): def catching_logs(handler, formatter=None, level=logging.NOTSET):
"""Context manager that safely registers a given handler."""
logger = logger or logging.getLogger(logger)
if handler in logger.handlers: # reentrancy
# Adding the same handler twice would confuse logging system.
# Just don't do that.
yield
else:
logger.addHandler(handler)
try:
yield
finally:
logger.removeHandler(handler)
@contextmanager
def catching_logs(handler, formatter=None,
level=logging.NOTSET, logger=None):
"""Context manager that prepares the whole logging machinery properly.""" """Context manager that prepares the whole logging machinery properly."""
logger = logger or logging.getLogger(logger) root_logger = logging.getLogger()
if formatter is not None: if formatter is not None:
handler.setFormatter(formatter) handler.setFormatter(formatter)
handler.setLevel(level) handler.setLevel(level)
with logging_using_handler(handler, logger): # Adding the same handler twice would confuse logging system.
orig_level = logger.level # Just don't do that.
logger.setLevel(min(orig_level, level)) add_new_handler = handler not in root_logger.handlers
try:
yield handler if add_new_handler:
finally: root_logger.addHandler(handler)
logger.setLevel(orig_level) orig_level = root_logger.level
root_logger.setLevel(min(orig_level, level))
try:
yield handler
finally:
root_logger.setLevel(orig_level)
if add_new_handler:
root_logger.removeHandler(handler)
class LogCaptureHandler(logging.StreamHandler): class LogCaptureHandler(logging.StreamHandler):

View File

@ -1,8 +1,10 @@
""" core implementation of testing process: init, session, runtest loop. """ """ core implementation of testing process: init, session, runtest loop. """
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import contextlib
import functools import functools
import os import os
import pkgutil
import six import six
import sys import sys
@ -206,6 +208,46 @@ def pytest_ignore_collect(path, config):
return False return False
@contextlib.contextmanager
def _patched_find_module():
"""Patch bug in pkgutil.ImpImporter.find_module
When using pkgutil.find_loader on python<3.4 it removes symlinks
from the path due to a call to os.path.realpath. This is not consistent
with actually doing the import (in these versions, pkgutil and __import__
did not share the same underlying code). This can break conftest
discovery for pytest where symlinks are involved.
The only supported python<3.4 by pytest is python 2.7.
"""
if six.PY2: # python 3.4+ uses importlib instead
def find_module_patched(self, fullname, path=None):
# Note: we ignore 'path' argument since it is only used via meta_path
subname = fullname.split(".")[-1]
if subname != fullname and self.path is None:
return None
if self.path is None:
path = None
else:
# original: path = [os.path.realpath(self.path)]
path = [self.path]
try:
file, filename, etc = pkgutil.imp.find_module(subname,
path)
except ImportError:
return None
return pkgutil.ImpLoader(fullname, file, filename, etc)
old_find_module = pkgutil.ImpImporter.find_module
pkgutil.ImpImporter.find_module = find_module_patched
try:
yield
finally:
pkgutil.ImpImporter.find_module = old_find_module
else:
yield
class FSHookProxy: class FSHookProxy:
def __init__(self, fspath, pm, remove_mods): def __init__(self, fspath, pm, remove_mods):
self.fspath = fspath self.fspath = fspath
@ -728,9 +770,10 @@ class Session(FSCollector):
"""Convert a dotted module name to path. """Convert a dotted module name to path.
""" """
import pkgutil
try: try:
loader = pkgutil.find_loader(x) with _patched_find_module():
loader = pkgutil.find_loader(x)
except ImportError: except ImportError:
return x return x
if loader is None: if loader is None:
@ -738,7 +781,8 @@ class Session(FSCollector):
# This method is sometimes invoked when AssertionRewritingHook, which # This method is sometimes invoked when AssertionRewritingHook, which
# does not define a get_filename method, is already in place: # does not define a get_filename method, is already in place:
try: try:
path = loader.get_filename(x) with _patched_find_module():
path = loader.get_filename(x)
except AttributeError: except AttributeError:
# Retrieve path from AssertionRewritingHook: # Retrieve path from AssertionRewritingHook:
path = loader.modules[x][0].co_filename path = loader.modules[x][0].co_filename

View File

@ -347,7 +347,7 @@ class RunResult:
: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 ``stdout.fnmatch_lines()`` reconstruct stdout or the commonly used ``stdout.fnmatch_lines()``
method method
:stderrr: :py:class:`LineMatcher` of stderr :stderr: :py:class:`LineMatcher` of stderr
:duration: duration in seconds :duration: duration in seconds
""" """

View File

@ -153,7 +153,8 @@ class TerminalReporter:
self.hasmarkup = self._tw.hasmarkup self.hasmarkup = self._tw.hasmarkup
self.isatty = file.isatty() self.isatty = file.isatty()
self._progress_items_reported = 0 self._progress_items_reported = 0
self._show_progress_info = self.config.getini('console_output_style') == 'progress' self._show_progress_info = (self.config.getoption('capture') != 'no' and
self.config.getini('console_output_style') == 'progress')
def hasopt(self, char): def hasopt(self, char):
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char) char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)

1
changelog/2981.bugfix Normal file
View File

@ -0,0 +1 @@
Fix **memory leak** where objects returned by fixtures were never destructed by the garbage collector.

1
changelog/2985.bugfix Normal file
View File

@ -0,0 +1 @@
Fix conversion of pyargs to filename to not convert symlinks and not use deprecated features on Python 3.

1
changelog/2995.bugfix Normal file
View File

@ -0,0 +1 @@
``PYTEST_DONT_REWRITE`` is now checked for plugins too rather than only for test modules.

1
changelog/3021.trivial Normal file
View File

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

1
changelog/3038.feature Normal file
View File

@ -0,0 +1 @@
Console output fallsback to "classic" mode when capture is disabled (``-s``), otherwise the output gets garbled to the point of being useless.

View File

@ -616,6 +616,7 @@ Collection hooks
``pytest`` calls the following hooks for collecting files and directories: ``pytest`` calls the following hooks for collecting files and directories:
.. autofunction:: pytest_collection
.. autofunction:: pytest_ignore_collect .. autofunction:: pytest_ignore_collect
.. autofunction:: pytest_collect_directory .. autofunction:: pytest_collect_directory
.. autofunction:: pytest_collect_file .. autofunction:: pytest_collect_file
@ -687,6 +688,14 @@ Reference of objects involved in hooks
:members: :members:
:show-inheritance: :show-inheritance:
.. autoclass:: _pytest.main.FSCollector()
:members:
:show-inheritance:
.. autoclass:: _pytest.main.Session()
:members:
:show-inheritance:
.. autoclass:: _pytest.main.Item() .. autoclass:: _pytest.main.Item()
:members: :members:
:show-inheritance: :show-inheritance:

View File

@ -151,7 +151,7 @@ def publish_release(ctx, version, user, pypi_name):
@invoke.task(help={ @invoke.task(help={
'version': 'version being released', 'version': 'version being released',
'write_out': 'write changes to the actial changelog' 'write_out': 'write changes to the actual changelog'
}) })
def changelog(ctx, version, write_out=False): def changelog(ctx, version, write_out=False):
if write_out: if write_out:

View File

@ -3,6 +3,8 @@ from __future__ import absolute_import, division, print_function
import os import os
import sys import sys
import six
import _pytest._code import _pytest._code
import py import py
import pytest import pytest
@ -645,6 +647,69 @@ class TestInvocationVariants(object):
"*1 passed*" "*1 passed*"
]) ])
@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks")
def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
"""
test --pyargs option with packages with path containing symlink can
have conftest.py in their package (#2985)
"""
monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', raising=False)
search_path = ["lib", os.path.join("local", "lib")]
dirname = "lib"
d = testdir.mkdir(dirname)
foo = d.mkdir("foo")
foo.ensure("__init__.py")
lib = foo.mkdir('bar')
lib.ensure("__init__.py")
lib.join("test_bar.py"). \
write("def test_bar(): pass\n"
"def test_other(a_fixture):pass")
lib.join("conftest.py"). \
write("import pytest\n"
"@pytest.fixture\n"
"def a_fixture():pass")
d_local = testdir.mkdir("local")
symlink_location = os.path.join(str(d_local), "lib")
if six.PY2:
os.symlink(str(d), symlink_location)
else:
os.symlink(str(d), symlink_location, target_is_directory=True)
# The structure of the test directory is now:
# .
# ├── local
# │ └── lib -> ../lib
# └── lib
# └── foo
# ├── __init__.py
# └── bar
# ├── __init__.py
# ├── conftest.py
# └── test_bar.py
def join_pythonpath(*dirs):
cur = os.getenv('PYTHONPATH')
if cur:
dirs += (cur,)
return os.pathsep.join(str(p) for p in dirs)
monkeypatch.setenv('PYTHONPATH', join_pythonpath(*search_path))
for p in search_path:
monkeypatch.syspath_prepend(p)
# module picked up in symlink-ed directory:
result = testdir.runpytest("--pyargs", "-v", "foo.bar")
testdir.chdir()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*lib/foo/bar/test_bar.py::test_bar*PASSED*",
"*lib/foo/bar/test_bar.py::test_other*PASSED*",
"*2 passed*"
])
def test_cmdline_python_package_not_exists(self, testdir): def test_cmdline_python_package_not_exists(self, testdir):
result = testdir.runpytest("--pyargs", "tpkgwhatv") result = testdir.runpytest("--pyargs", "tpkgwhatv")
assert result.ret assert result.ret
@ -848,3 +913,46 @@ def test_deferred_hook_checking(testdir):
}) })
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines(['* 1 passed *']) result.stdout.fnmatch_lines(['* 1 passed *'])
def test_fixture_values_leak(testdir):
"""Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected
life-times (#2981).
"""
testdir.makepyfile("""
import attr
import gc
import pytest
import weakref
@attr.s
class SomeObj(object):
name = attr.ib()
fix_of_test1_ref = None
session_ref = None
@pytest.fixture(scope='session')
def session_fix():
global session_ref
obj = SomeObj(name='session-fixture')
session_ref = weakref.ref(obj)
return obj
@pytest.fixture
def fix(session_fix):
global fix_of_test1_ref
obj = SomeObj(name='local-fixture')
fix_of_test1_ref = weakref.ref(obj)
return obj
def test1(fix):
assert fix_of_test1_ref() is fix
def test2():
gc.collect()
# fixture "fix" created during test1 must have been destroyed by now
assert fix_of_test1_ref() is None
""")
result = testdir.runpytest()
result.stdout.fnmatch_lines(['* 2 passed *'])

View File

@ -128,6 +128,16 @@ class TestAssertionRewrite(object):
assert len(m.body) == 1 assert len(m.body) == 1
assert m.body[0].msg is None assert m.body[0].msg is None
def test_dont_rewrite_plugin(self, testdir):
contents = {
"conftest.py": "pytest_plugins = 'plugin'; import plugin",
"plugin.py": "'PYTEST_DONT_REWRITE'",
"test_foo.py": "def test_foo(): pass",
}
testdir.makepyfile(**contents)
result = testdir.runpytest_subprocess()
assert "warnings" not in "".join(result.outlines)
def test_name(self): def test_name(self):
def f(): def f():
assert False assert False

View File

@ -1037,3 +1037,11 @@ class TestProgress:
r'\[gw\d\] \[\s*\d+%\] PASSED test_foo.py::test_foo\[1\]', r'\[gw\d\] \[\s*\d+%\] PASSED test_foo.py::test_foo\[1\]',
r'\[gw\d\] \[\s*\d+%\] PASSED test_foobar.py::test_foobar\[1\]', r'\[gw\d\] \[\s*\d+%\] PASSED test_foobar.py::test_foobar\[1\]',
]) ])
def test_capture_no(self, many_tests_file, testdir):
output = testdir.runpytest('-s')
output.stdout.re_match_lines([
r'test_bar.py \.{10}',
r'test_foo.py \.{5}',
r'test_foobar.py \.{5}',
])