diff --git a/_pytest/config.py b/_pytest/config.py index 338b40302..8ac5d1a1b 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -613,6 +613,11 @@ class Config(object): self.hook.pytest_unconfigure(config=self) self.pluginmanager.ensure_shutdown() + def warn(self, code, message): + """ generate a warning for this test session. """ + self.hook.pytest_logwarning(code=code, message=message, + fslocation=None, nodeid=None) + def pytest_cmdline_parse(self, pluginmanager, args): assert self == pluginmanager.config, (self, pluginmanager.config) self.parse(args) diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index 244100f5a..c33b6d172 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -227,6 +227,11 @@ pytest_report_teststatus.firstresult = True def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ +def pytest_logwarning(message, code, nodeid, fslocation): + """ process a warning specified by a message, a code string, + a nodeid and fslocation (both of which may be None + if the warning is not tied to a partilar node/location).""" + # ------------------------------------------------------------------------- # doctest hooks # ------------------------------------------------------------------------- diff --git a/_pytest/main.py b/_pytest/main.py index 3ebfc4dde..f7060bf6f 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -263,6 +263,20 @@ class Node(object): return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) + def warn(self, code, message): + """ generate a warning with the given code and message for this + item. """ + assert isinstance(code, str) + fslocation = getattr(self, "location", None) + if fslocation is None: + fslocation = getattr(self, "fspath", None) + else: + fslocation = "%s:%s" % fslocation[:2] + + self.ihook.pytest_logwarning(code=code, message=message, + nodeid=self.nodeid, + fslocation=fslocation) + # methods for ordering nodes @property def nodeid(self): diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 1b9f8905f..f85cbeca1 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -75,6 +75,14 @@ def pytest_report_teststatus(report): letter = "f" return report.outcome, letter, report.outcome.upper() +class WarningReport: + def __init__(self, code, message, nodeid=None, fslocation=None): + self.code = code + self.message = message + self.nodeid = nodeid + self.fslocation = fslocation + + class TerminalReporter: def __init__(self, config, file=None): self.config = config @@ -151,6 +159,12 @@ class TerminalReporter: self.write_line("INTERNALERROR> " + line) return 1 + def pytest_logwarning(self, code, fslocation, message, nodeid): + warnings = self.stats.setdefault("warnings", []) + warning = WarningReport(code=code, fslocation=fslocation, + message=message, nodeid=nodeid) + warnings.append(warning) + def pytest_plugin_registered(self, plugin): if self.config.option.traceconfig: msg = "PLUGIN registered: %s" % (plugin,) @@ -335,6 +349,7 @@ class TerminalReporter: self.summary_errors() self.summary_failures() self.summary_hints() + self.summary_warnings() self.config.hook.pytest_terminal_summary(terminalreporter=self) if exitstatus == 2: self._report_keyboardinterrupt() @@ -405,6 +420,16 @@ class TerminalReporter: for hint in self.config.pluginmanager._hints: self._tw.line("hint: %s" % hint) + def summary_warnings(self): + if self.hasopt("w"): + warnings = self.stats.get("warnings") + if not warnings: + return + self.write_sep("=", "warning summary") + for w in warnings: + self._tw.line("W%s %s %s" % (w.code, + w.fslocation, w.message)) + def summary_failures(self): if self.config.option.tbstyle != "no": reports = self.getreports('failed') @@ -449,7 +474,8 @@ class TerminalReporter: def summary_stats(self): session_duration = py.std.time.time() - self._sessionstarttime - keys = "failed passed skipped deselected xfailed xpassed".split() + keys = ("failed passed skipped deselected " + "xfailed xpassed warnings").split() for key in self.stats.keys(): if key not in keys: keys.append(key) diff --git a/testing/test_config.py b/testing/test_config.py index f0ee1a670..4d34876a4 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -360,4 +360,43 @@ def test_load_initial_conftest_last_ordering(testdir): assert l[-2] == m.pytest_load_initial_conftests assert l[-3].__module__ == "_pytest.config" +class TestWarning: + def test_warn_config(self, testdir): + testdir.makeconftest(""" + l = [] + def pytest_configure(config): + config.warn("C1", "hello") + def pytest_logwarning(code, message): + assert code == "C1" + assert message == "hello" + l.append(1) + """) + testdir.makepyfile(""" + def test_proper(pytestconfig): + import conftest + assert conftest.l == [1] + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + def test_warn_on_test_item_from_request(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.fixture + def fix(request): + request.node.warn("T1", "hello") + def test_hello(fix): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + *1 warning* + """) + assert "hello" not in result.stdout.str() + result = testdir.runpytest("-rw") + result.stdout.fnmatch_lines(""" + ===*warning summary*=== + *WT1*test_warn_on_test_item*:5*hello* + *1 warning* + """)