Merge pull request #4805 from nicoddemus/release-4.3.0

Release 4.3.0
This commit is contained in:
Bruno Oliveira 2019-02-18 17:18:50 -03:00 committed by GitHub
commit f0a9f9042f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 448 additions and 89 deletions

View File

@ -16,6 +16,7 @@ Allan Feldman
Aly Sivji Aly Sivji
Anatoly Bubenkoff Anatoly Bubenkoff
Anders Hovmöller Anders Hovmöller
Andras Mitzki
Andras Tim Andras Tim
Andrea Cimatoribus Andrea Cimatoribus
Andreas Zeidler Andreas Zeidler
@ -51,6 +52,7 @@ Charles Cloud
Charnjit SiNGH (CCSJ) Charnjit SiNGH (CCSJ)
Chris Lamb Chris Lamb
Christian Boelsen Christian Boelsen
Christian Fetzer
Christian Theunert Christian Theunert
Christian Tismer Christian Tismer
Christopher Gilling Christopher Gilling

View File

@ -18,6 +18,48 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 4.3.0 (2019-02-16)
=========================
Deprecations
------------
- `#4724 <https://github.com/pytest-dev/pytest/issues/4724>`_: ``pytest.warns()`` now emits a warning when it receives unknown keyword arguments.
This will be changed into an error in the future.
Features
--------
- `#2753 <https://github.com/pytest-dev/pytest/issues/2753>`_: Usage errors from argparse are mapped to pytest's ``UsageError``.
- `#3711 <https://github.com/pytest-dev/pytest/issues/3711>`_: Add the ``--ignore-glob`` parameter to exclude test-modules with Unix shell-style wildcards.
Add the ``collect_ignore_glob`` for ``conftest.py`` to exclude test-modules with Unix shell-style wildcards.
- `#4698 <https://github.com/pytest-dev/pytest/issues/4698>`_: The warning about Python 2.7 and 3.4 not being supported in pytest 5.0 has been removed.
In the end it was considered to be more
of a nuisance than actual utility and users of those Python versions shouldn't have problems as ``pip`` will not
install pytest 5.0 on those interpreters.
- `#4707 <https://github.com/pytest-dev/pytest/issues/4707>`_: With the help of new ``set_log_path()`` method there is a way to set ``log_file`` paths from hooks.
Bug Fixes
---------
- `#4651 <https://github.com/pytest-dev/pytest/issues/4651>`_: ``--help`` and ``--version`` are handled with ``UsageError``.
- `#4782 <https://github.com/pytest-dev/pytest/issues/4782>`_: Fix ``AssertionError`` with collection of broken symlinks with packages.
pytest 4.2.1 (2019-02-12) pytest 4.2.1 (2019-02-12)
========================= =========================

View File

@ -1 +0,0 @@
Fix ``AssertionError`` with collection of broken symlinks with packages.

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-4.3.0
release-4.2.1 release-4.2.1
release-4.2.0 release-4.2.0
release-4.1.1 release-4.1.1

View File

@ -0,0 +1,36 @@
pytest-4.3.0
=======================================
The pytest team is proud to announce the 4.3.0 release!
pytest is a mature Python testing tool with more than a 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
to take a look at the CHANGELOG:
https://docs.pytest.org/en/latest/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/latest/
As usual, you can upgrade from pypi via:
pip install -U pytest
Thanks to all who contributed to this release, among them:
* Andras Mitzki
* Anthony Sottile
* Bruno Oliveira
* Christian Fetzer
* Daniel Hahler
* Grygorii Iermolenko
* R. Alex Matevish
* Ronny Pfannschmidt
* cclauss
Happy testing,
The Pytest Development Team

View File

@ -436,10 +436,8 @@ Running it results in some skips if we don't have all the python interpreters in
.. code-block:: pytest .. code-block:: pytest
. $ pytest -rs -q multipython.py . $ pytest -rs -q multipython.py
...sss...sssssssss...sss... [100%] ........................... [100%]
========================= short test summary info ========================== 27 passed in 0.12 seconds
SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.4' not found
12 passed, 15 skipped in 0.12 seconds
Indirect parametrization of optional implementations/imports Indirect parametrization of optional implementations/imports
-------------------------------------------------------------------- --------------------------------------------------------------------

View File

@ -41,6 +41,9 @@ you will see that ``pytest`` only collects test-modules, which do not match the
========================= 5 passed in 0.02 seconds ========================= ========================= 5 passed in 0.02 seconds =========================
The ``--ignore-glob`` option allows to ignore test file paths based on Unix shell-style wildcards.
If you want to exclude test-modules that end with ``_01.py``, execute ``pytest`` with ``--ignore-glob='*_01.py'``.
Deselect tests during test collection Deselect tests during test collection
------------------------------------- -------------------------------------
@ -266,3 +269,17 @@ file will be left out:
collected 0 items collected 0 items
======================= no tests ran in 0.12 seconds ======================= ======================= no tests ran in 0.12 seconds =======================
It's also possible to ignore files based on Unix shell-style wildcards by adding
patterns to ``collect_ignore_glob``.
The following example ``conftest.py`` ignores the file ``setup.py`` and in
addition all files that end with ``*_py2.py`` when executed with a Python 3
interpreter::
# content of conftest.py
import sys
collect_ignore = ["setup.py"]
if sys.version_info[0] > 2:
collect_ignore_glob = ["*_py2.py"]

View File

@ -198,6 +198,9 @@ option names are:
* ``log_file_format`` * ``log_file_format``
* ``log_file_date_format`` * ``log_file_date_format``
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
is considered **experimental**.
.. _log_release_notes: .. _log_release_notes:
Release notes Release notes

View File

@ -797,6 +797,33 @@ Special Variables
pytest treats some global variables in a special manner when defined in a test module. pytest treats some global variables in a special manner when defined in a test module.
collect_ignore
~~~~~~~~~~~~~~
**Tutorial**: :ref:`customizing-test-collection`
Can be declared in *conftest.py files* to exclude test directories or modules.
Needs to be ``list[str]``.
.. code-block:: python
collect_ignore = ["setup.py"]
collect_ignore_glob
~~~~~~~~~~~~~~~~~~~
**Tutorial**: :ref:`customizing-test-collection`
Can be declared in *conftest.py files* to exclude test directories or modules
with Unix shell-style wildcards. Needs to be ``list[str]`` where ``str`` can
contain glob patterns.
.. code-block:: python
collect_ignore_glob = ["*_ignore.py"]
pytest_plugins pytest_plugins
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@ -651,8 +651,27 @@ class Config(object):
return self.pluginmanager.get_plugin("terminalreporter")._tw return self.pluginmanager.get_plugin("terminalreporter")._tw
def pytest_cmdline_parse(self, pluginmanager, args): def pytest_cmdline_parse(self, pluginmanager, args):
# REF1 assert self == pluginmanager.config, (self, pluginmanager.config) try:
self.parse(args) self.parse(args)
except UsageError:
# Handle --version and --help here in a minimal fashion.
# This gets done via helpconfig normally, but its
# pytest_cmdline_main is not called in case of errors.
if getattr(self.option, "version", False) or "--version" in args:
from _pytest.helpconfig import showversion
showversion(self)
elif (
getattr(self.option, "help", False) or "--help" in args or "-h" in args
):
self._parser._getparser().print_help()
sys.stdout.write(
"\nNOTE: displaying only minimal help due to UsageError.\n\n"
)
raise
return self return self
def notify_exception(self, excinfo, option=None): def notify_exception(self, excinfo, option=None):
@ -763,21 +782,32 @@ class Config(object):
for name in _iter_rewritable_modules(package_files): for name in _iter_rewritable_modules(package_files):
hook.mark_rewrite(name) hook.mark_rewrite(name)
def _validate_args(self, args): def _validate_args(self, args, via):
"""Validate known args.""" """Validate known args."""
self._parser.parse_known_and_unknown_args( self._parser._config_source_hint = via
args, namespace=copy.copy(self.option) try:
) self._parser.parse_known_and_unknown_args(
args, namespace=copy.copy(self.option)
)
finally:
del self._parser._config_source_hint
return args return args
def _preparse(self, args, addopts=True): def _preparse(self, args, addopts=True):
if addopts: if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "") env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
if len(env_addopts): if len(env_addopts):
args[:] = self._validate_args(shlex.split(env_addopts)) + args args[:] = (
self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
+ args
)
self._initini(args) self._initini(args)
if addopts: if addopts:
args[:] = self._validate_args(self.getini("addopts")) + args args[:] = (
self._validate_args(self.getini("addopts"), "via addopts config") + args
)
self._checkversion() self._checkversion()
self._consider_importhook(args) self._consider_importhook(args)
self.pluginmanager.consider_preparse(args) self.pluginmanager.consider_preparse(args)

View File

@ -1,12 +1,10 @@
import argparse import argparse
import sys as _sys
import warnings import warnings
from gettext import gettext as _
import py import py
import six import six
from ..main import EXIT_USAGEERROR from _pytest.config.exceptions import UsageError
FILE_OR_DIR = "file_or_dir" FILE_OR_DIR = "file_or_dir"
@ -337,14 +335,13 @@ class MyOptionParser(argparse.ArgumentParser):
self.extra_info = extra_info self.extra_info = extra_info
def error(self, message): def error(self, message):
"""error(message: string) """Transform argparse error message into UsageError."""
msg = "%s: error: %s" % (self.prog, message)
Prints a usage message incorporating the message to stderr and if hasattr(self._parser, "_config_source_hint"):
exits. msg = "%s (%s)" % (msg, self._parser._config_source_hint)
Overrides the method in parent class to change exit code"""
self.print_usage(_sys.stderr) raise UsageError(self.format_usage() + msg)
args = {"prog": self.prog, "message": message}
self.exit(EXIT_USAGEERROR, _("%(prog)s: error: %(message)s\n") % args)
def parse_args(self, args=None, namespace=None): def parse_args(self, args=None, namespace=None):
"""allow splitting of positional arguments""" """allow splitting of positional arguments"""

View File

@ -14,6 +14,7 @@ from __future__ import print_function
from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import RemovedInPytest4Warning from _pytest.warning_types import RemovedInPytest4Warning
from _pytest.warning_types import UnformattedWarning
YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored" YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored"
@ -87,3 +88,9 @@ PYTEST_LOGWARNING = PytestDeprecationWarning(
"pytest_logwarning is deprecated, no longer being called, and will be removed soon\n" "pytest_logwarning is deprecated, no longer being called, and will be removed soon\n"
"please use pytest_warning_captured instead" "please use pytest_warning_captured instead"
) )
PYTEST_WARNS_UNKNOWN_KWARGS = UnformattedWarning(
PytestDeprecationWarning,
"pytest.warns() got unexpected keyword arguments: {args!r}.\n"
"This will be an error in future versions.",
)

View File

@ -118,16 +118,20 @@ def pytest_cmdline_parse():
config.add_cleanup(unset_tracing) config.add_cleanup(unset_tracing)
def showversion(config):
p = py.path.local(pytest.__file__)
sys.stderr.write(
"This is pytest version %s, imported from %s\n" % (pytest.__version__, p)
)
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stderr.write(line + "\n")
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
if config.option.version: if config.option.version:
p = py.path.local(pytest.__file__) showversion(config)
sys.stderr.write(
"This is pytest version %s, imported from %s\n" % (pytest.__version__, p)
)
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stderr.write(line + "\n")
return 0 return 0
elif config.option.help: elif config.option.help:
config._do_configure() config._do_configure()

View File

@ -13,6 +13,7 @@ import six
import pytest import pytest
from _pytest.compat import dummy_context_manager from _pytest.compat import dummy_context_manager
from _pytest.config import create_terminal_writer from _pytest.config import create_terminal_writer
from _pytest.pathlib import Path
DEFAULT_LOG_FORMAT = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s" DEFAULT_LOG_FORMAT = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s"
@ -399,22 +400,21 @@ class LoggingPlugin(object):
) )
self.log_level = get_actual_log_level(config, "log_level") self.log_level = get_actual_log_level(config, "log_level")
self.log_file_level = get_actual_log_level(config, "log_file_level")
self.log_file_format = get_option_ini(config, "log_file_format", "log_format")
self.log_file_date_format = get_option_ini(
config, "log_file_date_format", "log_date_format"
)
self.log_file_formatter = logging.Formatter(
self.log_file_format, datefmt=self.log_file_date_format
)
log_file = get_option_ini(config, "log_file") log_file = get_option_ini(config, "log_file")
if log_file: if log_file:
self.log_file_level = get_actual_log_level(config, "log_file_level")
log_file_format = get_option_ini(config, "log_file_format", "log_format")
log_file_date_format = get_option_ini(
config, "log_file_date_format", "log_date_format"
)
# Each pytest runtests session will write to a clean logfile
self.log_file_handler = logging.FileHandler( self.log_file_handler = logging.FileHandler(
log_file, mode="w", encoding="UTF-8" log_file, mode="w", encoding="UTF-8"
) )
log_file_formatter = logging.Formatter( self.log_file_handler.setFormatter(self.log_file_formatter)
log_file_format, datefmt=log_file_date_format
)
self.log_file_handler.setFormatter(log_file_formatter)
else: else:
self.log_file_handler = None self.log_file_handler = None
@ -461,6 +461,27 @@ class LoggingPlugin(object):
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
) )
def set_log_path(self, fname):
"""Public method, which can set filename parameter for
Logging.FileHandler(). Also creates parent directory if
it does not exist.
.. warning::
Please considered as an experimental API.
"""
fname = Path(fname)
if not fname.is_absolute():
fname = Path(self._config.rootdir, fname)
if not fname.parent.exists():
fname.parent.mkdir(exist_ok=True, parents=True)
self.log_file_handler = logging.FileHandler(
str(fname), mode="w", encoding="UTF-8"
)
self.log_file_handler.setFormatter(self.log_file_formatter)
def _log_cli_enabled(self): def _log_cli_enabled(self):
"""Return True if log_cli should be considered enabled, either explicitly """Return True if log_cli should be considered enabled, either explicitly
or because --log-cli-level was given in the command-line. or because --log-cli-level was given in the command-line.
@ -483,6 +504,15 @@ class LoggingPlugin(object):
@contextmanager @contextmanager
def _runtest_for(self, item, when): def _runtest_for(self, item, when):
with self._runtest_for_main(item, when):
if self.log_file_handler is not None:
with catching_logs(self.log_file_handler, level=self.log_file_level):
yield
else:
yield
@contextmanager
def _runtest_for_main(self, item, when):
"""Implements the internals of pytest_runtest_xxx() hook.""" """Implements the internals of pytest_runtest_xxx() hook."""
with catching_logs( with catching_logs(
LogCaptureHandler(), formatter=self.formatter, level=self.log_level LogCaptureHandler(), formatter=self.formatter, level=self.log_level

View File

@ -4,6 +4,7 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
import contextlib import contextlib
import fnmatch
import functools import functools
import os import os
import pkgutil import pkgutil
@ -117,6 +118,12 @@ def pytest_addoption(parser):
metavar="path", metavar="path",
help="ignore path during collection (multi-allowed).", help="ignore path during collection (multi-allowed).",
) )
group.addoption(
"--ignore-glob",
action="append",
metavar="path",
help="ignore path pattern during collection (multi-allowed).",
)
group.addoption( group.addoption(
"--deselect", "--deselect",
action="append", action="append",
@ -296,6 +303,20 @@ def pytest_ignore_collect(path, config):
if py.path.local(path) in ignore_paths: if py.path.local(path) in ignore_paths:
return True return True
ignore_globs = config._getconftest_pathlist(
"collect_ignore_glob", path=path.dirpath()
)
ignore_globs = ignore_globs or []
excludeglobopt = config.getoption("ignore_glob")
if excludeglobopt:
ignore_globs.extend([py.path.local(x) for x in excludeglobopt])
if any(
fnmatch.fnmatch(six.text_type(path), six.text_type(glob))
for glob in ignore_globs
):
return True
allow_in_venv = config.getoption("collect_in_virtualenv") allow_in_venv = config.getoption("collect_in_virtualenv")
if not allow_in_venv and _in_venv(path): if not allow_in_venv and _in_venv(path):
return True return True

View File

@ -402,6 +402,12 @@ class RunResult(object):
self.stderr = LineMatcher(errlines) self.stderr = LineMatcher(errlines)
self.duration = duration self.duration = duration
def __repr__(self):
return (
"<RunResult ret=%r len(stdout.lines)=%d len(stderr.lines)=%d duration=%.2fs>"
% (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration)
)
def parseoutcomes(self): def parseoutcomes(self):
"""Return a dictionary of outcomestring->num from parsing the terminal """Return a dictionary of outcomestring->num from parsing the terminal
output that the test process produced. output that the test process produced.

View File

@ -11,6 +11,7 @@ import warnings
import six import six
import _pytest._code import _pytest._code
from _pytest.deprecated import PYTEST_WARNS_UNKNOWN_KWARGS
from _pytest.deprecated import WARNS_EXEC from _pytest.deprecated import WARNS_EXEC
from _pytest.fixtures import yield_fixture from _pytest.fixtures import yield_fixture
from _pytest.outcomes import fail from _pytest.outcomes import fail
@ -84,10 +85,12 @@ def warns(expected_warning, *args, **kwargs):
""" """
__tracebackhide__ = True __tracebackhide__ = True
match_expr = None
if not args: if not args:
if "match" in kwargs: match_expr = kwargs.pop("match", None)
match_expr = kwargs.pop("match") if kwargs:
warnings.warn(
PYTEST_WARNS_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=2
)
return WarningsChecker(expected_warning, match_expr=match_expr) return WarningsChecker(expected_warning, match_expr=match_expr)
elif isinstance(args[0], str): elif isinstance(args[0], str):
warnings.warn(WARNS_EXEC, stacklevel=2) warnings.warn(WARNS_EXEC, stacklevel=2)
@ -97,12 +100,12 @@ def warns(expected_warning, *args, **kwargs):
loc = frame.f_locals.copy() loc = frame.f_locals.copy()
loc.update(kwargs) loc.update(kwargs)
with WarningsChecker(expected_warning, match_expr=match_expr): with WarningsChecker(expected_warning):
code = _pytest._code.Source(code).compile() code = _pytest._code.Source(code).compile()
six.exec_(code, frame.f_globals, loc) six.exec_(code, frame.f_globals, loc)
else: else:
func = args[0] func = args[0]
with WarningsChecker(expected_warning, match_expr=match_expr): with WarningsChecker(expected_warning):
return func(*args[1:], **kwargs) return func(*args[1:], **kwargs)

View File

@ -674,7 +674,6 @@ class TerminalReporter(object):
self.summary_passes() self.summary_passes()
# Display any extra warnings from teardown here (if any). # Display any extra warnings from teardown here (if any).
self.summary_warnings() self.summary_warnings()
self.summary_deprecated_python()
def pytest_keyboard_interrupt(self, excinfo): def pytest_keyboard_interrupt(self, excinfo):
self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
@ -796,20 +795,6 @@ class TerminalReporter(object):
self.write_sep("_", msg) self.write_sep("_", msg)
self._outrep_summary(rep) self._outrep_summary(rep)
def summary_deprecated_python(self):
if sys.version_info[:2] <= (3, 4) and self.verbosity >= 0:
self.write_sep("=", "deprecated python version", yellow=True, bold=False)
using_version = ".".join(str(x) for x in sys.version_info[:3])
self.line(
"You are using Python {}, which will no longer be supported in pytest 5.0".format(
using_version
),
yellow=True,
bold=False,
)
self.line("For more information, please read:")
self.line(" https://docs.pytest.org/en/latest/py27-py34-deprecation.html")
def print_teardown_sections(self, rep): def print_teardown_sections(self, rep):
showcapture = self.config.option.showcapture showcapture = self.config.option.showcapture
if showcapture == "no": if showcapture == "no":

View File

@ -854,9 +854,7 @@ class TestDurations(object):
result = testdir.runpytest("--durations=2") result = testdir.runpytest("--durations=2")
assert result.ret == 0 assert result.ret == 0
lines = result.stdout.get_lines_after("*slowest*durations*") lines = result.stdout.get_lines_after("*slowest*durations*")
# account for the "deprecated python version" header assert "4 passed" in lines[2]
index = 2 if sys.version_info[:2] > (3, 4) else 6
assert "4 passed" in lines[index]
def test_calls_showall(self, testdir): def test_calls_showall(self, testdir):
testdir.makepyfile(self.source) testdir.makepyfile(self.source)

View File

@ -3,9 +3,9 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
import os import os
import sys
import pytest import pytest
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
pytestmark = pytest.mark.pytester_example_path("deprecated") pytestmark = pytest.mark.pytester_example_path("deprecated")
@ -222,19 +222,9 @@ def test_fixture_named_request(testdir):
) )
def test_python_deprecation(testdir): def test_pytest_warns_unknown_kwargs():
result = testdir.runpytest() with pytest.warns(
python_ver = ".".join(str(x) for x in sys.version_info[:3]) PytestDeprecationWarning,
msg = "You are using Python {}, which will no longer be supported in pytest 5.0".format( match=r"pytest.warns\(\) got unexpected keyword arguments: \['foo'\]",
python_ver ):
) pytest.warns(UserWarning, foo="hello")
if sys.version_info[:2] <= (3, 4):
result.stdout.fnmatch_lines(
[
msg,
"For more information, please read:",
" https://docs.pytest.org/en/latest/py27-py34-deprecation.html",
]
)
else:
assert msg not in result.stdout.str()

View File

@ -1002,3 +1002,51 @@ def test_log_in_hooks(testdir):
assert "sessionstart" in contents assert "sessionstart" in contents
assert "runtestloop" in contents assert "runtestloop" in contents
assert "sessionfinish" in contents assert "sessionfinish" in contents
def test_log_set_path(testdir):
report_dir_base = testdir.tmpdir.strpath
testdir.makeini(
"""
[pytest]
log_file_level = DEBUG
log_cli=true
"""
)
testdir.makeconftest(
"""
import os
import pytest
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_setup(item):
config = item.config
logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
report_file = os.path.join({}, item._request.node.name)
logging_plugin.set_log_path(report_file)
yield
""".format(
repr(report_dir_base)
)
)
testdir.makepyfile(
"""
import logging
logger = logging.getLogger("testcase-logger")
def test_first():
logger.info("message from test 1")
assert True
def test_second():
logger.debug("message from test 2")
assert True
"""
)
testdir.runpytest()
with open(os.path.join(report_dir_base, "test_first"), "r") as rfh:
content = rfh.read()
assert "message from test 1" in content
with open(os.path.join(report_dir_base, "test_second"), "r") as rfh:
content = rfh.read()
assert "message from test 2" in content

View File

@ -374,6 +374,26 @@ class TestCustomConftests(object):
assert result.ret == 0 assert result.ret == 0
assert "passed" in result.stdout.str() assert "passed" in result.stdout.str()
def test_collectignoreglob_exclude_on_option(self, testdir):
testdir.makeconftest(
"""
collect_ignore_glob = ['*w*l[dt]*']
def pytest_addoption(parser):
parser.addoption("--XX", action="store_true", default=False)
def pytest_configure(config):
if config.getvalue("XX"):
collect_ignore_glob[:] = []
"""
)
testdir.makepyfile(test_world="def test_hello(): pass")
testdir.makepyfile(test_welt="def test_hallo(): pass")
result = testdir.runpytest()
assert result.ret == EXIT_NOTESTSCOLLECTED
result.stdout.fnmatch_lines("*collected 0 items*")
result = testdir.runpytest("--XX")
assert result.ret == 0
result.stdout.fnmatch_lines("*2 passed*")
def test_pytest_fs_collect_hooks_are_seen(self, testdir): def test_pytest_fs_collect_hooks_are_seen(self, testdir):
testdir.makeconftest( testdir.makeconftest(
""" """

View File

@ -8,10 +8,12 @@ import textwrap
import _pytest._code import _pytest._code
import pytest import pytest
from _pytest.config import _iter_rewritable_modules from _pytest.config import _iter_rewritable_modules
from _pytest.config.exceptions import UsageError
from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import determine_setup
from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import get_common_ancestor
from _pytest.config.findpaths import getcfg from _pytest.config.findpaths import getcfg
from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_NOTESTSCOLLECTED
from _pytest.main import EXIT_USAGEERROR
class TestParseIni(object): class TestParseIni(object):
@ -1031,9 +1033,12 @@ class TestOverrideIniArgs(object):
monkeypatch.setenv("PYTEST_ADDOPTS", "-o") monkeypatch.setenv("PYTEST_ADDOPTS", "-o")
config = get_config() config = get_config()
with pytest.raises(SystemExit) as excinfo: with pytest.raises(UsageError) as excinfo:
config._preparse(["cache_dir=ignored"], addopts=True) config._preparse(["cache_dir=ignored"], addopts=True)
assert excinfo.value.args[0] == _pytest.main.EXIT_USAGEERROR assert (
"error: argument -o/--override-ini: expected one argument (via PYTEST_ADDOPTS)"
in excinfo.value.args[0]
)
def test_addopts_from_ini_not_concatenated(self, testdir): def test_addopts_from_ini_not_concatenated(self, testdir):
"""addopts from ini should not take values from normal args (#4265).""" """addopts from ini should not take values from normal args (#4265)."""
@ -1046,7 +1051,7 @@ class TestOverrideIniArgs(object):
result = testdir.runpytest("cache_dir=ignored") result = testdir.runpytest("cache_dir=ignored")
result.stderr.fnmatch_lines( result.stderr.fnmatch_lines(
[ [
"%s: error: argument -o/--override-ini: expected one argument" "%s: error: argument -o/--override-ini: expected one argument (via addopts config)"
% (testdir.request.config._parser.optparser.prog,) % (testdir.request.config._parser.optparser.prog,)
] ]
) )
@ -1083,3 +1088,68 @@ class TestOverrideIniArgs(object):
result = testdir.runpytest("-o", "foo=1", "-o", "bar=0", "test_foo.py") result = testdir.runpytest("-o", "foo=1", "-o", "bar=0", "test_foo.py")
assert "ERROR:" not in result.stderr.str() assert "ERROR:" not in result.stderr.str()
result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="]) result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="])
def test_help_via_addopts(testdir):
testdir.makeini(
"""
[pytest]
addopts = --unknown-option-should-allow-for-help --help
"""
)
result = testdir.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines(
[
"usage: *",
"positional arguments:",
# Displays full/default help.
"to see available markers type: pytest --markers",
]
)
def test_help_and_version_after_argument_error(testdir):
testdir.makeconftest(
"""
def validate(arg):
raise argparse.ArgumentTypeError("argerror")
def pytest_addoption(parser):
group = parser.getgroup('cov')
group.addoption(
"--invalid-option-should-allow-for-help",
type=validate,
)
"""
)
testdir.makeini(
"""
[pytest]
addopts = --invalid-option-should-allow-for-help
"""
)
result = testdir.runpytest("--help")
result.stdout.fnmatch_lines(
[
"usage: *",
"positional arguments:",
"NOTE: displaying only minimal help due to UsageError.",
]
)
result.stderr.fnmatch_lines(
[
"ERROR: usage: *",
"%s: error: argument --invalid-option-should-allow-for-help: expected one argument"
% (testdir.request.config._parser.optparser.prog,),
]
)
# Does not display full/default help.
assert "to see available markers type: pytest --markers" not in result.stdout.lines
assert result.ret == EXIT_USAGEERROR
result = testdir.runpytest("--version")
result.stderr.fnmatch_lines(
["*pytest*{}*imported from*".format(pytest.__version__)]
)
assert result.ret == EXIT_USAGEERROR

View File

@ -11,6 +11,7 @@ import py
import pytest import pytest
from _pytest.config import argparsing as parseopt from _pytest.config import argparsing as parseopt
from _pytest.config.exceptions import UsageError
@pytest.fixture @pytest.fixture
@ -19,11 +20,9 @@ def parser():
class TestParser(object): class TestParser(object):
def test_no_help_by_default(self, capsys): def test_no_help_by_default(self):
parser = parseopt.Parser(usage="xyz") parser = parseopt.Parser(usage="xyz")
pytest.raises(SystemExit, lambda: parser.parse(["-h"])) pytest.raises(UsageError, lambda: parser.parse(["-h"]))
out, err = capsys.readouterr()
assert err.find("error: unrecognized arguments") != -1
def test_custom_prog(self, parser): def test_custom_prog(self, parser):
"""Custom prog can be set for `argparse.ArgumentParser`.""" """Custom prog can be set for `argparse.ArgumentParser`."""

View File

@ -127,6 +127,17 @@ def test_runresult_assertion_on_xpassed(testdir):
assert result.ret == 0 assert result.ret == 0
def test_runresult_repr():
from _pytest.pytester import RunResult
assert (
repr(
RunResult(ret="ret", outlines=[""], errlines=["some", "errors"], duration=1)
)
== "<RunResult ret='ret' len(stdout.lines)=1 len(stderr.lines)=2 duration=1.00s>"
)
def test_xpassed_with_strict_is_considered_a_failure(testdir): def test_xpassed_with_strict_is_considered_a_failure(testdir):
testdir.makepyfile( testdir.makepyfile(
""" """

View File

@ -253,6 +253,21 @@ def test_exclude(testdir):
result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.fnmatch_lines(["*1 passed*"])
def test_exclude_glob(testdir):
hellodir = testdir.mkdir("hello")
hellodir.join("test_hello.py").write("x y syntaxerror")
hello2dir = testdir.mkdir("hello2")
hello2dir.join("test_hello2.py").write("x y syntaxerror")
hello3dir = testdir.mkdir("hallo3")
hello3dir.join("test_hello3.py").write("x y syntaxerror")
subdir = testdir.mkdir("sub")
subdir.join("test_hello4.py").write("x y syntaxerror")
testdir.makepyfile(test_ok="def test_pass(): pass")
result = testdir.runpytest("--ignore-glob=*h[ea]llo*")
assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed*"])
def test_deselect(testdir): def test_deselect(testdir):
testdir.makepyfile( testdir.makepyfile(
test_a=""" test_a="""