Merge pull request #4692 from nicoddemus/merge-master-into-features

Merge master into features
This commit is contained in:
Bruno Oliveira 2019-01-30 14:16:09 -02:00 committed by GitHub
commit e2a15c79e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 483 additions and 156 deletions

View File

@ -1,7 +1,7 @@
exclude: doc/en/example/py2py3/test_py2.py exclude: doc/en/example/py2py3/test_py2.py
repos: repos:
- repo: https://github.com/ambv/black - repo: https://github.com/ambv/black
rev: 18.6b4 rev: 18.9b0
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
@ -13,7 +13,7 @@ repos:
additional_dependencies: [black==18.9b0] additional_dependencies: [black==18.9b0]
language_version: python3 language_version: python3
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.0.0 rev: v2.1.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@ -21,20 +21,23 @@ repos:
- id: debug-statements - id: debug-statements
exclude: _pytest/debugging.py exclude: _pytest/debugging.py
language_version: python3 language_version: python3
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.0
hooks:
- id: flake8 - id: flake8
language_version: python3 language_version: python3
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder_python_imports
rev: v1.3.3 rev: v1.3.5
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ['--application-directories=.:src'] args: ['--application-directories=.:src']
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v1.10.1 rev: v1.11.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--keep-percent-format] args: [--keep-percent-format]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.1.0 rev: v1.2.0
hooks: hooks:
- id: rst-backticks - id: rst-backticks
- repo: local - repo: local
@ -51,3 +54,17 @@ repos:
entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst' entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst'
exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst) exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst)
files: ^changelog/ files: ^changelog/
- id: py-deprecated
name: py library is deprecated
language: pygrep
entry: >
(?x)\bpy\.(
_code\.|
builtin\.|
code\.|
io\.(BytesIO|saferepr)|
path\.local\.sysfind|
process\.|
std\.
)
types: [python]

View File

@ -7,6 +7,7 @@ Aaron Coleman
Abdeali JK Abdeali JK
Abhijeet Kasurde Abhijeet Kasurde
Adam Johnson Adam Johnson
Adam Uhlir
Ahn Ki-Wook Ahn Ki-Wook
Alan Velasco Alan Velasco
Alexander Johnson Alexander Johnson
@ -52,6 +53,7 @@ Christian Boelsen
Christian Theunert Christian Theunert
Christian Tismer Christian Tismer
Christopher Gilling Christopher Gilling
Christopher Dignam
CrazyMerlyn CrazyMerlyn
Cyrus Maden Cyrus Maden
Dhiren Serai Dhiren Serai

View File

@ -0,0 +1,4 @@
Warning summary now groups warnings by message instead of by test id.
This makes the output more compact and better conveys the general idea of how much code is
actually generating warnings, instead of how many tests call that code.

View File

@ -0,0 +1 @@
``monkeypatch.delattr`` handles class descriptors like ``staticmethod``/``classmethod``.

View File

@ -0,0 +1 @@
Restore marks being considered keywords for keyword expressions.

View File

@ -0,0 +1 @@
``tmp_path`` fixture and other related ones provides resolved path (a.k.a real path)

View File

@ -0,0 +1 @@
Copy saferepr from pylib

View File

@ -0,0 +1 @@
``pytest_terminal_summary`` uses result from ``pytest_report_teststatus`` hook, rather than hardcoded strings.

View File

@ -0,0 +1 @@
Correctly handle ``unittest.SkipTest`` exception containing non-ascii characters on Python 2.

View File

@ -41,6 +41,7 @@ Full pytest documentation
backwards-compatibility backwards-compatibility
deprecations deprecations
py27-py34-deprecation
historical-notes historical-notes
license license
contributing contributing

View File

@ -25,11 +25,32 @@ Below is a complete list of all pytest features which are considered deprecated.
.. deprecated:: 4.1 .. deprecated:: 4.1
It is a common mistake to think this parameter will match the exception message, while in fact It is a common mistake to think this parameter will match the exception message, while in fact
it only serves to provide a custom message in case the ``pytest.raises`` check fails. To avoid this it only serves to provide a custom message in case the ``pytest.raises`` check fails. To prevent
mistake and because it is believed to be little used, pytest is deprecating it without providing users from making this mistake, and because it is believed to be little used, pytest is
an alternative for the moment. deprecating it without providing an alternative for the moment.
If you have concerns about this, please comment on `issue #3974 <https://github.com/pytest-dev/pytest/issues/3974>`__. If you have a valid use case for this parameter, consider that to obtain the same results
you can just call ``pytest.fail`` manually at the end of the ``with`` statement.
For example:
.. code-block:: python
with pytest.raises(TimeoutError, message="Client got unexpected message"):
wait_for(websocket.recv(), 0.5)
Becomes:
.. code-block:: python
with pytest.raises(TimeoutError):
wait_for(websocket.recv(), 0.5)
pytest.fail("Client got unexpected message")
If you still have concerns about this deprecation and future removal, please comment on
`issue #3974 <https://github.com/pytest-dev/pytest/issues/3974>`__.
``pytest.config`` global ``pytest.config`` global

View File

@ -24,10 +24,10 @@ example: specifying and selecting acceptance tests
pytest.skip("specify -A to run acceptance tests") pytest.skip("specify -A to run acceptance tests")
self.tmpdir = request.config.mktemp(request.function.__name__, numbered=True) self.tmpdir = request.config.mktemp(request.function.__name__, numbered=True)
def run(self, cmd): def run(self, *cmd):
""" called by test code to execute an acceptance test. """ """ called by test code to execute an acceptance test. """
self.tmpdir.chdir() self.tmpdir.chdir()
return py.process.cmdexec(cmd) return subprocess.check_output(cmd).decode()
and the actual test function example: and the actual test function example:
@ -36,7 +36,7 @@ and the actual test function example:
def test_some_acceptance_aspect(accept): def test_some_acceptance_aspect(accept):
accept.tmpdir.mkdir("somesub") accept.tmpdir.mkdir("somesub")
result = accept.run("ls -la") result = accept.run("ls", "-la")
assert "somesub" in result assert "somesub" in result
If you run this test without specifying a command line option If you run this test without specifying a command line option

View File

@ -2,10 +2,10 @@
module containing a parametrized tests testing cross-python module containing a parametrized tests testing cross-python
serialization via the pickle module. serialization via the pickle module.
""" """
import distutils.spawn
import subprocess
import textwrap import textwrap
import py
import pytest import pytest
pythonlist = ["python2.7", "python3.4", "python3.5"] pythonlist = ["python2.7", "python3.4", "python3.5"]
@ -24,7 +24,7 @@ def python2(request, python1):
class Python(object): class Python(object):
def __init__(self, version, picklefile): def __init__(self, version, picklefile):
self.pythonpath = py.path.local.sysfind(version) self.pythonpath = distutils.spawn.find_executable(version)
if not self.pythonpath: if not self.pythonpath:
pytest.skip("{!r} not found".format(version)) pytest.skip("{!r} not found".format(version))
self.picklefile = picklefile self.picklefile = picklefile
@ -43,7 +43,7 @@ class Python(object):
) )
) )
) )
py.process.cmdexec("{} {}".format(self.pythonpath, dumpfile)) subprocess.check_call((self.pythonpath, str(dumpfile)))
def load_and_is_true(self, expression): def load_and_is_true(self, expression):
loadfile = self.picklefile.dirpath("load.py") loadfile = self.picklefile.dirpath("load.py")
@ -63,7 +63,7 @@ class Python(object):
) )
) )
print(loadfile) print(loadfile)
py.process.cmdexec("{} {}".format(self.pythonpath, loadfile)) subprocess.check_call((self.pythonpath, str(loadfile)))
@pytest.mark.parametrize("obj", [42, {}, {1: 3}]) @pytest.mark.parametrize("obj", [42, {}, {1: 3}])

View File

@ -0,0 +1,22 @@
Python 2.7 and 3.4 support plan
===============================
Python 2.7 EOL is fast approaching, with
upstream support `ending in 2020 <https://legacy.python.org/dev/peps/pep-0373/#id4>`__.
Python 3.4's last release is scheduled for
`March 2019 <https://www.python.org/dev/peps/pep-0429/#release-schedule>`__. pytest is one of
the participating projects of the https://python3statement.org.
We plan to drop support for Python 2.7 and 3.4 at the same time with the release of **pytest 5.0**,
scheduled to be released by **mid-2019**. Thanks to the `python_requires <https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires>`__ ``setuptools`` option,
Python 2.7 and Python 3.4 users using a modern ``pip`` version
will install the last compatible pytest ``4.X`` version automatically even if ``5.0`` or later
are available on PyPI.
During the period **from mid-2019 and 2020**, the pytest core team plans to make
bug-fix releases of the pytest ``4.X`` series by back-porting patches to the ``4.x-maintenance``
branch.
**After 2020**, the core team will no longer actively back port-patches, but the ``4.x-maintenance``
branch will continue to exist so the community itself can contribute patches. The
core team will be happy to accept those patches and make new ``4.X`` releases **until mid-2020**.

View File

@ -36,10 +36,11 @@ platforms = unix, linux, osx, cygwin, win32
zip_safe = no zip_safe = no
packages = packages =
_pytest _pytest
_pytest.assertion
_pytest._code _pytest._code
_pytest.mark _pytest._io
_pytest.assertion
_pytest.config _pytest.config
_pytest.mark
py_modules = pytest py_modules = pytest
python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*

View File

@ -18,6 +18,7 @@ import six
from six import text_type from six import text_type
import _pytest import _pytest
from _pytest._io.saferepr import saferepr
from _pytest.compat import _PY2 from _pytest.compat import _PY2
from _pytest.compat import _PY3 from _pytest.compat import _PY3
from _pytest.compat import PY35 from _pytest.compat import PY35
@ -142,7 +143,7 @@ class Frame(object):
def repr(self, object): def repr(self, object):
""" return a 'safe' (non-recursive, one-line) string repr for 'object' """ return a 'safe' (non-recursive, one-line) string repr for 'object'
""" """
return py.io.saferepr(object) return saferepr(object)
def is_true(self, object): def is_true(self, object):
return object return object
@ -421,7 +422,7 @@ class ExceptionInfo(object):
if exprinfo is None and isinstance(tup[1], AssertionError): if exprinfo is None and isinstance(tup[1], AssertionError):
exprinfo = getattr(tup[1], "msg", None) exprinfo = getattr(tup[1], "msg", None)
if exprinfo is None: if exprinfo is None:
exprinfo = py.io.saferepr(tup[1]) exprinfo = saferepr(tup[1])
if exprinfo and exprinfo.startswith(cls._assert_start_repr): if exprinfo and exprinfo.startswith(cls._assert_start_repr):
_striptext = "AssertionError: " _striptext = "AssertionError: "
@ -618,7 +619,7 @@ class FormattedExcinfo(object):
return source return source
def _saferepr(self, obj): def _saferepr(self, obj):
return py.io.saferepr(obj) return saferepr(obj)
def repr_args(self, entry): def repr_args(self, entry):
if self.funcargs: if self.funcargs:

View File

@ -237,9 +237,7 @@ def getfslineno(obj):
def findsource(obj): def findsource(obj):
try: try:
sourcelines, lineno = inspect.findsource(obj) sourcelines, lineno = inspect.findsource(obj)
except py.builtin._sysex: except Exception:
raise
except: # noqa
return None, -1 return None, -1
source = Source() source = Source()
source.lines = [line.rstrip() for line in sourcelines] source.lines = [line.rstrip() for line in sourcelines]

View File

View File

@ -0,0 +1,72 @@
import sys
from six.moves import reprlib
class SafeRepr(reprlib.Repr):
"""subclass of repr.Repr that limits the resulting size of repr()
and includes information on exceptions raised during the call.
"""
def repr(self, x):
return self._callhelper(reprlib.Repr.repr, self, x)
def repr_unicode(self, x, level):
# Strictly speaking wrong on narrow builds
def repr(u):
if "'" not in u:
return u"'%s'" % u
elif '"' not in u:
return u'"%s"' % u
else:
return u"'%s'" % u.replace("'", r"\'")
s = repr(x[: self.maxstring])
if len(s) > self.maxstring:
i = max(0, (self.maxstring - 3) // 2)
j = max(0, self.maxstring - 3 - i)
s = repr(x[:i] + x[len(x) - j :])
s = s[:i] + "..." + s[len(s) - j :]
return s
def repr_instance(self, x, level):
return self._callhelper(repr, x)
def _callhelper(self, call, x, *args):
try:
# Try the vanilla repr and make sure that the result is a string
s = call(x, *args)
except Exception:
cls, e, tb = sys.exc_info()
exc_name = getattr(cls, "__name__", "unknown")
try:
exc_info = str(e)
except Exception:
exc_info = "unknown"
return '<[%s("%s") raised in repr()] %s object at 0x%x>' % (
exc_name,
exc_info,
x.__class__.__name__,
id(x),
)
else:
if len(s) > self.maxsize:
i = max(0, (self.maxsize - 3) // 2)
j = max(0, self.maxsize - 3 - i)
s = s[:i] + "..." + s[len(s) - j :]
return s
def saferepr(obj, maxsize=240):
"""return a size-limited safe repr-string for the given object.
Failing __repr__ functions of user instances will be represented
with a short exception info and 'saferepr' generally takes
care to never raise exceptions itself. This function is a wrapper
around the Repr/reprlib functionality of the standard 2.6 lib.
"""
# review exception handling
srepr = SafeRepr()
srepr.maxstring = maxsize
srepr.maxsize = maxsize
srepr.maxother = 160
return srepr.repr(obj)

View File

@ -19,6 +19,7 @@ import atomicwrites
import py import py
import six import six
from _pytest._io.saferepr import saferepr
from _pytest.assertion import util from _pytest.assertion import util
from _pytest.compat import spec_from_file_location from _pytest.compat import spec_from_file_location
from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import fnmatch_ex
@ -471,7 +472,7 @@ def _saferepr(obj):
JSON reprs. JSON reprs.
""" """
r = py.io.saferepr(obj) r = saferepr(obj)
# only occurs in python2.x, repr must return text in python3+ # only occurs in python2.x, repr must return text in python3+
if isinstance(r, bytes): if isinstance(r, bytes):
# Represent unprintable bytes as `\x##` # Represent unprintable bytes as `\x##`
@ -490,7 +491,7 @@ def _format_assertmsg(obj):
For strings this simply replaces newlines with '\n~' so that For strings this simply replaces newlines with '\n~' so that
util.format_explanation() will preserve them instead of escaping util.format_explanation() will preserve them instead of escaping
newlines. For other objects py.io.saferepr() is used first. newlines. For other objects saferepr() is used first.
""" """
# reprlib appears to have a bug which means that if a string # reprlib appears to have a bug which means that if a string
@ -499,7 +500,7 @@ def _format_assertmsg(obj):
# However in either case we want to preserve the newline. # However in either case we want to preserve the newline.
replaces = [(u"\n", u"\n~"), (u"%", u"%%")] replaces = [(u"\n", u"\n~"), (u"%", u"%%")]
if not isinstance(obj, six.string_types): if not isinstance(obj, six.string_types):
obj = py.io.saferepr(obj) obj = saferepr(obj)
replaces.append((u"\\n", u"\n~")) replaces.append((u"\\n", u"\n~"))
if isinstance(obj, bytes): if isinstance(obj, bytes):
@ -665,7 +666,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Insert some special imports at the top of the module but after any # Insert some special imports at the top of the module but after any
# docstrings and __future__ imports. # docstrings and __future__ imports.
aliases = [ aliases = [
ast.alias(py.builtin.builtins.__name__, "@py_builtins"), ast.alias(six.moves.builtins.__name__, "@py_builtins"),
ast.alias("_pytest.assertion.rewrite", "@pytest_ar"), ast.alias("_pytest.assertion.rewrite", "@pytest_ar"),
] ]
doc = getattr(mod, "docstring", None) doc = getattr(mod, "docstring", None)
@ -740,7 +741,7 @@ class AssertionRewriter(ast.NodeVisitor):
return ast.Name(name, ast.Load()) return ast.Name(name, ast.Load())
def display(self, expr): def display(self, expr):
"""Call py.io.saferepr on the expression.""" """Call saferepr on the expression."""
return self.helper("saferepr", expr) return self.helper("saferepr", expr)
def helper(self, name, *args): def helper(self, name, *args):

View File

@ -5,11 +5,11 @@ from __future__ import print_function
import pprint import pprint
import py
import six import six
import _pytest._code import _pytest._code
from ..compat import Sequence from ..compat import Sequence
from _pytest._io.saferepr import saferepr
# The _reprcompare attribute on the util module is used by the new assertion # The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was # interpretation code and assertion rewriter to detect this plugin was
@ -105,8 +105,8 @@ except NameError:
def assertrepr_compare(config, op, left, right): def assertrepr_compare(config, op, left, right):
"""Return specialised explanations for some operators/operands""" """Return specialised explanations for some operators/operands"""
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
left_repr = py.io.saferepr(left, maxsize=int(width // 2)) left_repr = saferepr(left, maxsize=int(width // 2))
right_repr = py.io.saferepr(right, maxsize=width - len(left_repr)) right_repr = saferepr(right, maxsize=width - len(left_repr))
summary = u"%s %s %s" % (ecu(left_repr), op, ecu(right_repr)) summary = u"%s %s %s" % (ecu(left_repr), op, ecu(right_repr))
@ -282,12 +282,12 @@ def _compare_eq_sequence(left, right, verbose=False):
if len(left) > len(right): if len(left) > len(right):
explanation += [ explanation += [
u"Left contains more items, first extra item: %s" u"Left contains more items, first extra item: %s"
% py.io.saferepr(left[len(right)]) % saferepr(left[len(right)])
] ]
elif len(left) < len(right): elif len(left) < len(right):
explanation += [ explanation += [
u"Right contains more items, first extra item: %s" u"Right contains more items, first extra item: %s"
% py.io.saferepr(right[len(left)]) % saferepr(right[len(left)])
] ]
return explanation return explanation
@ -299,11 +299,11 @@ def _compare_eq_set(left, right, verbose=False):
if diff_left: if diff_left:
explanation.append(u"Extra items in the left set:") explanation.append(u"Extra items in the left set:")
for item in diff_left: for item in diff_left:
explanation.append(py.io.saferepr(item)) explanation.append(saferepr(item))
if diff_right: if diff_right:
explanation.append(u"Extra items in the right set:") explanation.append(u"Extra items in the right set:")
for item in diff_right: for item in diff_right:
explanation.append(py.io.saferepr(item)) explanation.append(saferepr(item))
return explanation return explanation
@ -320,9 +320,7 @@ def _compare_eq_dict(left, right, verbose=False):
if diff: if diff:
explanation += [u"Differing items:"] explanation += [u"Differing items:"]
for k in diff: for k in diff:
explanation += [ explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})]
py.io.saferepr({k: left[k]}) + " != " + py.io.saferepr({k: right[k]})
]
extra_left = set(left) - set(right) extra_left = set(left) - set(right)
if extra_left: if extra_left:
explanation.append(u"Left contains more items:") explanation.append(u"Left contains more items:")
@ -376,7 +374,7 @@ def _notin_text(term, text, verbose=False):
tail = text[index + len(term) :] tail = text[index + len(term) :]
correct_text = head + tail correct_text = head + tail
diff = _diff_text(correct_text, text, verbose) diff = _diff_text(correct_text, text, verbose)
newdiff = [u"%s is contained here:" % py.io.saferepr(term, maxsize=42)] newdiff = [u"%s is contained here:" % saferepr(term, maxsize=42)]
for line in diff: for line in diff:
if line.startswith(u"Skipping"): if line.startswith(u"Skipping"):
continue continue

View File

@ -17,6 +17,7 @@ import six
from six import text_type from six import text_type
import _pytest import _pytest
from _pytest._io.saferepr import saferepr
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
@ -294,7 +295,7 @@ def get_real_func(obj):
else: else:
raise ValueError( raise ValueError(
("could not find real function of {start}\nstopped at {current}").format( ("could not find real function of {start}\nstopped at {current}").format(
start=py.io.saferepr(start_obj), current=py.io.saferepr(obj) start=saferepr(start_obj), current=saferepr(obj)
) )
) )
if isinstance(obj, functools.partial): if isinstance(obj, functools.partial):

View File

@ -437,7 +437,7 @@ class PytestPluginManager(PluginManager):
and not self._using_pyargs and not self._using_pyargs
): ):
from _pytest.deprecated import ( from _pytest.deprecated import (
PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST,
) )
fail( fail(

View File

@ -14,10 +14,10 @@ import attr
import py import py
import six import six
from more_itertools import flatten from more_itertools import flatten
from py._code.code import FormattedExcinfo
import _pytest import _pytest
from _pytest import nodes from _pytest import nodes
from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest.compat import _format_args from _pytest.compat import _format_args
from _pytest.compat import _PytestWrapper from _pytest.compat import _PytestWrapper
@ -307,8 +307,8 @@ class FuncFixtureInfo(object):
# fixture names specified via usefixtures and via autouse=True in fixture # fixture names specified via usefixtures and via autouse=True in fixture
# definitions. # definitions.
initialnames = attr.ib(type=tuple) initialnames = attr.ib(type=tuple)
names_closure = attr.ib() # type: List[str] names_closure = attr.ib() # List[str]
name2fixturedefs = attr.ib() # type: List[str, List[FixtureDef]] name2fixturedefs = attr.ib() # List[str, List[FixtureDef]]
def prune_dependency_tree(self): def prune_dependency_tree(self):
"""Recompute names_closure from initialnames and name2fixturedefs """Recompute names_closure from initialnames and name2fixturedefs

View File

@ -244,7 +244,7 @@ class _NodeReporter(object):
self._add_simple(Junit.skipped, "collection skipped", report.longrepr) self._add_simple(Junit.skipped, "collection skipped", report.longrepr)
def append_error(self, report): def append_error(self, report):
if getattr(report, "when", None) == "teardown": if report.when == "teardown":
msg = "test teardown failure" msg = "test teardown failure"
else: else:
msg = "test setup failure" msg = "test setup failure"

View File

@ -217,7 +217,7 @@ class LogCaptureFixture(object):
"""Creates a new funcarg.""" """Creates a new funcarg."""
self._item = item self._item = item
# dict of log name -> log level # dict of log name -> log level
self._initial_log_levels = {} # type: Dict[str, int] self._initial_log_levels = {} # Dict[str, int]
def _finalize(self): def _finalize(self):
"""Finalizes the fixture. """Finalizes the fixture.

View File

@ -45,13 +45,14 @@ class KeywordMapping(object):
mapped_names.add(item.name) mapped_names.add(item.name)
# Add the names added as extra keywords to current or parent items # Add the names added as extra keywords to current or parent items
for name in item.listextrakeywords(): mapped_names.update(item.listextrakeywords())
mapped_names.add(name)
# Add the names attached to the current function through direct assignment # Add the names attached to the current function through direct assignment
if hasattr(item, "function"): if hasattr(item, "function"):
for name in item.function.__dict__: mapped_names.update(item.function.__dict__)
mapped_names.add(name)
# add the markers to the keywords as we no longer handle them correctly
mapped_names.update(mark.name for mark in item.iter_markers())
return cls(mapped_names) return cls(mapped_names)

View File

@ -143,9 +143,9 @@ class Mark(object):
#: name of the mark #: name of the mark
name = attr.ib(type=str) name = attr.ib(type=str)
#: positional arguments of the mark decorator #: positional arguments of the mark decorator
args = attr.ib() # type: List[object] args = attr.ib() # List[object]
#: keyword arguments of the mark decorator #: keyword arguments of the mark decorator
kwargs = attr.ib() # type: Dict[str, object] kwargs = attr.ib() # Dict[str, object]
def combined_with(self, other): def combined_with(self, other):
""" """

View File

@ -181,6 +181,8 @@ class MonkeyPatch(object):
attribute is missing. attribute is missing.
""" """
__tracebackhide__ = True __tracebackhide__ = True
import inspect
if name is notset: if name is notset:
if not isinstance(target, six.string_types): if not isinstance(target, six.string_types):
raise TypeError( raise TypeError(
@ -194,7 +196,11 @@ class MonkeyPatch(object):
if raising: if raising:
raise AttributeError(name) raise AttributeError(name)
else: else:
self._setattr.append((target, name, getattr(target, name, notset))) oldval = getattr(target, name, notset)
# Avoid class descriptors like staticmethod/classmethod.
if inspect.isclass(target):
oldval = target.__dict__.get(name, notset)
self._setattr.append((target, name, oldval))
delattr(target, name) delattr(target, name)
def setitem(self, dic, name, value): def setitem(self, dic, name, value):

View File

@ -5,6 +5,8 @@ from __future__ import print_function
import sys import sys
import six
from _pytest import python from _pytest import python
from _pytest import runner from _pytest import runner
from _pytest import unittest from _pytest import unittest
@ -24,7 +26,7 @@ def pytest_runtest_makereport(item, call):
if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()): if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()):
# let's substitute the excinfo with a pytest.skip one # let's substitute the excinfo with a pytest.skip one
call2 = runner.CallInfo.from_call( call2 = runner.CallInfo.from_call(
lambda: runner.skip(str(call.excinfo.value)), call.when lambda: runner.skip(six.text_type(call.excinfo.value)), call.when
) )
call.excinfo = call2.excinfo call.excinfo = call2.excinfo

View File

@ -4,6 +4,7 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
import codecs import codecs
import distutils.spawn
import gc import gc
import os import os
import platform import platform
@ -20,6 +21,7 @@ import six
import pytest import pytest
from _pytest._code import Source from _pytest._code import Source
from _pytest._io.saferepr import saferepr
from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.assertion.rewrite import AssertionRewritingHook
from _pytest.capture import MultiCapture from _pytest.capture import MultiCapture
from _pytest.capture import SysCapture from _pytest.capture import SysCapture
@ -79,7 +81,7 @@ class LsofFdLeakChecker(object):
def _exec_lsof(self): def _exec_lsof(self):
pid = os.getpid() pid = os.getpid()
return py.process.cmdexec("lsof -Ffn0 -p %d" % pid) return subprocess.check_output(("lsof", "-Ffn0", "-p", str(pid))).decode()
def _parse_lsof_output(self, out): def _parse_lsof_output(self, out):
def isopen(line): def isopen(line):
@ -106,11 +108,8 @@ class LsofFdLeakChecker(object):
def matching_platform(self): def matching_platform(self):
try: try:
py.process.cmdexec("lsof -v") subprocess.check_output(("lsof", "-v"))
except (py.process.cmdexec.Error, UnicodeDecodeError): except (OSError, subprocess.CalledProcessError):
# cmdexec may raise UnicodeDecodeError on Windows systems with
# locale other than English:
# https://bitbucket.org/pytest-dev/py/issues/66
return False return False
else: else:
return True return True
@ -152,7 +151,7 @@ def getexecutable(name, cache={}):
try: try:
return cache[name] return cache[name]
except KeyError: except KeyError:
executable = py.path.local.sysfind(name) executable = distutils.spawn.find_executable(name)
if executable: if executable:
import subprocess import subprocess
@ -306,13 +305,10 @@ class HookRecorder(object):
"""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: if not when and rep.when != "call" and rep.passed:
if not when and rep.when != "call" and rep.passed: # setup/teardown passing reports - let's ignore those
# setup/teardown passing reports - let's ignore those continue
continue if when and rep.when != when:
except AttributeError:
pass
if when and getattr(rep, "when", None) != when:
continue continue
if not inamepart or inamepart in rep.nodeid.split("::"): if not inamepart or inamepart in rep.nodeid.split("::"):
values.append(rep) values.append(rep)
@ -339,7 +335,7 @@ class HookRecorder(object):
failed = [] failed = []
for rep in self.getreports("pytest_collectreport pytest_runtest_logreport"): for rep in self.getreports("pytest_collectreport pytest_runtest_logreport"):
if rep.passed: if rep.passed:
if getattr(rep, "when", None) == "call": if rep.when == "call":
passed.append(rep) passed.append(rep)
elif rep.skipped: elif rep.skipped:
skipped.append(rep) skipped.append(rep)
@ -1225,9 +1221,7 @@ def getdecoded(out):
try: try:
return out.decode("utf-8") return out.decode("utf-8")
except UnicodeDecodeError: except UnicodeDecodeError:
return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % ( return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (saferepr(out),)
py.io.saferepr(out),
)
class LineComp(object): class LineComp(object):

View File

@ -1029,7 +1029,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
:rtype: List[str] :rtype: List[str]
:return: the list of ids for each argname given :return: the list of ids for each argname given
""" """
from py.io import saferepr from _pytest._io.saferepr import saferepr
idfn = None idfn = None
if callable(ids): if callable(ids):

View File

@ -19,6 +19,8 @@ def getslaveinfoline(node):
class BaseReport(object): class BaseReport(object):
when = None
def __init__(self, **kw): def __init__(self, **kw):
self.__dict__.update(kw) self.__dict__.update(kw)
@ -159,6 +161,8 @@ class TestReport(BaseReport):
class CollectReport(BaseReport): class CollectReport(BaseReport):
when = "collect"
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
self.nodeid = nodeid self.nodeid = nodeid
self.outcome = outcome self.outcome = outcome

View File

@ -204,7 +204,7 @@ class CallInfo(object):
""" Result/Exception info a function invocation. """ """ Result/Exception info a function invocation. """
_result = attr.ib() _result = attr.ib()
# type: Optional[ExceptionInfo] # Optional[ExceptionInfo]
excinfo = attr.ib() excinfo = attr.ib()
start = attr.ib() start = attr.ib()
stop = attr.ib() stop = attr.ib()

View File

@ -180,9 +180,9 @@ def pytest_runtest_makereport(item, call):
def pytest_report_teststatus(report): def pytest_report_teststatus(report):
if hasattr(report, "wasxfail"): if hasattr(report, "wasxfail"):
if report.skipped: if report.skipped:
return "xfailed", "x", "xfail" return "xfailed", "x", "XFAIL"
elif report.passed: elif report.passed:
return "xpassed", "X", ("XPASS", {"yellow": True}) return "xpassed", "X", "XPASS"
# called by the terminalreporter instance/plugin # called by the terminalreporter instance/plugin
@ -191,11 +191,6 @@ def pytest_report_teststatus(report):
def pytest_terminal_summary(terminalreporter): def pytest_terminal_summary(terminalreporter):
tr = terminalreporter tr = terminalreporter
if not tr.reportchars: if not tr.reportchars:
# for name in "xfailed skipped failed xpassed":
# if not tr.stats.get(name, 0):
# tr.write_line("HINT: use '-r' option to see extra "
# "summary info about tests")
# break
return return
lines = [] lines = []
@ -209,21 +204,23 @@ def pytest_terminal_summary(terminalreporter):
tr._tw.line(line) tr._tw.line(line)
def show_simple(terminalreporter, lines, stat, format): def show_simple(terminalreporter, lines, stat):
failed = terminalreporter.stats.get(stat) failed = terminalreporter.stats.get(stat)
if failed: if failed:
for rep in failed: for rep in failed:
verbose_word = _get_report_str(terminalreporter, rep)
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid) pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
lines.append(format % (pos,)) lines.append("%s %s" % (verbose_word, pos))
def show_xfailed(terminalreporter, lines): def show_xfailed(terminalreporter, lines):
xfailed = terminalreporter.stats.get("xfailed") xfailed = terminalreporter.stats.get("xfailed")
if xfailed: if xfailed:
for rep in xfailed: for rep in xfailed:
verbose_word = _get_report_str(terminalreporter, rep)
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid) pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail reason = rep.wasxfail
lines.append("XFAIL %s" % (pos,)) lines.append("%s %s" % (verbose_word, pos))
if reason: if reason:
lines.append(" " + str(reason)) lines.append(" " + str(reason))
@ -232,9 +229,10 @@ def show_xpassed(terminalreporter, lines):
xpassed = terminalreporter.stats.get("xpassed") xpassed = terminalreporter.stats.get("xpassed")
if xpassed: if xpassed:
for rep in xpassed: for rep in xpassed:
verbose_word = _get_report_str(terminalreporter, rep)
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid) pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail reason = rep.wasxfail
lines.append("XPASS %s %s" % (pos, reason)) lines.append("%s %s %s" % (verbose_word, pos, reason))
def folded_skips(skipped): def folded_skips(skipped):
@ -246,8 +244,11 @@ def folded_skips(skipped):
# folding reports with global pytestmark variable # folding reports with global pytestmark variable
# this is workaround, because for now we cannot identify the scope of a skip marker # this is workaround, because for now we cannot identify the scope of a skip marker
# TODO: revisit after marks scope would be fixed # TODO: revisit after marks scope would be fixed
when = getattr(event, "when", None) if (
if when == "setup" and "skip" in keywords and "pytestmark" not in keywords: event.when == "setup"
and "skip" in keywords
and "pytestmark" not in keywords
):
key = (key[0], None, key[2]) key = (key[0], None, key[2])
d.setdefault(key, []).append(event) d.setdefault(key, []).append(event)
values = [] values = []
@ -260,39 +261,42 @@ def show_skipped(terminalreporter, lines):
tr = terminalreporter tr = terminalreporter
skipped = tr.stats.get("skipped", []) skipped = tr.stats.get("skipped", [])
if skipped: if skipped:
# if not tr.hasopt('skipped'): verbose_word = _get_report_str(terminalreporter, report=skipped[0])
# tr.write_line(
# "%d skipped tests, specify -rs for more info" %
# len(skipped))
# return
fskips = folded_skips(skipped) fskips = folded_skips(skipped)
if fskips: if fskips:
# tr.write_sep("_", "skipped test summary")
for num, fspath, lineno, reason in fskips: for num, fspath, lineno, reason in fskips:
if reason.startswith("Skipped: "): if reason.startswith("Skipped: "):
reason = reason[9:] reason = reason[9:]
if lineno is not None: if lineno is not None:
lines.append( lines.append(
"SKIP [%d] %s:%d: %s" % (num, fspath, lineno + 1, reason) "%s [%d] %s:%d: %s"
% (verbose_word, num, fspath, lineno + 1, reason)
) )
else: else:
lines.append("SKIP [%d] %s: %s" % (num, fspath, reason)) lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason))
def shower(stat, format): def shower(stat):
def show_(terminalreporter, lines): def show_(terminalreporter, lines):
return show_simple(terminalreporter, lines, stat, format) return show_simple(terminalreporter, lines, stat)
return show_ return show_
def _get_report_str(terminalreporter, report):
_category, _short, verbose = terminalreporter.config.hook.pytest_report_teststatus(
report=report, config=terminalreporter.config
)
return verbose
REPORTCHAR_ACTIONS = { REPORTCHAR_ACTIONS = {
"x": show_xfailed, "x": show_xfailed,
"X": show_xpassed, "X": show_xpassed,
"f": shower("failed", "FAIL %s"), "f": shower("failed"),
"F": shower("failed", "FAIL %s"), "F": shower("failed"),
"s": show_skipped, "s": show_skipped,
"S": show_skipped, "S": show_skipped,
"p": shower("passed", "PASSED %s"), "p": shower("passed"),
"E": shower("error", "ERROR %s"), "E": shower("error"),
} }

View File

@ -7,7 +7,7 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import itertools import collections
import platform import platform
import sys import sys
import time import time
@ -376,8 +376,11 @@ class TerminalReporter(object):
return return
running_xdist = hasattr(rep, "node") running_xdist = hasattr(rep, "node")
if markup is None: if markup is None:
if rep.passed: was_xfail = hasattr(report, "wasxfail")
if rep.passed and not was_xfail:
markup = {"green": True} markup = {"green": True}
elif rep.passed and was_xfail:
markup = {"yellow": True}
elif rep.failed: elif rep.failed:
markup = {"red": True} markup = {"red": True}
elif rep.skipped: elif rep.skipped:
@ -728,33 +731,33 @@ class TerminalReporter(object):
final = hasattr(self, "_already_displayed_warnings") final = hasattr(self, "_already_displayed_warnings")
if final: if final:
warnings = all_warnings[self._already_displayed_warnings :] warning_reports = all_warnings[self._already_displayed_warnings :]
else: else:
warnings = all_warnings warning_reports = all_warnings
self._already_displayed_warnings = len(warnings) self._already_displayed_warnings = len(warning_reports)
if not warnings: if not warning_reports:
return return
grouped = itertools.groupby( reports_grouped_by_message = collections.OrderedDict()
warnings, key=lambda wr: wr.get_location(self.config) for wr in warning_reports:
) reports_grouped_by_message.setdefault(wr.message, []).append(wr)
title = "warnings summary (final)" if final else "warnings summary" title = "warnings summary (final)" if final else "warnings summary"
self.write_sep("=", title, yellow=True, bold=False) self.write_sep("=", title, yellow=True, bold=False)
for location, warning_records in grouped: for message, warning_reports in reports_grouped_by_message.items():
# legacy warnings show their location explicitly, while standard warnings look better without has_any_location = False
# it because the location is already formatted into the message for w in warning_reports:
warning_records = list(warning_records) location = w.get_location(self.config)
if location:
self._tw.line(str(location))
for w in warning_records:
if location: if location:
lines = w.message.splitlines() self._tw.line(str(location))
indented = "\n".join(" " + x for x in lines) has_any_location = True
message = indented.rstrip() if has_any_location:
else: lines = message.splitlines()
message = w.message.rstrip() indented = "\n".join(" " + x for x in lines)
self._tw.line(message) message = indented.rstrip()
else:
message = message.rstrip()
self._tw.line(message)
self._tw.line() self._tw.line()
self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html") self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html")
@ -824,8 +827,7 @@ class TerminalReporter(object):
self.write_sep("=", "ERRORS") self.write_sep("=", "ERRORS")
for rep in self.stats["error"]: for rep in self.stats["error"]:
msg = self._getfailureheadline(rep) msg = self._getfailureheadline(rep)
if not hasattr(rep, "when"): if rep.when == "collect":
# collect
msg = "ERROR collecting " + msg msg = "ERROR collecting " + msg
elif rep.when == "setup": elif rep.when == "setup":
msg = "ERROR at setup of " + msg msg = "ERROR at setup of " + msg

View File

@ -804,8 +804,8 @@ class TestInvocationVariants(object):
result = testdir.runpytest("-rf") result = testdir.runpytest("-rf")
lines = result.stdout.str().splitlines() lines = result.stdout.str().splitlines()
for line in lines: for line in lines:
if line.startswith("FAIL "): if line.startswith(("FAIL ", "FAILED ")):
testid = line[5:].strip() _fail, _sep, testid = line.partition(" ")
break break
result = testdir.runpytest(testid, "-rf") result = testdir.runpytest(testid, "-rf")
result.stdout.fnmatch_lines([line, "*1 failed*"]) result.stdout.fnmatch_lines([line, "*1 failed*"])

View File

@ -0,0 +1,6 @@
import pytest
@pytest.mark.foo
def test_mark():
pass

View File

@ -0,0 +1,16 @@
import warnings
import pytest
def func():
warnings.warn(UserWarning("foo"))
@pytest.mark.parametrize("i", range(5))
def test_foo(i):
func()
def test_bar():
func()

View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
from _pytest._io.saferepr import saferepr
def test_simple_repr():
assert saferepr(1) == "1"
assert saferepr(None) == "None"
def test_maxsize():
s = saferepr("x" * 50, maxsize=25)
assert len(s) == 25
expected = repr("x" * 10 + "..." + "x" * 10)
assert s == expected
def test_maxsize_error_on_instance():
class A:
def __repr__():
raise ValueError("...")
s = saferepr(("*" * 50, A()), maxsize=25)
assert len(s) == 25
assert s[0] == "(" and s[-1] == ")"
def test_exceptions():
class BrokenRepr:
def __init__(self, ex):
self.ex = ex
def __repr__(self):
raise self.ex
class BrokenReprException(Exception):
__str__ = None
__repr__ = None
assert "Exception" in saferepr(BrokenRepr(Exception("broken")))
s = saferepr(BrokenReprException("really broken"))
assert "TypeError" in s
assert "TypeError" in saferepr(BrokenRepr("string"))
s2 = saferepr(BrokenRepr(BrokenReprException("omg even worse")))
assert "NameError" not in s2
assert "unknown" in s2
def test_big_repr():
from _pytest._io.saferepr import SafeRepr
assert len(saferepr(range(1000))) <= len("[" + SafeRepr().maxlist * "1000" + "]")
def test_repr_on_newstyle():
class Function(object):
def __repr__(self):
return "<%s>" % (self.name)
assert saferepr(Function())
def test_unicode():
val = u"£€"
reprval = u"'£€'"
assert saferepr(val) == reprval

View File

@ -960,7 +960,7 @@ class TestTracebackCutting(object):
def test_filter_traceback_generated_code(self): def test_filter_traceback_generated_code(self):
"""test that filter_traceback() works with the fact that """test that filter_traceback() works with the fact that
py.code.Code.path attribute might return an str object. _pytest._code.code.Code.path attribute might return an str object.
In this case, one of the entries on the traceback was produced by In this case, one of the entries on the traceback was produced by
dynamically generated code. dynamically generated code.
See: https://bitbucket.org/pytest-dev/py/issues/71 See: https://bitbucket.org/pytest-dev/py/issues/71
@ -981,7 +981,7 @@ class TestTracebackCutting(object):
def test_filter_traceback_path_no_longer_valid(self, testdir): def test_filter_traceback_path_no_longer_valid(self, testdir):
"""test that filter_traceback() works with the fact that """test that filter_traceback() works with the fact that
py.code.Code.path attribute might return an str object. _pytest._code.code.Code.path attribute might return an str object.
In this case, one of the files in the traceback no longer exists. In this case, one of the files in the traceback no longer exists.
This fixes #1133. This fixes #1133.
""" """

View File

@ -7,7 +7,6 @@ import sys
import textwrap import textwrap
import attr import attr
import py
import six import six
import _pytest.assertion as plugin import _pytest.assertion as plugin
@ -455,10 +454,13 @@ class TestAssert_reprcompare(object):
assert len(expl) > 1 assert len(expl) > 1
def test_Sequence(self): def test_Sequence(self):
col = py.builtin._tryimport("collections.abc", "collections", "sys") if sys.version_info >= (3, 3):
if not hasattr(col, "MutableSequence"): import collections.abc as collections_abc
else:
import collections as collections_abc
if not hasattr(collections_abc, "MutableSequence"):
pytest.skip("cannot import MutableSequence") pytest.skip("cannot import MutableSequence")
MutableSequence = col.MutableSequence MutableSequence = collections_abc.MutableSequence
class TestSequence(MutableSequence): # works with a Sequence subclass class TestSequence(MutableSequence): # works with a Sequence subclass
def __init__(self, iterable): def __init__(self, iterable):

View File

@ -4,8 +4,10 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
import contextlib import contextlib
import io
import os import os
import pickle import pickle
import subprocess
import sys import sys
import textwrap import textwrap
from io import UnsupportedOperation from io import UnsupportedOperation
@ -850,15 +852,6 @@ class TestCaptureIO(object):
assert f.getvalue() == "foo\r\n" assert f.getvalue() == "foo\r\n"
def test_bytes_io():
f = py.io.BytesIO()
f.write(b"hello")
with pytest.raises(TypeError):
f.write(u"hello")
s = f.getvalue()
assert s == b"hello"
def test_dontreadfrominput(): def test_dontreadfrominput():
from _pytest.capture import DontReadFromInput from _pytest.capture import DontReadFromInput
@ -933,18 +926,18 @@ def test_dupfile(tmpfile):
def test_dupfile_on_bytesio(): def test_dupfile_on_bytesio():
io = py.io.BytesIO() bio = io.BytesIO()
f = capture.safe_text_dupfile(io, "wb") f = capture.safe_text_dupfile(bio, "wb")
f.write("hello") f.write("hello")
assert io.getvalue() == b"hello" assert bio.getvalue() == b"hello"
assert "BytesIO object" in f.name assert "BytesIO object" in f.name
def test_dupfile_on_textio(): def test_dupfile_on_textio():
io = py.io.TextIO() tio = py.io.TextIO()
f = capture.safe_text_dupfile(io, "wb") f = capture.safe_text_dupfile(tio, "wb")
f.write("hello") f.write("hello")
assert io.getvalue() == "hello" assert tio.getvalue() == "hello"
assert not hasattr(f, "name") assert not hasattr(f, "name")
@ -952,12 +945,12 @@ def test_dupfile_on_textio():
def lsof_check(): def lsof_check():
pid = os.getpid() pid = os.getpid()
try: try:
out = py.process.cmdexec("lsof -p %d" % pid) out = subprocess.check_output(("lsof", "-p", str(pid))).decode()
except (py.process.cmdexec.Error, UnicodeDecodeError): except (OSError, subprocess.CalledProcessError, UnicodeDecodeError):
# about UnicodeDecodeError, see note on pytester # about UnicodeDecodeError, see note on pytester
pytest.skip("could not run 'lsof'") pytest.skip("could not run 'lsof'")
yield yield
out2 = py.process.cmdexec("lsof -p %d" % pid) out2 = subprocess.check_output(("lsof", "-p", str(pid))).decode()
len1 = len([x for x in out.split("\n") if "REG" in x]) len1 = len([x for x in out.split("\n") if "REG" in x])
len2 = len([x for x in out2.split("\n") if "REG" in x]) len2 = len([x for x in out2.split("\n") if "REG" in x])
assert len2 < len1 + 3, out2 assert len2 < len1 + 3, out2

View File

@ -292,6 +292,13 @@ def test_keyword_option_custom(spec, testdir):
assert list(passed) == list(passed_result) assert list(passed) == list(passed_result)
def test_keyword_option_considers_mark(testdir):
testdir.copy_example("marks/marks_considered_keywords")
rec = testdir.inline_run("-k", "foo")
passed = rec.listoutcomes()[0]
assert len(passed) == 1
@pytest.mark.parametrize( @pytest.mark.parametrize(
"spec", "spec",
[ [

View File

@ -391,6 +391,33 @@ def test_issue156_undo_staticmethod(Sample):
assert Sample.hello() assert Sample.hello()
def test_undo_class_descriptors_delattr():
class SampleParent(object):
@classmethod
def hello(_cls):
pass
@staticmethod
def world():
pass
class SampleChild(SampleParent):
pass
monkeypatch = MonkeyPatch()
original_hello = SampleChild.hello
original_world = SampleChild.world
monkeypatch.delattr(SampleParent, "hello")
monkeypatch.delattr(SampleParent, "world")
assert getattr(SampleParent, "hello", None) is None
assert getattr(SampleParent, "world", None) is None
monkeypatch.undo()
assert original_hello == SampleChild.hello
assert original_world == SampleChild.world
def test_issue1338_name_resolving(): def test_issue1338_name_resolving():
pytest.importorskip("requests") pytest.importorskip("requests")
monkeypatch = MonkeyPatch() monkeypatch = MonkeyPatch()

View File

@ -1,3 +1,4 @@
# encoding: utf-8
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import division from __future__ import division
from __future__ import print_function from __future__ import print_function
@ -366,3 +367,17 @@ def test_nottest_class_decorator(testdir):
assert not reprec.getfailedcollections() assert not reprec.getfailedcollections()
calls = reprec.getreports("pytest_runtest_logreport") calls = reprec.getreports("pytest_runtest_logreport")
assert not calls assert not calls
def test_skip_test_with_unicode(testdir):
testdir.makepyfile(
"""
# encoding: utf-8
import unittest
class TestClass():
def test_io(self):
raise unittest.SkipTest(u'😊')
"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines("* 1 skipped *")

View File

@ -3,6 +3,7 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import distutils.spawn
import os import os
import sys import sys
@ -296,7 +297,7 @@ class TestParser(object):
def test_argcomplete(testdir, monkeypatch): def test_argcomplete(testdir, monkeypatch):
if not py.path.local.sysfind("bash"): if not distutils.spawn.find_executable("bash"):
pytest.skip("bash not available") pytest.skip("bash not available")
script = str(testdir.tmpdir.join("test_argcomplete")) script = str(testdir.tmpdir.join("test_argcomplete"))
pytest_bin = sys.argv[0] pytest_bin = sys.argv[0]

View File

@ -770,6 +770,7 @@ def test_skip_reasons_folding():
# ev3 might be a collection report # ev3 might be a collection report
ev3 = X() ev3 = X()
ev3.when = "collect"
ev3.longrepr = longrepr ev3.longrepr = longrepr
ev3.skipped = True ev3.skipped = True
@ -1202,6 +1203,6 @@ def test_summary_list_after_errors(testdir):
[ [
"=* FAILURES *=", "=* FAILURES *=",
"*= short test summary info =*", "*= short test summary info =*",
"FAIL test_summary_list_after_errors.py::test_fail", "FAILED test_summary_list_after_errors.py::test_fail",
] ]
) )

View File

@ -614,7 +614,7 @@ class TestTerminalFunctional(object):
"*test_verbose_reporting.py::test_fail *FAIL*", "*test_verbose_reporting.py::test_fail *FAIL*",
"*test_verbose_reporting.py::test_pass *PASS*", "*test_verbose_reporting.py::test_pass *PASS*",
"*test_verbose_reporting.py::TestClass::test_skip *SKIP*", "*test_verbose_reporting.py::TestClass::test_skip *SKIP*",
"*test_verbose_reporting.py::test_gen *xfail*", "*test_verbose_reporting.py::test_gen *XFAIL*",
] ]
) )
assert result.ret == 1 assert result.ret == 1

View File

@ -121,6 +121,22 @@ def test_tmpdir_always_is_realpath(testdir):
assert not result.ret assert not result.ret
def test_tmp_path_always_is_realpath(testdir, monkeypatch):
# for reasoning see: test_tmpdir_always_is_realpath test-case
realtemp = testdir.tmpdir.mkdir("myrealtemp")
linktemp = testdir.tmpdir.join("symlinktemp")
attempt_symlink_to(linktemp, str(realtemp))
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(linktemp))
testdir.makepyfile(
"""
def test_1(tmp_path):
assert tmp_path.resolve() == tmp_path
"""
)
reprec = testdir.inline_run()
reprec.assertoutcome(passed=1)
def test_tmpdir_too_long_on_parametrization(testdir): def test_tmpdir_too_long_on_parametrization(testdir):
testdir.makepyfile( testdir.makepyfile(
""" """

View File

@ -693,3 +693,22 @@ def test_warnings_checker_twice():
warnings.warn("Message A", UserWarning) warnings.warn("Message A", UserWarning)
with expectation: with expectation:
warnings.warn("Message B", UserWarning) warnings.warn("Message B", UserWarning)
@pytest.mark.filterwarnings("always")
def test_group_warnings_by_message(testdir):
testdir.copy_example("warnings/test_group_warnings_by_message.py")
result = testdir.runpytest()
result.stdout.fnmatch_lines(
[
"test_group_warnings_by_message.py::test_foo[0]",
"test_group_warnings_by_message.py::test_foo[1]",
"test_group_warnings_by_message.py::test_foo[2]",
"test_group_warnings_by_message.py::test_foo[3]",
"test_group_warnings_by_message.py::test_foo[4]",
"test_group_warnings_by_message.py::test_bar",
]
)
warning_code = 'warnings.warn(UserWarning("foo"))'
assert warning_code in result.stdout.str()
assert result.stdout.str().count(warning_code) == 1