Merge pull request #3041 from segevfiner/capture-no-disable-progress
Use classic console output when -s is used
This commit is contained in:
commit
d87279115d
1
AUTHORS
1
AUTHORS
|
@ -74,6 +74,7 @@ Grig Gheorghiu
|
|||
Grigorii Eremeev (budulianin)
|
||||
Guido Wesdorp
|
||||
Harald Armin Massa
|
||||
Henk-Jaap Wagenaar
|
||||
Hugo van Kemenade
|
||||
Hui Wang (coldnight)
|
||||
Ian Bicking
|
||||
|
|
|
@ -179,11 +179,13 @@ class AssertionRewritingHook(object):
|
|||
The named module or package as well as any nested modules will
|
||||
be rewritten on import.
|
||||
"""
|
||||
already_imported = set(names).intersection(set(sys.modules))
|
||||
if already_imported:
|
||||
for name in already_imported:
|
||||
if name not in self._rewritten_names:
|
||||
self._warn_already_imported(name)
|
||||
already_imported = (set(names)
|
||||
.intersection(sys.modules)
|
||||
.difference(self._rewritten_names))
|
||||
for name in already_imported:
|
||||
if not AssertionRewriter.is_rewrite_disabled(
|
||||
sys.modules[name].__doc__ or ""):
|
||||
self._warn_already_imported(name)
|
||||
self._must_rewrite.update(names)
|
||||
|
||||
def _warn_already_imported(self, name):
|
||||
|
@ -635,7 +637,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
not isinstance(field, ast.expr)):
|
||||
nodes.append(field)
|
||||
|
||||
def is_rewrite_disabled(self, docstring):
|
||||
@staticmethod
|
||||
def is_rewrite_disabled(docstring):
|
||||
return "PYTEST_DONT_REWRITE" in docstring
|
||||
|
||||
def variable(self):
|
||||
|
|
|
@ -267,7 +267,6 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
|||
self.fixturename = None
|
||||
#: Scope string, one of "function", "class", "module", "session"
|
||||
self.scope = "function"
|
||||
self._fixture_values = {} # argname -> fixture value
|
||||
self._fixture_defs = {} # argname -> FixtureDef
|
||||
fixtureinfo = pyfuncitem._fixtureinfo
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
|
||||
|
@ -450,8 +449,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
|||
raise
|
||||
# remove indent to prevent the python3 exception
|
||||
# from leaking into the call
|
||||
result = self._getfixturevalue(fixturedef)
|
||||
self._fixture_values[argname] = result
|
||||
self._compute_fixture_value(fixturedef)
|
||||
self._fixture_defs[argname] = fixturedef
|
||||
return fixturedef
|
||||
|
||||
|
@ -466,7 +464,14 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
|||
values.append(fixturedef)
|
||||
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
|
||||
# (latter managed by fixturedef)
|
||||
argname = fixturedef.argname
|
||||
|
@ -515,12 +520,11 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
|||
exc_clear()
|
||||
try:
|
||||
# call the fixture function
|
||||
val = fixturedef.execute(request=subrequest)
|
||||
fixturedef.execute(request=subrequest)
|
||||
finally:
|
||||
# if fixture function failed it might have registered finalizers
|
||||
self.session._setupstate.addfinalizer(functools.partial(fixturedef.finish, request=subrequest),
|
||||
subrequest.node)
|
||||
return val
|
||||
|
||||
def _check_scope(self, argname, invoking_scope, requested_scope):
|
||||
if argname == "request":
|
||||
|
@ -573,7 +577,6 @@ class SubRequest(FixtureRequest):
|
|||
self.scope = scope
|
||||
self._fixturedef = fixturedef
|
||||
self._pyfuncitem = request._pyfuncitem
|
||||
self._fixture_values = request._fixture_values
|
||||
self._fixture_defs = request._fixture_defs
|
||||
self._arg2fixturedefs = request._arg2fixturedefs
|
||||
self._arg2index = request._arg2index
|
||||
|
|
|
@ -12,22 +12,31 @@ hookspec = HookspecMarker("pytest")
|
|||
@hookspec(historic=True)
|
||||
def pytest_addhooks(pluginmanager):
|
||||
"""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)
|
||||
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
|
||||
the pytest namespace. This hook is called at plugin registration
|
||||
time.
|
||||
the pytest namespace.
|
||||
|
||||
This hook is called at plugin registration time.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
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)
|
||||
|
@ -41,7 +50,7 @@ def pytest_addoption(parser):
|
|||
files situated at the tests root directory due to how pytest
|
||||
: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>`.
|
||||
To add ini-file values call :py:func:`parser.addini(...)
|
||||
<_pytest.config.Parser.addini>`.
|
||||
|
@ -56,8 +65,7 @@ def pytest_addoption(parser):
|
|||
a value read from an ini-style file.
|
||||
|
||||
The config object is passed around on many internal objects via the ``.config``
|
||||
attribute or can be retrieved as the ``pytestconfig`` fixture or accessed
|
||||
via (deprecated) ``pytest.config``.
|
||||
attribute or can be retrieved as the ``pytestconfig`` fixture.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -72,8 +80,7 @@ def pytest_configure(config):
|
|||
After that, the hook is called for other conftest files as they are
|
||||
imported.
|
||||
|
||||
:arg config: pytest config object
|
||||
:type config: _pytest.config.Config
|
||||
:arg _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
@ -87,11 +94,22 @@ def pytest_configure(config):
|
|||
def pytest_cmdline_parse(pluginmanager, 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):
|
||||
"""(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)
|
||||
|
@ -99,12 +117,20 @@ def pytest_cmdline_main(config):
|
|||
""" called for performing the main command line action. The default
|
||||
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):
|
||||
""" 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)
|
||||
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):
|
||||
""" 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):
|
||||
""" 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)
|
||||
|
@ -134,6 +171,9 @@ def pytest_ignore_collect(path, config):
|
|||
more specific hooks.
|
||||
|
||||
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):
|
||||
""" 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):
|
||||
""" 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
|
||||
|
||||
|
@ -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``.
|
||||
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
|
||||
|
@ -224,11 +275,14 @@ def pytest_runtestloop(session):
|
|||
""" called for performing the main runtest loop
|
||||
(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):
|
||||
""" (deprecated, use pytest_runtest_logstart). """
|
||||
"""(**Deprecated**) use pytest_runtest_logstart. """
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
|
@ -307,15 +361,25 @@ def pytest_fixture_post_finalizer(fixturedef, request):
|
|||
|
||||
|
||||
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):
|
||||
""" 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):
|
||||
""" 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
|
||||
*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.
|
||||
|
||||
: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):
|
||||
""" 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
|
||||
|
||||
.. note::
|
||||
|
@ -358,7 +424,7 @@ def pytest_report_collectionfinish(config, startdir, items):
|
|||
|
||||
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 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
|
||||
action just before the python debugger enters in interactive mode.
|
||||
|
||||
:arg config: pytest config object
|
||||
:type config: _pytest.config.Config
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
|
|
@ -79,39 +79,28 @@ def pytest_addoption(parser):
|
|||
|
||||
|
||||
@contextmanager
|
||||
def logging_using_handler(handler, logger=None):
|
||||
"""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):
|
||||
def catching_logs(handler, formatter=None, level=logging.NOTSET):
|
||||
"""Context manager that prepares the whole logging machinery properly."""
|
||||
logger = logger or logging.getLogger(logger)
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
if formatter is not None:
|
||||
handler.setFormatter(formatter)
|
||||
handler.setLevel(level)
|
||||
|
||||
with logging_using_handler(handler, logger):
|
||||
orig_level = logger.level
|
||||
logger.setLevel(min(orig_level, level))
|
||||
try:
|
||||
yield handler
|
||||
finally:
|
||||
logger.setLevel(orig_level)
|
||||
# Adding the same handler twice would confuse logging system.
|
||||
# Just don't do that.
|
||||
add_new_handler = handler not in root_logger.handlers
|
||||
|
||||
if add_new_handler:
|
||||
root_logger.addHandler(handler)
|
||||
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):
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
""" core implementation of testing process: init, session, runtest loop. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
import six
|
||||
import sys
|
||||
|
||||
|
@ -206,6 +208,46 @@ def pytest_ignore_collect(path, config):
|
|||
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:
|
||||
def __init__(self, fspath, pm, remove_mods):
|
||||
self.fspath = fspath
|
||||
|
@ -728,9 +770,10 @@ class Session(FSCollector):
|
|||
"""Convert a dotted module name to path.
|
||||
|
||||
"""
|
||||
import pkgutil
|
||||
|
||||
try:
|
||||
loader = pkgutil.find_loader(x)
|
||||
with _patched_find_module():
|
||||
loader = pkgutil.find_loader(x)
|
||||
except ImportError:
|
||||
return x
|
||||
if loader is None:
|
||||
|
@ -738,7 +781,8 @@ class Session(FSCollector):
|
|||
# This method is sometimes invoked when AssertionRewritingHook, which
|
||||
# does not define a get_filename method, is already in place:
|
||||
try:
|
||||
path = loader.get_filename(x)
|
||||
with _patched_find_module():
|
||||
path = loader.get_filename(x)
|
||||
except AttributeError:
|
||||
# Retrieve path from AssertionRewritingHook:
|
||||
path = loader.modules[x][0].co_filename
|
||||
|
|
|
@ -347,7 +347,7 @@ class RunResult:
|
|||
:stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to
|
||||
reconstruct stdout or the commonly used ``stdout.fnmatch_lines()``
|
||||
method
|
||||
:stderrr: :py:class:`LineMatcher` of stderr
|
||||
:stderr: :py:class:`LineMatcher` of stderr
|
||||
:duration: duration in seconds
|
||||
|
||||
"""
|
||||
|
|
|
@ -153,7 +153,8 @@ class TerminalReporter:
|
|||
self.hasmarkup = self._tw.hasmarkup
|
||||
self.isatty = file.isatty()
|
||||
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):
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Fix **memory leak** where objects returned by fixtures were never destructed by the garbage collector.
|
|
@ -0,0 +1 @@
|
|||
Fix conversion of pyargs to filename to not convert symlinks and not use deprecated features on Python 3.
|
|
@ -0,0 +1 @@
|
|||
``PYTEST_DONT_REWRITE`` is now checked for plugins too rather than only for test modules.
|
|
@ -0,0 +1 @@
|
|||
Code cleanup.
|
|
@ -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.
|
|
@ -616,6 +616,7 @@ Collection hooks
|
|||
|
||||
``pytest`` calls the following hooks for collecting files and directories:
|
||||
|
||||
.. autofunction:: pytest_collection
|
||||
.. autofunction:: pytest_ignore_collect
|
||||
.. autofunction:: pytest_collect_directory
|
||||
.. autofunction:: pytest_collect_file
|
||||
|
@ -687,6 +688,14 @@ Reference of objects involved in hooks
|
|||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.main.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.main.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.main.Item()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
|
|
@ -151,7 +151,7 @@ def publish_release(ctx, version, user, pypi_name):
|
|||
|
||||
@invoke.task(help={
|
||||
'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):
|
||||
if write_out:
|
||||
|
|
|
@ -3,6 +3,8 @@ from __future__ import absolute_import, division, print_function
|
|||
import os
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
|
@ -645,6 +647,69 @@ class TestInvocationVariants(object):
|
|||
"*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):
|
||||
result = testdir.runpytest("--pyargs", "tpkgwhatv")
|
||||
assert result.ret
|
||||
|
@ -848,3 +913,46 @@ def test_deferred_hook_checking(testdir):
|
|||
})
|
||||
result = testdir.runpytest()
|
||||
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 *'])
|
||||
|
|
|
@ -128,6 +128,16 @@ class TestAssertionRewrite(object):
|
|||
assert len(m.body) == 1
|
||||
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 f():
|
||||
assert False
|
||||
|
|
|
@ -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_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}',
|
||||
])
|
||||
|
|
Loading…
Reference in New Issue