Improve warning representation in terminal plugin and fix tests

This commit is contained in:
Bruno Oliveira 2017-03-16 21:55:03 -03:00
parent be5db6fa22
commit 78194093af
9 changed files with 55 additions and 23 deletions

View File

@ -910,11 +910,11 @@ class Config(object):
fin = self._cleanup.pop() fin = self._cleanup.pop()
fin() fin()
def warn(self, code, message, fslocation=None): def warn(self, code, message, fslocation=None, nodeid=None):
""" generate a warning for this test session. """ """ generate a warning for this test session. """
self.hook.pytest_logwarning.call_historic(kwargs=dict( self.hook.pytest_logwarning.call_historic(kwargs=dict(
code=code, message=message, code=code, message=message,
fslocation=fslocation, nodeid=None)) fslocation=fslocation, nodeid=nodeid))
def get_terminal_writer(self): def get_terminal_writer(self):
return self.pluginmanager.get_plugin("terminalreporter")._tw return self.pluginmanager.get_plugin("terminalreporter")._tw

View File

@ -1080,7 +1080,7 @@ class FixtureManager(object):
continue continue
marker = defaultfuncargprefixmarker marker = defaultfuncargprefixmarker
from _pytest import deprecated from _pytest import deprecated
self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name)) self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid)
name = name[len(self._argprefix):] name = name[len(self._argprefix):]
elif not isinstance(marker, FixtureFunctionMarker): elif not isinstance(marker, FixtureFunctionMarker):
# magic globals with __getattr__ might have got us a wrong # magic globals with __getattr__ might have got us a wrong

View File

@ -2,7 +2,6 @@
This is a good source for looking at the various reporting hooks. This is a good source for looking at the various reporting hooks.
""" """
import operator
import itertools import itertools
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
@ -83,13 +82,40 @@ def pytest_report_teststatus(report):
letter = "f" letter = "f"
return report.outcome, letter, report.outcome.upper() return report.outcome, letter, report.outcome.upper()
class WarningReport(object): class WarningReport(object):
"""
Simple structure to hold warnings information captured by ``pytest_logwarning``.
"""
def __init__(self, code, message, nodeid=None, fslocation=None): def __init__(self, code, message, nodeid=None, fslocation=None):
"""
:param code: unused
:param str message: user friendly message about the warning
:param str|None nodeid: node id that generated the warning (see ``get_location``).
:param tuple|py.path.local fslocation:
file system location of the source of the warning (see ``get_location``).
"""
self.code = code self.code = code
self.message = message self.message = message
self.nodeid = nodeid self.nodeid = nodeid
self.fslocation = fslocation self.fslocation = fslocation
def get_location(self, config):
"""
Returns the more user-friendly information about the location
of a warning, or None.
"""
if self.nodeid:
return self.nodeid
if self.fslocation:
if isinstance(self.fslocation, tuple) and len(self.fslocation) == 2:
filename, linenum = self.fslocation
relpath = py.path.local(filename).relto(config.invocation_dir)
return '%s:%d' % (relpath, linenum)
else:
return str(self.fslocation)
return None
class TerminalReporter(object): class TerminalReporter(object):
def __init__(self, config, file=None): def __init__(self, config, file=None):
@ -169,8 +195,6 @@ class TerminalReporter(object):
def pytest_logwarning(self, code, fslocation, message, nodeid): def pytest_logwarning(self, code, fslocation, message, nodeid):
warnings = self.stats.setdefault("warnings", []) warnings = self.stats.setdefault("warnings", [])
if isinstance(fslocation, tuple):
fslocation = "%s:%d" % fslocation
warning = WarningReport(code=code, fslocation=fslocation, warning = WarningReport(code=code, fslocation=fslocation,
message=message, nodeid=nodeid) message=message, nodeid=nodeid)
warnings.append(warning) warnings.append(warning)
@ -444,12 +468,17 @@ class TerminalReporter(object):
all_warnings = self.stats.get("warnings") all_warnings = self.stats.get("warnings")
if not all_warnings: if not all_warnings:
return return
grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config))
self.write_sep("=", "warnings summary", yellow=True, bold=False) self.write_sep("=", "warnings summary", yellow=True, bold=False)
grouped = itertools.groupby(all_warnings, key=operator.attrgetter('nodeid')) for location, warnings in grouped:
for nodeid, warnings in grouped: self._tw.line(str(location) or '<undetermined location>')
self._tw.line(str(nodeid))
for w in warnings: for w in warnings:
self._tw.line(w.message) lines = w.message.splitlines()
indented = '\n'.join(' ' + x for x in lines)
self._tw.line(indented)
self._tw.line()
self._tw.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html') self._tw.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html')
def summary_passes(self): def summary_passes(self):

View File

@ -26,7 +26,7 @@ def test_funcarg_prefix_deprecation(testdir):
""") """)
result = testdir.runpytest('-ra') result = testdir.runpytest('-ra')
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
('pytest_funcarg__value: ' ('*pytest_funcarg__value: '
'declaring fixtures using "pytest_funcarg__" prefix is deprecated ' 'declaring fixtures using "pytest_funcarg__" prefix is deprecated '
'and scheduled to be removed in pytest 4.0. ' 'and scheduled to be removed in pytest 4.0. '
'Please remove the prefix and use the @pytest.fixture decorator instead.'), 'Please remove the prefix and use the @pytest.fixture decorator instead.'),

View File

@ -113,9 +113,9 @@ class TestClass(object):
pass pass
""") """)
result = testdir.runpytest("-rw") result = testdir.runpytest("-rw")
result.stdout.fnmatch_lines_random(""" result.stdout.fnmatch_lines([
WC1*test_class_with_init_warning.py*__init__* "*cannot collect test class 'TestClass1' because it has a __init__ constructor",
""") ])
def test_class_subclassobject(self, testdir): def test_class_subclassobject(self, testdir):
testdir.getmodulecol(""" testdir.getmodulecol("""
@ -1241,8 +1241,8 @@ def test_dont_collect_non_function_callable(testdir):
result = testdir.runpytest('-rw') result = testdir.runpytest('-rw')
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
'*collected 1 item*', '*collected 1 item*',
'WC2 *', "*cannot collect 'test_a' because it is not a function*",
'*1 passed, 1 pytest-warnings in *', '*1 passed, 1 warnings in *',
]) ])

View File

@ -975,7 +975,10 @@ def test_assert_tuple_warning(testdir):
assert(False, 'you shall not pass') assert(False, 'you shall not pass')
""") """)
result = testdir.runpytest('-rw') result = testdir.runpytest('-rw')
result.stdout.fnmatch_lines('*test_assert_tuple_warning.py:2 assertion is always true*') result.stdout.fnmatch_lines([
'*test_assert_tuple_warning.py:2',
'*assertion is always true*',
])
def test_assert_indirect_tuple_no_warning(testdir): def test_assert_indirect_tuple_no_warning(testdir):
testdir.makepyfile(""" testdir.makepyfile("""

View File

@ -638,7 +638,7 @@ class TestWarning(object):
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines(""" result.stdout.fnmatch_lines("""
===*warnings summary*=== ===*warnings summary*===
*test_warn_on_test_item_from_request::test_hello* *test_warn_on_test_item_from_request.py::test_hello*
*hello* *hello*
""") """)

View File

@ -854,7 +854,7 @@ def test_record_property(testdir):
pnodes[1].assert_attr(name="foo", value="<1") pnodes[1].assert_attr(name="foo", value="<1")
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
'test_record_property.py::test_record', 'test_record_property.py::test_record',
'record_xml_property*experimental*', '*record_xml_property*experimental*',
]) ])

View File

@ -39,10 +39,10 @@ def test_normal_flow(testdir, pyfile_with_warnings):
'*test_normal_flow.py::test_func', '*test_normal_flow.py::test_func',
'*normal_flow_module.py:3: PendingDeprecationWarning: functionality is pending deprecation', '*normal_flow_module.py:3: PendingDeprecationWarning: functionality is pending deprecation',
' warnings.warn(PendingDeprecationWarning("functionality is pending deprecation"))', '* warnings.warn(PendingDeprecationWarning("functionality is pending deprecation"))',
'*normal_flow_module.py:4: DeprecationWarning: functionality is deprecated', '*normal_flow_module.py:4: DeprecationWarning: functionality is deprecated',
' warnings.warn(DeprecationWarning("functionality is deprecated"))', '* warnings.warn(DeprecationWarning("functionality is deprecated"))',
'* 1 passed, 2 warnings*', '* 1 passed, 2 warnings*',
]) ])
assert result.stdout.str().count('test_normal_flow.py::test_func') == 1 assert result.stdout.str().count('test_normal_flow.py::test_func') == 1
@ -67,10 +67,10 @@ def test_setup_teardown_warnings(testdir, pyfile_with_warnings):
'*== %s ==*' % WARNINGS_SUMMARY_HEADER, '*== %s ==*' % WARNINGS_SUMMARY_HEADER,
'*test_setup_teardown_warnings.py:6: UserWarning: warning during setup', '*test_setup_teardown_warnings.py:6: UserWarning: warning during setup',
' warnings.warn(UserWarning("warning during setup"))', '*warnings.warn(UserWarning("warning during setup"))',
'*test_setup_teardown_warnings.py:8: UserWarning: warning during teardown', '*test_setup_teardown_warnings.py:8: UserWarning: warning during teardown',
' warnings.warn(UserWarning("warning during teardown"))', '*warnings.warn(UserWarning("warning during teardown"))',
'* 1 passed, 2 warnings*', '* 1 passed, 2 warnings*',
]) ])