From 78194093afe6bbb82aa2e636b67046ce96b7c238 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 16 Mar 2017 21:55:03 -0300 Subject: [PATCH] Improve warning representation in terminal plugin and fix tests --- _pytest/config.py | 4 ++-- _pytest/fixtures.py | 2 +- _pytest/terminal.py | 43 +++++++++++++++++++++++++++++++------- testing/deprecated_test.py | 2 +- testing/python/collect.py | 10 ++++----- testing/test_assertion.py | 5 ++++- testing/test_config.py | 2 +- testing/test_junitxml.py | 2 +- testing/test_warnings.py | 8 +++---- 9 files changed, 55 insertions(+), 23 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index 861775b87..0874c4599 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -910,11 +910,11 @@ class Config(object): fin = self._cleanup.pop() fin() - def warn(self, code, message, fslocation=None): + def warn(self, code, message, fslocation=None, nodeid=None): """ generate a warning for this test session. """ self.hook.pytest_logwarning.call_historic(kwargs=dict( code=code, message=message, - fslocation=fslocation, nodeid=None)) + fslocation=fslocation, nodeid=nodeid)) def get_terminal_writer(self): return self.pluginmanager.get_plugin("terminalreporter")._tw diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index c4d21635f..cd5c673ca 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -1080,7 +1080,7 @@ class FixtureManager(object): continue marker = defaultfuncargprefixmarker 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):] elif not isinstance(marker, FixtureFunctionMarker): # magic globals with __getattr__ might have got us a wrong diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 741a0b600..dd92ddfa3 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -2,7 +2,6 @@ This is a good source for looking at the various reporting hooks. """ -import operator import itertools from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ @@ -83,13 +82,40 @@ def pytest_report_teststatus(report): letter = "f" return report.outcome, letter, report.outcome.upper() + class WarningReport(object): + """ + Simple structure to hold warnings information captured by ``pytest_logwarning``. + """ 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.message = message self.nodeid = nodeid 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): def __init__(self, config, file=None): @@ -169,8 +195,6 @@ class TerminalReporter(object): def pytest_logwarning(self, code, fslocation, message, nodeid): warnings = self.stats.setdefault("warnings", []) - if isinstance(fslocation, tuple): - fslocation = "%s:%d" % fslocation warning = WarningReport(code=code, fslocation=fslocation, message=message, nodeid=nodeid) warnings.append(warning) @@ -444,12 +468,17 @@ class TerminalReporter(object): all_warnings = self.stats.get("warnings") if not all_warnings: return + + grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config)) + self.write_sep("=", "warnings summary", yellow=True, bold=False) - grouped = itertools.groupby(all_warnings, key=operator.attrgetter('nodeid')) - for nodeid, warnings in grouped: - self._tw.line(str(nodeid)) + for location, warnings in grouped: + self._tw.line(str(location) or '') 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') def summary_passes(self): diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index bad844281..ac1abda9c 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -26,7 +26,7 @@ def test_funcarg_prefix_deprecation(testdir): """) result = testdir.runpytest('-ra') result.stdout.fnmatch_lines([ - ('pytest_funcarg__value: ' + ('*pytest_funcarg__value: ' 'declaring fixtures using "pytest_funcarg__" prefix is deprecated ' 'and scheduled to be removed in pytest 4.0. ' 'Please remove the prefix and use the @pytest.fixture decorator instead.'), diff --git a/testing/python/collect.py b/testing/python/collect.py index e4069983a..57dcaf54f 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -113,9 +113,9 @@ class TestClass(object): pass """) result = testdir.runpytest("-rw") - result.stdout.fnmatch_lines_random(""" - WC1*test_class_with_init_warning.py*__init__* - """) + result.stdout.fnmatch_lines([ + "*cannot collect test class 'TestClass1' because it has a __init__ constructor", + ]) def test_class_subclassobject(self, testdir): testdir.getmodulecol(""" @@ -1241,8 +1241,8 @@ def test_dont_collect_non_function_callable(testdir): result = testdir.runpytest('-rw') result.stdout.fnmatch_lines([ '*collected 1 item*', - 'WC2 *', - '*1 passed, 1 pytest-warnings in *', + "*cannot collect 'test_a' because it is not a function*", + '*1 passed, 1 warnings in *', ]) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 8bfe65abd..2c04df63e 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -975,7 +975,10 @@ def test_assert_tuple_warning(testdir): assert(False, 'you shall not pass') """) 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): testdir.makepyfile(""" diff --git a/testing/test_config.py b/testing/test_config.py index 21142c8df..40b944adc 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -638,7 +638,7 @@ class TestWarning(object): result = testdir.runpytest() result.stdout.fnmatch_lines(""" ===*warnings summary*=== - *test_warn_on_test_item_from_request::test_hello* + *test_warn_on_test_item_from_request.py::test_hello* *hello* """) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index d4e4d4c09..a87745350 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -854,7 +854,7 @@ def test_record_property(testdir): pnodes[1].assert_attr(name="foo", value="<1") result.stdout.fnmatch_lines([ 'test_record_property.py::test_record', - 'record_xml_property*experimental*', + '*record_xml_property*experimental*', ]) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index ad80325f5..e0baed8d1 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -39,10 +39,10 @@ def test_normal_flow(testdir, pyfile_with_warnings): '*test_normal_flow.py::test_func', '*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', - ' warnings.warn(DeprecationWarning("functionality is deprecated"))', + '* warnings.warn(DeprecationWarning("functionality is deprecated"))', '* 1 passed, 2 warnings*', ]) 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, '*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', - ' warnings.warn(UserWarning("warning during teardown"))', + '*warnings.warn(UserWarning("warning during teardown"))', '* 1 passed, 2 warnings*', ])