diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 37570ed43..05c868079 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,10 @@ New Features * ``pytest.param`` can be used to declare test parameter sets with marks and test ids. Thanks `@RonnyPfannschmidt`_ for the PR. +* The ``pytest-warnings`` plugin has been integrated into the core, so now ``pytest`` automatically + captures and displays warnings at the end of the test session. + Thanks `@nicoddemus`_ for the PR. + Changes ------- diff --git a/_pytest/config.py b/_pytest/config.py index 92f9005b4..db37280ca 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -99,7 +99,7 @@ default_plugins = ( "mark main terminal runner python fixtures debugging unittest capture skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion " "junitxml resultlog doctest cacheprovider freeze_support " - "setuponly setupplan").split() + "setuponly setupplan warnings").split() builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") @@ -911,11 +911,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 a151d615d..9196e4fdd 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -1081,7 +1081,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/pytester.py b/_pytest/pytester.py index ee0e5bbe7..6381595ee 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1008,7 +1008,7 @@ class Testdir(object): The pexpect child is returned. """ - basetemp = self.tmpdir.mkdir("pexpect") + basetemp = self.tmpdir.mkdir("temp-pexpect") invoke = " ".join(map(str, self._getpytestargs())) cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) return self.spawn(cmd, expect_timeout=expect_timeout) diff --git a/_pytest/python.py b/_pytest/python.py index 81eed00f0..e763aa888 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -936,7 +936,7 @@ def _idval(val, argname, idx, idfn, config=None): import warnings msg = "Raised while trying to determine id of parameter %s at position %d." % (argname, idx) msg += '\nUpdate your code as this will raise an error in pytest-4.0.' - warnings.warn(msg) + warnings.warn(msg, DeprecationWarning) if s: return _escape_strings(s) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 91cc85fdf..579439fe4 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -56,14 +56,12 @@ def deprecated_call(func=None, *args, **kwargs): def warn_explicit(message, category, *args, **kwargs): categories.append(category) - old_warn_explicit(message, category, *args, **kwargs) def warn(message, category=None, *args, **kwargs): if isinstance(message, Warning): categories.append(message.__class__) else: categories.append(category) - old_warn(message, category, *args, **kwargs) old_warn = warnings.warn old_warn_explicit = warnings.warn_explicit diff --git a/_pytest/runner.py b/_pytest/runner.py index 4277f8ee3..07f4be019 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -554,14 +554,21 @@ def importorskip(modname, minversion=None): __version__ attribute. If no minversion is specified the a skip is only triggered if the module can not be imported. """ + import warnings __tracebackhide__ = True compile(modname, '', 'eval') # to catch syntaxerrors should_skip = False - try: - __import__(modname) - except ImportError: - # Do not raise chained exception here(#1485) - should_skip = True + + with warnings.catch_warnings(): + # make sure to ignore ImportWarnings that might happen because + # of existing directories with the same name we're trying to + # import but without a __init__.py file + warnings.simplefilter('ignore') + try: + __import__(modname) + except ImportError: + # Do not raise chained exception here(#1485) + should_skip = True if should_skip: raise Skipped("could not import %r" %(modname,), allow_module_level=True) mod = sys.modules[modname] diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 4607b752e..528772764 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -4,6 +4,7 @@ This is a good source for looking at the various reporting hooks. """ from __future__ import absolute_import, division, print_function +import itertools from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED import pytest @@ -26,11 +27,11 @@ def pytest_addoption(parser): help="show extra test summary info as specified by chars (f)ailed, " "(E)error, (s)skipped, (x)failed, (X)passed, " "(p)passed, (P)passed with output, (a)all except pP. " - "The pytest warnings are displayed at all times except when " - "--disable-pytest-warnings is set") - group._addoption('--disable-pytest-warnings', default=False, - dest='disablepytestwarnings', action='store_true', - help='disable warnings summary, overrides -r w flag') + "Warnings are displayed at all times except when " + "--disable-warnings is set") + group._addoption('--disable-warnings', '--disable-pytest-warnings', default=False, + dest='disable_warnings', action='store_true', + help='disable warnings summary') group._addoption('-l', '--showlocals', action="store_true", dest="showlocals", default=False, help="show locals in tracebacks (disabled by default).") @@ -59,9 +60,9 @@ def pytest_configure(config): def getreportopt(config): reportopts = "" reportchars = config.option.reportchars - if not config.option.disablepytestwarnings and 'w' not in reportchars: + if not config.option.disable_warnings and 'w' not in reportchars: reportchars += 'w' - elif config.option.disablepytestwarnings and 'w' in reportchars: + elif config.option.disable_warnings and 'w' in reportchars: reportchars = reportchars.replace('w', '') if reportchars: for char in reportchars: @@ -82,13 +83,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): @@ -168,8 +196,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) @@ -440,13 +466,21 @@ class TerminalReporter(object): def summary_warnings(self): if self.hasopt("w"): - warnings = self.stats.get("warnings") - if not warnings: + all_warnings = self.stats.get("warnings") + if not all_warnings: return - self.write_sep("=", "pytest-warning summary") - for w in warnings: - self._tw.line("W%s %s %s" % (w.code, - w.fslocation, w.message)) + + grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config)) + + self.write_sep("=", "warnings summary", yellow=True, bold=False) + for location, warnings in grouped: + self._tw.line(str(location) or '') + for w in warnings: + 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): if self.config.option.tbstyle != "no": @@ -548,8 +582,7 @@ def flatten(l): def build_summary_stats_line(stats): keys = ("failed passed skipped deselected " - "xfailed xpassed warnings error").split() - key_translation = {'warnings': 'pytest-warnings'} + "xfailed xpassed warnings error").split() unknown_key_seen = False for key in stats.keys(): if key not in keys: @@ -560,8 +593,7 @@ def build_summary_stats_line(stats): for key in keys: val = stats.get(key, None) if val: - key_name = key_translation.get(key, key) - parts.append("%d %s" % (len(val), key_name)) + parts.append("%d %s" % (len(val), key)) if parts: line = ", ".join(parts) diff --git a/_pytest/warnings.py b/_pytest/warnings.py new file mode 100644 index 000000000..bfa2b0087 --- /dev/null +++ b/_pytest/warnings.py @@ -0,0 +1,73 @@ +from __future__ import absolute_import, division, print_function + +import warnings +from contextlib import contextmanager + +import pytest + + +def _setoption(wmod, arg): + """ + Copy of the warning._setoption function but does not escape arguments. + """ + parts = arg.split(':') + if len(parts) > 5: + raise wmod._OptionError("too many fields (max 5): %r" % (arg,)) + while len(parts) < 5: + parts.append('') + action, message, category, module, lineno = [s.strip() + for s in parts] + action = wmod._getaction(action) + category = wmod._getcategory(category) + if lineno: + try: + lineno = int(lineno) + if lineno < 0: + raise ValueError + except (ValueError, OverflowError): + raise wmod._OptionError("invalid lineno %r" % (lineno,)) + else: + lineno = 0 + wmod.filterwarnings(action, message, category, module, lineno) + + +def pytest_addoption(parser): + group = parser.getgroup("pytest-warnings") + group.addoption( + '-W', '--pythonwarnings', action='append', + help="set which warnings to report, see -W option of python itself.") + parser.addini("filterwarnings", type="linelist", + help="Each line specifies warning filter pattern which would be passed" + "to warnings.filterwarnings. Process after -W and --pythonwarnings.") + + +@contextmanager +def catch_warnings_for_item(item): + """ + catches the warnings generated during setup/call/teardown execution + of the given item and after it is done posts them as warnings to this + item. + """ + args = item.config.getoption('pythonwarnings') or [] + inifilters = item.config.getini("filterwarnings") + with warnings.catch_warnings(record=True) as log: + warnings.simplefilter('once') + for arg in args: + warnings._setoption(arg) + + for arg in inifilters: + _setoption(warnings, arg) + + yield + + for warning in log: + msg = warnings.formatwarning( + warning.message, warning.category, + warning.filename, warning.lineno, warning.line) + item.warn("unused", msg) + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_protocol(item): + with catch_warnings_for_item(item): + yield diff --git a/doc/en/contents.rst b/doc/en/contents.rst index d7f900810..f6a561839 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -18,7 +18,7 @@ Full pytest documentation monkeypatch tmpdir capture - recwarn + warnings doctest mark skipping diff --git a/doc/en/customize.rst b/doc/en/customize.rst index c6d3eb473..4421889db 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -240,3 +240,23 @@ Builtin configuration file options By default, pytest will stop searching for ``conftest.py`` files upwards from ``pytest.ini``/``tox.ini``/``setup.cfg`` of the project if any, or up to the file-system root. + + +.. confval:: filterwarnings + + .. versionadded:: 3.1 + + Sets a list of filters and actions that should be taken for matched + warnings. By default all warnings emitted during the test session + will be displayed in a summary at the end of the test session. + + .. code-block:: ini + + # content of pytest.ini + [pytest] + filterwarnings = + error + ignore::DeprecationWarning + + This tells pytest to ignore deprecation warnings and turn all other warnings + into errors. For more information please refer to :ref:`warnings`. diff --git a/doc/en/recwarn.rst b/doc/en/recwarn.rst index 823ba945b..513af0d45 100644 --- a/doc/en/recwarn.rst +++ b/doc/en/recwarn.rst @@ -1,142 +1,3 @@ -.. _`asserting warnings`: +:orphan: -.. _assertwarnings: - -Asserting Warnings -===================================================== - -.. _`asserting warnings with the warns function`: - -.. _warns: - -Asserting warnings with the warns function ------------------------------------------------ - -.. versionadded:: 2.8 - -You can check that code raises a particular warning using ``pytest.warns``, -which works in a similar manner to :ref:`raises `:: - - import warnings - import pytest - - def test_warning(): - with pytest.warns(UserWarning): - warnings.warn("my warning", UserWarning) - -The test will fail if the warning in question is not raised. - -You can also call ``pytest.warns`` on a function or code string:: - - pytest.warns(expected_warning, func, *args, **kwargs) - pytest.warns(expected_warning, "func(*args, **kwargs)") - -The function also returns a list of all raised warnings (as -``warnings.WarningMessage`` objects), which you can query for -additional information:: - - with pytest.warns(RuntimeWarning) as record: - warnings.warn("another warning", RuntimeWarning) - - # check that only one warning was raised - assert len(record) == 1 - # check that the message matches - assert record[0].message.args[0] == "another warning" - -Alternatively, you can examine raised warnings in detail using the -:ref:`recwarn ` fixture (see below). - -.. note:: - ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated - differently; see :ref:`ensuring_function_triggers`. - -.. _`recording warnings`: - -.. _recwarn: - -Recording warnings ------------------------- - -You can record raised warnings either using ``pytest.warns`` or with -the ``recwarn`` fixture. - -To record with ``pytest.warns`` without asserting anything about the warnings, -pass ``None`` as the expected warning type:: - - with pytest.warns(None) as record: - warnings.warn("user", UserWarning) - warnings.warn("runtime", RuntimeWarning) - - assert len(record) == 2 - assert str(record[0].message) == "user" - assert str(record[1].message) == "runtime" - -The ``recwarn`` fixture will record warnings for the whole function:: - - import warnings - - def test_hello(recwarn): - warnings.warn("hello", UserWarning) - assert len(recwarn) == 1 - w = recwarn.pop(UserWarning) - assert issubclass(w.category, UserWarning) - assert str(w.message) == "hello" - assert w.filename - assert w.lineno - -Both ``recwarn`` and ``pytest.warns`` return the same interface for recorded -warnings: a WarningsRecorder instance. To view the recorded warnings, you can -iterate over this instance, call ``len`` on it to get the number of recorded -warnings, or index into it to get a particular recorded warning. It also -provides these methods: - -.. autoclass:: _pytest.recwarn.WarningsRecorder() - :members: - -Each recorded warning has the attributes ``message``, ``category``, -``filename``, ``lineno``, ``file``, and ``line``. The ``category`` is the -class of the warning. The ``message`` is the warning itself; calling -``str(message)`` will return the actual message of the warning. - -.. note:: - :class:`RecordedWarning` was changed from a plain class to a namedtuple in pytest 3.1 - -.. note:: - ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated - differently; see :ref:`ensuring_function_triggers`. - -.. _`ensuring a function triggers a deprecation warning`: - -.. _ensuring_function_triggers: - -Ensuring a function triggers a deprecation warning -------------------------------------------------------- - -You can also call a global helper for checking -that a certain function call triggers a ``DeprecationWarning`` or -``PendingDeprecationWarning``:: - - import pytest - - def test_global(): - pytest.deprecated_call(myfunction, 17) - -By default, ``DeprecationWarning`` and ``PendingDeprecationWarning`` will not be -caught when using ``pytest.warns`` or ``recwarn`` because default Python warnings filters hide -them. If you wish to record them in your own code, use the -command ``warnings.simplefilter('always')``:: - - import warnings - import pytest - - def test_deprecation(recwarn): - warnings.simplefilter('always') - warnings.warn("deprecated", DeprecationWarning) - assert len(recwarn) == 1 - assert recwarn.pop(DeprecationWarning) - -You can also use it as a contextmanager:: - - def test_global(): - with pytest.deprecated_call(): - myobject.deprecated_method() +This page has been moved, please see :ref:`assertwarnings`. diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst new file mode 100644 index 000000000..ae15e3129 --- /dev/null +++ b/doc/en/warnings.rst @@ -0,0 +1,218 @@ +.. _`warnings`: + +Warnings Capture +================ + +.. versionadded:: 3.1 + +Starting from version ``3.1``, pytest now automatically catches all warnings during test execution +and displays them at the end of the session:: + + # content of test_show_warnings.py + import warnings + + def deprecated_function(): + warnings.warn("this function is deprecated, use another_function()", DeprecationWarning) + return 1 + + def test_one(): + assert deprecated_function() == 1 + +Running pytest now produces this output:: + + $ pytest test_show_warnings.py + . + ============================== warnings summary =============================== + test_show_warning.py::test_one + C:\pytest\.tmp\test_show_warning.py:4: DeprecationWarning: this function is deprecated, use another_function() + warnings.warn("this function is deprecated, use another_function()", DeprecationWarning) + + -- Docs: http://doc.pytest.org/en/latest/warnings.html + 1 passed, 1 warnings in 0.01 seconds + +The ``-W`` flag can be passed to control which warnings will be displayed or even turn +them into errors:: + + $ pytest -q test_show_warning.py -W error::DeprecationWarning + F + ================================== FAILURES =================================== + __________________________________ test_one ___________________________________ + + def test_one(): + > assert deprecated_function() == 1 + + test_show_warning.py:8: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + def deprecated_function(): + > warnings.warn("this function is deprecated, use another_function()", DeprecationWarning) + E DeprecationWarning: this function is deprecated, use another_function() + + test_show_warning.py:4: DeprecationWarning + 1 failed in 0.02 seconds + +The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option. +For example, the configuration below will ignore all deprecation warnings, but will transform +all other warnings into errors. + +.. code-block:: ini + + [pytest] + filterwarnings = + error + ignore::DeprecationWarning + + +When a warning matches more than one option in the list, the action for the last matching option +is performed. + +Both ``-W`` command-line option and ``filterwarnings`` ini option are based on Python's own +`-W option`_ and `warnings.simplefilter`_, so please refer to those sections in the Python +documentation for other examples and advanced usage. + +*Credits go to Florian Schulze for the reference implementation in the* `pytest-warnings`_ +*plugin.* + +.. _`-W option`: https://docs.python.org/3/using/cmdline.html?highlight=#cmdoption-W +.. _warnings.simplefilter: https://docs.python.org/3/library/warnings.html#warnings.simplefilter +.. _`pytest-warnings`: https://github.com/fschulze/pytest-warnings + +.. _`asserting warnings`: + +.. _assertwarnings: + +.. _`asserting warnings with the warns function`: + +.. _warns: + +Asserting warnings with the warns function +----------------------------------------------- + +.. versionadded:: 2.8 + +You can check that code raises a particular warning using ``pytest.warns``, +which works in a similar manner to :ref:`raises `:: + + import warnings + import pytest + + def test_warning(): + with pytest.warns(UserWarning): + warnings.warn("my warning", UserWarning) + +The test will fail if the warning in question is not raised. + +You can also call ``pytest.warns`` on a function or code string:: + + pytest.warns(expected_warning, func, *args, **kwargs) + pytest.warns(expected_warning, "func(*args, **kwargs)") + +The function also returns a list of all raised warnings (as +``warnings.WarningMessage`` objects), which you can query for +additional information:: + + with pytest.warns(RuntimeWarning) as record: + warnings.warn("another warning", RuntimeWarning) + + # check that only one warning was raised + assert len(record) == 1 + # check that the message matches + assert record[0].message.args[0] == "another warning" + +Alternatively, you can examine raised warnings in detail using the +:ref:`recwarn ` fixture (see below). + +.. note:: + ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated + differently; see :ref:`ensuring_function_triggers`. + +.. _`recording warnings`: + +.. _recwarn: + +Recording warnings +------------------------ + +You can record raised warnings either using ``pytest.warns`` or with +the ``recwarn`` fixture. + +To record with ``pytest.warns`` without asserting anything about the warnings, +pass ``None`` as the expected warning type:: + + with pytest.warns(None) as record: + warnings.warn("user", UserWarning) + warnings.warn("runtime", RuntimeWarning) + + assert len(record) == 2 + assert str(record[0].message) == "user" + assert str(record[1].message) == "runtime" + +The ``recwarn`` fixture will record warnings for the whole function:: + + import warnings + + def test_hello(recwarn): + warnings.warn("hello", UserWarning) + assert len(recwarn) == 1 + w = recwarn.pop(UserWarning) + assert issubclass(w.category, UserWarning) + assert str(w.message) == "hello" + assert w.filename + assert w.lineno + +Both ``recwarn`` and ``pytest.warns`` return the same interface for recorded +warnings: a WarningsRecorder instance. To view the recorded warnings, you can +iterate over this instance, call ``len`` on it to get the number of recorded +warnings, or index into it to get a particular recorded warning. It also +provides these methods: + +.. autoclass:: _pytest.recwarn.WarningsRecorder() + :members: + +Each recorded warning has the attributes ``message``, ``category``, +``filename``, ``lineno``, ``file``, and ``line``. The ``category`` is the +class of the warning. The ``message`` is the warning itself; calling +``str(message)`` will return the actual message of the warning. + +.. note:: + :class:`RecordedWarning` was changed from a plain class to a namedtuple in pytest 3.1 + +.. note:: + ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated + differently; see :ref:`ensuring_function_triggers`. + +.. _`ensuring a function triggers a deprecation warning`: + +.. _ensuring_function_triggers: + +Ensuring a function triggers a deprecation warning +------------------------------------------------------- + +You can also call a global helper for checking +that a certain function call triggers a ``DeprecationWarning`` or +``PendingDeprecationWarning``:: + + import pytest + + def test_global(): + pytest.deprecated_call(myfunction, 17) + +By default, ``DeprecationWarning`` and ``PendingDeprecationWarning`` will not be +caught when using ``pytest.warns`` or ``recwarn`` because default Python warnings filters hide +them. If you wish to record them in your own code, use the +command ``warnings.simplefilter('always')``:: + + import warnings + import pytest + + def test_deprecation(recwarn): + warnings.simplefilter('always') + warnings.warn("deprecated", DeprecationWarning) + assert len(recwarn) == 1 + assert recwarn.pop(DeprecationWarning) + +You can also use it as a contextmanager:: + + def test_global(): + with pytest.deprecated_call(): + myobject.deprecated_method() diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index debda79ca..fe02d82f0 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -339,10 +339,16 @@ class TestGeneralUsage(object): "*ERROR*test_b.py::b*", ]) + @pytest.mark.usefixtures('recwarn') def test_namespace_import_doesnt_confuse_import_hook(self, testdir): - # Ref #383. Python 3.3's namespace package messed with our import hooks - # Importing a module that didn't exist, even if the ImportError was - # gracefully handled, would make our test crash. + """ + Ref #383. Python 3.3's namespace package messed with our import hooks + Importing a module that didn't exist, even if the ImportError was + gracefully handled, would make our test crash. + + Use recwarn here to silence this warning in Python 2.6 and 2.7: + ImportWarning: Not importing directory '...\not_a_package': missing __init__.py + """ testdir.mkdir('not_a_package') p = testdir.makepyfile(""" try: @@ -524,6 +530,7 @@ class TestInvocationVariants(object): ]) def test_cmdline_python_package(self, testdir, monkeypatch): + import warnings monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', False) path = testdir.mkpydir("tpkg") path.join("test_hello.py").write("def test_hello(): pass") @@ -546,7 +553,11 @@ class TestInvocationVariants(object): return what empty_package = testdir.mkpydir("empty_package") monkeypatch.setenv('PYTHONPATH', join_pythonpath(empty_package)) - result = testdir.runpytest("--pyargs", ".") + # the path which is not a package raises a warning on pypy; + # no idea why only pypy and not normal python warn about it here + with warnings.catch_warnings(): + warnings.simplefilter('ignore', ImportWarning) + result = testdir.runpytest("--pyargs", ".") assert result.ret == 0 result.stdout.fnmatch_lines([ "*2 passed*" diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index ad2f2021d..0c41a71bf 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -27,7 +27,7 @@ def test_funcarg_prefix_deprecation(testdir): """) result = testdir.runpytest('-ra') result.stdout.fnmatch_lines([ - ('WC1 None 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 e67b6bc84..30beb5715 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(""" @@ -1243,8 +1243,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/python/fixture.py b/testing/python/fixture.py index 269d2c68a..4c9ad7a91 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -545,22 +545,33 @@ class TestRequestBasic(object): return l.pop() def test_func(something): pass """) + import contextlib + if getfixmethod == 'getfuncargvalue': + warning_expectation = pytest.warns(DeprecationWarning) + else: + # see #1830 for a cleaner way to accomplish this + @contextlib.contextmanager + def expecting_no_warning(): yield + + warning_expectation = expecting_no_warning() + req = item._request - fixture_fetcher = getattr(req, getfixmethod) - pytest.raises(FixtureLookupError, fixture_fetcher, "notexists") - val = fixture_fetcher("something") - assert val == 1 - val = fixture_fetcher("something") - assert val == 1 - val2 = fixture_fetcher("other") - assert val2 == 2 - val2 = fixture_fetcher("other") # see about caching - assert val2 == 2 - pytest._fillfuncargs(item) - assert item.funcargs["something"] == 1 - assert len(get_public_names(item.funcargs)) == 2 - assert "request" in item.funcargs - #assert item.funcargs == {'something': 1, "other": 2} + with warning_expectation: + fixture_fetcher = getattr(req, getfixmethod) + with pytest.raises(FixtureLookupError): + fixture_fetcher("notexists") + val = fixture_fetcher("something") + assert val == 1 + val = fixture_fetcher("something") + assert val == 1 + val2 = fixture_fetcher("other") + assert val2 == 2 + val2 = fixture_fetcher("other") # see about caching + assert val2 == 2 + pytest._fillfuncargs(item) + assert item.funcargs["something"] == 1 + assert len(get_public_names(item.funcargs)) == 2 + assert "request" in item.funcargs def test_request_addfinalizer(self, testdir): item = testdir.getitem(""" diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index c347dc9e2..380dbf0e6 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -347,7 +347,8 @@ class TestMetafunc(object): def test_foo(arg): pass """) - result = testdir.runpytest("--collect-only") + with pytest.warns(DeprecationWarning): + result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines([ "", " ", diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 789352a7f..c385f6aa1 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -976,7 +976,10 @@ def test_assert_tuple_warning(testdir): assert(False, 'you shall not pass') """) result = testdir.runpytest('-rw') - result.stdout.fnmatch_lines('WR1*: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_cache.py b/testing/test_cache.py index 47072e07e..f5904be39 100755 --- a/testing/test_cache.py +++ b/testing/test_cache.py @@ -55,7 +55,7 @@ class TestNewAPI(object): assert result.ret == 1 result.stdout.fnmatch_lines([ "*could not create cache path*", - "*1 pytest-warnings*", + "*1 warnings*", ]) def test_config_cache(self, testdir): diff --git a/testing/test_config.py b/testing/test_config.py index 94ad2b130..0d8e6abfc 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -141,7 +141,7 @@ class TestConfigAPI(object): from __future__ import unicode_literals def pytest_addoption(parser): - parser.addoption('--hello', type='string') + parser.addoption('--hello', type=str) """) config = testdir.parseconfig('--hello=this') assert config.getoption('hello') == 'this' @@ -633,13 +633,14 @@ class TestWarning(object): pass """) result = testdir.runpytest("--disable-pytest-warnings") - assert result.parseoutcomes()["pytest-warnings"] > 0 + assert result.parseoutcomes()["warnings"] > 0 assert "hello" not in result.stdout.str() result = testdir.runpytest() result.stdout.fnmatch_lines(""" - ===*pytest-warning summary*=== - *WT1*test_warn_on_test_item*:7 hello* + ===*warnings summary*=== + *test_warn_on_test_item_from_request.py::test_hello* + *hello* """) class TestRootdir(object): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 3d07e54e5..7003d9f5f 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -852,7 +852,10 @@ def test_record_property(testdir): pnodes = psnode.find_by_tag('property') pnodes[0].assert_attr(name="bar", value="1") pnodes[1].assert_attr(name="foo", value="<1") - result.stdout.fnmatch_lines('*C3*test_record_property.py*experimental*') + result.stdout.fnmatch_lines([ + 'test_record_property.py::test_record', + '*record_xml_property*experimental*', + ]) def test_record_property_same_name(testdir): diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index f990e8b04..38542783a 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -34,15 +34,16 @@ class TestParser(object): ) def test_argument_type(self): - argument = parseopt.Argument('-t', dest='abc', type='int') + argument = parseopt.Argument('-t', dest='abc', type=int) assert argument.type is int - argument = parseopt.Argument('-t', dest='abc', type='string') + argument = parseopt.Argument('-t', dest='abc', type=str) assert argument.type is str argument = parseopt.Argument('-t', dest='abc', type=float) assert argument.type is float - with pytest.raises(KeyError): - argument = parseopt.Argument('-t', dest='abc', type='choice') - argument = parseopt.Argument('-t', dest='abc', type='choice', + with pytest.warns(DeprecationWarning): + with pytest.raises(KeyError): + argument = parseopt.Argument('-t', dest='abc', type='choice') + argument = parseopt.Argument('-t', dest='abc', type=str, choices=['red', 'blue']) assert argument.type is str @@ -176,8 +177,8 @@ class TestParser(object): elif option.type is str: option.default = "world" parser = parseopt.Parser(processopt=defaultget) - parser.addoption("--this", dest="this", type="int", action="store") - parser.addoption("--hello", dest="hello", type="string", action="store") + parser.addoption("--this", dest="this", type=int, action="store") + parser.addoption("--hello", dest="hello", type=str, action="store") parser.addoption("--no", dest="no", action="store_true") option = parser.parse([]) assert option.hello == "world" diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index ec8389746..1f0f4f602 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -285,8 +285,8 @@ class TestPytestPluginManager(object): result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True) assert result.ret == EXIT_NOTESTSCOLLECTED result.stdout.fnmatch_lines([ - "WI1*skipped plugin*skipping1*hello*", - "WI1*skipped plugin*skipping2*hello*", + "*skipped plugin*skipping1*hello*", + "*skipped plugin*skipping2*hello*", ]) def test_consider_env_plugin_instantiation(self, testdir, monkeypatch, pytestpm): diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 890d2ce0a..0f921f057 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -146,7 +146,9 @@ class TestDeprecatedCall(object): pytest.deprecated_call(deprecated_function) """) result = testdir.runpytest() - result.stdout.fnmatch_lines('*=== 2 passed in *===') + # the 2 tests must pass, but the call to test_one() will generate a warning + # in pytest's summary + result.stdout.fnmatch_lines('*=== 2 passed, 1 warnings in *===') class TestWarns(object): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 9d7a2d7d2..5a90b3dd4 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -616,7 +616,7 @@ def test_getreportopt(): class config(object): class option(object): reportchars = "" - disablepytestwarnings = True + disable_warnings = True config.option.reportchars = "sf" assert getreportopt(config) == "sf" @@ -625,11 +625,11 @@ def test_getreportopt(): assert getreportopt(config) == "sfx" config.option.reportchars = "sfx" - config.option.disablepytestwarnings = False + config.option.disable_warnings = False assert getreportopt(config) == "sfxw" config.option.reportchars = "sfxw" - config.option.disablepytestwarnings = False + config.option.disable_warnings = False assert getreportopt(config) == "sfxw" @@ -838,8 +838,8 @@ def test_terminal_summary_warnings_are_displayed(testdir): """) result = testdir.runpytest('-rw') result.stdout.fnmatch_lines([ - '*C1*internal warning', - '*== 1 pytest-warnings in *', + '*internal warning', + '*== 1 warnings in *', ]) @@ -859,9 +859,9 @@ def test_terminal_summary_warnings_are_displayed(testdir): ("yellow", "1 weird", {"weird": (1,)}), ("yellow", "1 passed, 1 weird", {"weird": (1,), "passed": (1,)}), - ("yellow", "1 pytest-warnings", {"warnings": (1,)}), - ("yellow", "1 passed, 1 pytest-warnings", {"warnings": (1,), - "passed": (1,)}), + ("yellow", "1 warnings", {"warnings": (1,)}), + ("yellow", "1 passed, 1 warnings", {"warnings": (1,), + "passed": (1,)}), ("green", "5 passed", {"passed": (1,2,3,4,5)}), diff --git a/testing/test_warnings.py b/testing/test_warnings.py new file mode 100644 index 000000000..e0baed8d1 --- /dev/null +++ b/testing/test_warnings.py @@ -0,0 +1,108 @@ +import pytest + + +WARNINGS_SUMMARY_HEADER = 'warnings summary' + +@pytest.fixture +def pyfile_with_warnings(testdir, request): + """ + Create a test file which calls a function in a module which generates warnings. + """ + testdir.syspathinsert() + test_name = request.function.__name__ + module_name = test_name.lstrip('test_') + '_module' + testdir.makepyfile(**{ + module_name: ''' + import warnings + def foo(): + warnings.warn(PendingDeprecationWarning("functionality is pending deprecation")) + warnings.warn(DeprecationWarning("functionality is deprecated")) + return 1 + ''', + test_name: ''' + import {module_name} + def test_func(): + assert {module_name}.foo() == 1 + '''.format(module_name=module_name) + }) + + +def test_normal_flow(testdir, pyfile_with_warnings): + """ + Check that the warnings section is displayed, containing test node ids followed by + all warnings generated by that test node. + """ + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*== %s ==*' % WARNINGS_SUMMARY_HEADER, + + '*test_normal_flow.py::test_func', + + '*normal_flow_module.py:3: 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"))', + '* 1 passed, 2 warnings*', + ]) + assert result.stdout.str().count('test_normal_flow.py::test_func') == 1 + + +def test_setup_teardown_warnings(testdir, pyfile_with_warnings): + testdir.makepyfile(''' + import warnings + import pytest + + @pytest.fixture + def fix(): + warnings.warn(UserWarning("warning during setup")) + yield + warnings.warn(UserWarning("warning during teardown")) + + def test_func(fix): + pass + ''') + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*== %s ==*' % WARNINGS_SUMMARY_HEADER, + + '*test_setup_teardown_warnings.py:6: 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"))', + '* 1 passed, 2 warnings*', + ]) + + +@pytest.mark.parametrize('method', ['cmdline', 'ini']) +def test_as_errors(testdir, pyfile_with_warnings, method): + args = ('-W', 'error') if method == 'cmdline' else () + if method == 'ini': + testdir.makeini(''' + [pytest] + filterwarnings= error + ''') + result = testdir.runpytest(*args) + result.stdout.fnmatch_lines([ + 'E PendingDeprecationWarning: functionality is pending deprecation', + 'as_errors_module.py:3: PendingDeprecationWarning', + '* 1 failed in *', + ]) + + +@pytest.mark.parametrize('method', ['cmdline', 'ini']) +def test_ignore(testdir, pyfile_with_warnings, method): + args = ('-W', 'ignore') if method == 'cmdline' else () + if method == 'ini': + testdir.makeini(''' + [pytest] + filterwarnings= ignore + ''') + + result = testdir.runpytest(*args) + result.stdout.fnmatch_lines([ + '* 1 passed in *', + ]) + assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() + diff --git a/tox.ini b/tox.ini index 1b9fb9f5a..5c103f94c 100644 --- a/tox.ini +++ b/tox.ini @@ -176,7 +176,15 @@ python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance python_functions=test norecursedirs = .tox ja .hg cx_freeze_source - +filterwarnings= error + # produced by path.local + ignore:bad escape.*:DeprecationWarning:re + # produced by path.readlines + ignore:.*U.*mode is deprecated:DeprecationWarning + # produced by pytest-xdist + ignore:.*type argument to addoption.*:DeprecationWarning + # produced by python >=3.5 on execnet (pytest-xdist) + ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning [flake8] ignore =E401,E225,E261,E128,E124,E301,E302,E121,E303,W391,E501,E231,E126,E701,E265,E241,E251,E226,E101,W191,E131,E203,E122,E123,E271,E712,E222,E127,E125,E221,W292,E111,E113,E293,E262,W293,E129,E702,E201,E272,E202,E704,E731,E402