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

This commit is contained in:
Bruno Oliveira
2019-01-29 19:36:56 -02:00
48 changed files with 468 additions and 144 deletions

View File

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

View File

@@ -237,9 +237,7 @@ def getfslineno(obj):
def findsource(obj):
try:
sourcelines, lineno = inspect.findsource(obj)
except py.builtin._sysex:
raise
except: # noqa
except Exception:
return None, -1
source = Source()
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 six
from _pytest._io.saferepr import saferepr
from _pytest.assertion import util
from _pytest.compat import spec_from_file_location
from _pytest.pathlib import fnmatch_ex
@@ -471,7 +472,7 @@ def _saferepr(obj):
JSON reprs.
"""
r = py.io.saferepr(obj)
r = saferepr(obj)
# only occurs in python2.x, repr must return text in python3+
if isinstance(r, bytes):
# Represent unprintable bytes as `\x##`
@@ -490,7 +491,7 @@ def _format_assertmsg(obj):
For strings this simply replaces newlines with '\n~' so that
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
@@ -499,7 +500,7 @@ def _format_assertmsg(obj):
# However in either case we want to preserve the newline.
replaces = [(u"\n", u"\n~"), (u"%", u"%%")]
if not isinstance(obj, six.string_types):
obj = py.io.saferepr(obj)
obj = saferepr(obj)
replaces.append((u"\\n", u"\n~"))
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
# docstrings and __future__ imports.
aliases = [
ast.alias(py.builtin.builtins.__name__, "@py_builtins"),
ast.alias(six.moves.builtins.__name__, "@py_builtins"),
ast.alias("_pytest.assertion.rewrite", "@pytest_ar"),
]
doc = getattr(mod, "docstring", None)
@@ -740,7 +741,7 @@ class AssertionRewriter(ast.NodeVisitor):
return ast.Name(name, ast.Load())
def display(self, expr):
"""Call py.io.saferepr on the expression."""
"""Call saferepr on the expression."""
return self.helper("saferepr", expr)
def helper(self, name, *args):

View File

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

View File

@@ -17,6 +17,7 @@ import six
from six import text_type
import _pytest
from _pytest._io.saferepr import saferepr
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
@@ -294,7 +295,7 @@ def get_real_func(obj):
else:
raise ValueError(
("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):

View File

@@ -14,10 +14,10 @@ import attr
import py
import six
from more_itertools import flatten
from py._code.code import FormattedExcinfo
import _pytest
from _pytest import nodes
from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr
from _pytest.compat import _format_args
from _pytest.compat import _PytestWrapper

View File

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

View File

@@ -45,13 +45,14 @@ class KeywordMapping(object):
mapped_names.add(item.name)
# Add the names added as extra keywords to current or parent items
for name in item.listextrakeywords():
mapped_names.add(name)
mapped_names.update(item.listextrakeywords())
# Add the names attached to the current function through direct assignment
if hasattr(item, "function"):
for name in item.function.__dict__:
mapped_names.add(name)
mapped_names.update(item.function.__dict__)
# 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)

View File

@@ -181,6 +181,8 @@ class MonkeyPatch(object):
attribute is missing.
"""
__tracebackhide__ = True
import inspect
if name is notset:
if not isinstance(target, six.string_types):
raise TypeError(
@@ -194,7 +196,11 @@ class MonkeyPatch(object):
if raising:
raise AttributeError(name)
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)
def setitem(self, dic, name, value):

View File

@@ -5,6 +5,8 @@ from __future__ import print_function
import sys
import six
from _pytest import python
from _pytest import runner
from _pytest import unittest
@@ -24,7 +26,7 @@ def pytest_runtest_makereport(item, call):
if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()):
# let's substitute the excinfo with a pytest.skip one
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

View File

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

View File

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

View File

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

View File

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

View File

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