diff --git a/.travis.yml b/.travis.yml index 1c055e663..6489a1647 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,11 @@ env: - TOXENV=py37-pluggymaster PYTEST_NO_COVERAGE=1 - TOXENV=py37-freeze PYTEST_NO_COVERAGE=1 +matrix: + allow_failures: + - python: '3.8-dev' + env: TOXENV=py38 + jobs: include: # Coverage tracking is slow with pypy, skip it. @@ -35,6 +40,8 @@ jobs: python: '3.5' - env: TOXENV=py36 python: '3.6' + - env: TOXENV=py38 + python: '3.8-dev' - env: TOXENV=py37 - &test-macos language: generic diff --git a/AUTHORS b/AUTHORS index 35e07dcb4..6bc084d88 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,6 +27,7 @@ Anthony Shaw Anthony Sottile Anton Lodder Antony Lee +Arel Cordero Armin Rigo Aron Coyle Aron Curzon @@ -173,6 +174,7 @@ Nathaniel Waisbrot Ned Batchelder Neven Mundar Nicholas Devenish +Nicholas Murphy Niclas Olofsson Nicolas Delaby Oleg Pidsadnyi diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 76c947473..ac168dfd3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,7 +24,7 @@ pytest 4.2.0 (2019-01-30) Features -------- -- `#3094 `_: `Class xunit-style `__ functions and methods +- `#3094 `_: `Classic xunit-style `__ functions and methods now obey the scope of *autouse* fixtures. This fixes a number of surprising issues like ``setup_method`` being called before session-scoped @@ -96,6 +96,9 @@ Trivial/Internal Changes - `#4657 `_: Copy saferepr from pylib +- `#4668 `_: The verbose word for expected failures in the teststatus report changes from ``xfail`` to ``XFAIL`` to be consistent with other test outcomes. + + pytest 4.1.1 (2019-01-12) ========================= diff --git a/changelog/2895.bugfix.rst b/changelog/2895.bugfix.rst new file mode 100644 index 000000000..8e01e193c --- /dev/null +++ b/changelog/2895.bugfix.rst @@ -0,0 +1 @@ +The ``pytest_report_collectionfinish`` hook now is also called with ``--collect-only``. diff --git a/changelog/3899.bugfix.rst b/changelog/3899.bugfix.rst new file mode 100644 index 000000000..8f117779e --- /dev/null +++ b/changelog/3899.bugfix.rst @@ -0,0 +1 @@ +Do not raise ``UsageError`` when an imported package has a ``pytest_plugins.py`` child module. diff --git a/changelog/3899.doc.rst b/changelog/3899.doc.rst new file mode 100644 index 000000000..675684a01 --- /dev/null +++ b/changelog/3899.doc.rst @@ -0,0 +1 @@ +Add note to ``plugins.rst`` that ``pytest_plugins`` should not be used as a name for a user module containing plugins. diff --git a/changelog/4324.doc.rst b/changelog/4324.doc.rst new file mode 100644 index 000000000..5e37a91aa --- /dev/null +++ b/changelog/4324.doc.rst @@ -0,0 +1 @@ +Document how to use ``raises`` and ``does_not_raise`` to write parametrized tests with conditional raises. diff --git a/changelog/4592.bugfix.rst b/changelog/4592.bugfix.rst new file mode 100644 index 000000000..f1eaae7eb --- /dev/null +++ b/changelog/4592.bugfix.rst @@ -0,0 +1 @@ +Fix handling of ``collect_ignore`` via parent ``conftest.py``. diff --git a/changelog/4700.bugfix.rst b/changelog/4700.bugfix.rst new file mode 100644 index 000000000..3f8acb876 --- /dev/null +++ b/changelog/4700.bugfix.rst @@ -0,0 +1,2 @@ +Fix regression where ``setUpClass`` would always be called in subclasses even if all tests +were skipped by a ``unittest.skip()`` decorator applied in the subclass. diff --git a/changelog/4709.doc.rst b/changelog/4709.doc.rst new file mode 100644 index 000000000..5f21728f6 --- /dev/null +++ b/changelog/4709.doc.rst @@ -0,0 +1,2 @@ +Document how to customize test failure messages when using +``pytest.warns``. diff --git a/changelog/4739.bugfix.rst b/changelog/4739.bugfix.rst new file mode 100644 index 000000000..dcd44d3fa --- /dev/null +++ b/changelog/4739.bugfix.rst @@ -0,0 +1 @@ +Fix ``parametrize(... ids=)`` when the function returns non-strings. diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 787bbe10c..b5d4693ad 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -565,3 +565,50 @@ As the result: - The test ``test_eval[1+7-8]`` passed, but the name is autogenerated and confusing. - The test ``test_eval[basic_2+4]`` passed. - The test ``test_eval[basic_6*9]`` was expected to fail and did fail. + +.. _`parametrizing_conditional_raising`: + +Parametrizing conditional raising +-------------------------------------------------------------------- + +Use :func:`pytest.raises` with the +:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests +in which some tests raise exceptions and others do not. + +It is helpful to define a no-op context manager ``does_not_raise`` to serve +as a complement to ``raises``. For example:: + + from contextlib import contextmanager + import pytest + + @contextmanager + def does_not_raise(): + yield + + + @pytest.mark.parametrize('example_input,expectation', [ + (3, does_not_raise()), + (2, does_not_raise()), + (1, does_not_raise()), + (0, pytest.raises(ZeroDivisionError)), + ]) + def test_division(example_input, expectation): + """Test how much I know division.""" + with expectation: + assert (6 / example_input) is not None + +In the example above, the first three test cases should run unexceptionally, +while the fourth should raise ``ZeroDivisionError``. + +If you're only supporting Python 3.7+, you can simply use ``nullcontext`` +to define ``does_not_raise``:: + + from contextlib import nullcontext as does_not_raise + +Or, if you're supporting Python 3.3+ you can use:: + + from contextlib import ExitStack as does_not_raise + +Or, if desired, you can ``pip install contextlib2`` and use:: + + from contextlib2 import ExitStack as does_not_raise diff --git a/doc/en/plugins.rst b/doc/en/plugins.rst index 3513e79fb..7c7e1132d 100644 --- a/doc/en/plugins.rst +++ b/doc/en/plugins.rst @@ -84,6 +84,11 @@ will be loaded as well. :ref:`full explanation ` in the Writing plugins section. +.. note:: + The name ``pytest_plugins`` is reserved and should not be used as a + name for a custom plugin module. + + .. _`findpluginname`: Finding out which plugins are active diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 8f0244aea..11f73f43e 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -233,7 +233,7 @@ You can also use it as a contextmanager:: .. _warns: Asserting warnings with the warns function ------------------------------------------------ +------------------------------------------ .. versionadded:: 2.8 @@ -291,7 +291,7 @@ Alternatively, you can examine raised warnings in detail using the .. _recwarn: Recording warnings ------------------------- +------------------ You can record raised warnings either using ``pytest.warns`` or with the ``recwarn`` fixture. @@ -329,6 +329,26 @@ warnings, or index into it to get a particular recorded warning. Full API: :class:`WarningsRecorder`. +.. _custom_failure_messages: + +Custom failure messages +----------------------- + +Recording warnings provides an opportunity to produce custom test +failure messages for when no warnings are issued or other conditions +are met. + +.. code-block:: python + + def test(): + with pytest.warns(Warning) as record: + f() + if not record: + pytest.fail("Expected a warning!") + +If no warnings are issued when calling ``f``, then ``not record`` will +evaluate to ``True``. You can then call ``pytest.fail`` with a +custom error message. .. _internal-warnings: diff --git a/pyproject.toml b/pyproject.toml index d17b936c1..2a4cd65c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ requires = [ "setuptools-scm", "wheel", ] +build-backend = "setuptools.build_meta" [tool.towncrier] package = "pytest" diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 23e09ff40..26999e125 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -559,8 +559,8 @@ def _get_plugin_specs_as_list(specs): which case it is returned as a list. Specs can also be `None` in which case an empty list is returned. """ - if specs is not None: - if isinstance(specs, str): + if specs is not None and not isinstance(specs, types.ModuleType): + if isinstance(specs, six.string_types): specs = specs.split(",") if specs else [] if not isinstance(specs, (list, tuple)): raise UsageError( diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 0c47b8b51..ba0acd269 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -370,6 +370,8 @@ def get_actual_log_level(config, *setting_names): ) +# run after terminalreporter/capturemanager are configured +@pytest.hookimpl(trylast=True) def pytest_configure(config): config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") @@ -388,8 +390,6 @@ class LoggingPlugin(object): # enable verbose output automatically if live logging is enabled if self._log_cli_enabled() and not config.getoption("verbose"): - # sanity check: terminal reporter should not have been loaded at this point - assert self._config.pluginmanager.get_plugin("terminalreporter") is None config.option.verbose = 1 self.print_logs = get_option_ini(config, "log_print") @@ -420,6 +420,54 @@ class LoggingPlugin(object): self.log_cli_handler = None + self.live_logs_context = lambda: dummy_context_manager() + # Note that the lambda for the live_logs_context is needed because + # live_logs_context can otherwise not be entered multiple times due + # to limitations of contextlib.contextmanager. + + if self._log_cli_enabled(): + self._setup_cli_logging() + + def _setup_cli_logging(self): + config = self._config + terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") + if terminal_reporter is None: + # terminal reporter is disabled e.g. by pytest-xdist. + return + + # FIXME don't set verbosity level and derived attributes of + # terminalwriter directly + terminal_reporter.verbosity = config.option.verbose + terminal_reporter.showheader = terminal_reporter.verbosity >= 0 + terminal_reporter.showfspath = terminal_reporter.verbosity >= 0 + terminal_reporter.showlongtestinfo = terminal_reporter.verbosity > 0 + + capture_manager = config.pluginmanager.get_plugin("capturemanager") + # if capturemanager plugin is disabled, live logging still works. + log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) + log_cli_format = get_option_ini(config, "log_cli_format", "log_format") + log_cli_date_format = get_option_ini( + config, "log_cli_date_format", "log_date_format" + ) + if ( + config.option.color != "no" + and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format) + ): + log_cli_formatter = ColoredLevelFormatter( + create_terminal_writer(config), + log_cli_format, + datefmt=log_cli_date_format, + ) + else: + log_cli_formatter = logging.Formatter( + log_cli_format, datefmt=log_cli_date_format + ) + log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level") + self.log_cli_handler = log_cli_handler + self.live_logs_context = lambda: catching_logs( + log_cli_handler, formatter=log_cli_formatter, level=log_cli_level + ) + def _log_cli_enabled(self): """Return True if log_cli should be considered enabled, either explicitly or because --log-cli-level was given in the command-line. @@ -430,10 +478,6 @@ class LoggingPlugin(object): @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(self): - # This has to be called before the first log message is logged, - # so we can access the terminal reporter plugin. - self._setup_cli_logging() - with self.live_logs_context(): if self.log_cli_handler: self.log_cli_handler.set_when("collection") @@ -513,7 +557,6 @@ class LoggingPlugin(object): @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionstart(self): - self._setup_cli_logging() with self.live_logs_context(): if self.log_cli_handler: self.log_cli_handler.set_when("sessionstart") @@ -533,46 +576,6 @@ class LoggingPlugin(object): else: yield # run all the tests - def _setup_cli_logging(self): - """Sets up the handler and logger for the Live Logs feature, if enabled.""" - terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") - if self._log_cli_enabled() and terminal_reporter is not None: - capture_manager = self._config.pluginmanager.get_plugin("capturemanager") - log_cli_handler = _LiveLoggingStreamHandler( - terminal_reporter, capture_manager - ) - log_cli_format = get_option_ini( - self._config, "log_cli_format", "log_format" - ) - log_cli_date_format = get_option_ini( - self._config, "log_cli_date_format", "log_date_format" - ) - if ( - self._config.option.color != "no" - and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format) - ): - log_cli_formatter = ColoredLevelFormatter( - create_terminal_writer(self._config), - log_cli_format, - datefmt=log_cli_date_format, - ) - else: - log_cli_formatter = logging.Formatter( - log_cli_format, datefmt=log_cli_date_format - ) - log_cli_level = get_actual_log_level( - self._config, "log_cli_level", "log_level" - ) - self.log_cli_handler = log_cli_handler - self.live_logs_context = lambda: catching_logs( - log_cli_handler, formatter=log_cli_formatter, level=log_cli_level - ) - else: - self.live_logs_context = lambda: dummy_context_manager() - # Note that the lambda for the live_logs_context is needed because - # live_logs_context can otherwise not be entered multiple times due - # to limitations of contextlib.contextmanager - class _LiveLoggingStreamHandler(logging.StreamHandler): """ diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d0d826bb6..68d5bac40 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -607,6 +607,7 @@ class Session(nodes.FSCollector): yield y def _collectfile(self, path, handle_dupes=True): + assert path.isfile() ihook = self.gethookproxy(path) if not self.isinitpath(path): if ihook.pytest_ignore_collect(path=path, config=self.config): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 85373f47c..48962d137 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -599,6 +599,7 @@ class Package(Module): return proxy def _collectfile(self, path, handle_dupes=True): + assert path.isfile() ihook = self.gethookproxy(path) if not self.isinitpath(path): if ihook.pytest_ignore_collect(path=path, config=self.config): @@ -642,11 +643,12 @@ class Package(Module): ): continue - if path.isdir() and path.join("__init__.py").check(file=1): - pkg_prefixes.add(path) - - for x in self._collectfile(path): - yield x + if path.isdir(): + if path.join("__init__.py").check(file=1): + pkg_prefixes.add(path) + else: + for x in self._collectfile(path): + yield x def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): @@ -1144,9 +1146,10 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): def _idval(val, argname, idx, idfn, item, config): if idfn: - s = None try: - s = idfn(val) + generated_id = idfn(val) + if generated_id is not None: + val = generated_id except Exception as e: # See issue https://github.com/pytest-dev/pytest/issues/2169 msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n" @@ -1154,10 +1157,7 @@ def _idval(val, argname, idx, idfn, item, config): # we only append the exception type and message because on Python 2 reraise does nothing msg += " {}: {}\n".format(type(e).__name__, e) six.raise_from(ValueError(msg), e) - if s: - return ascii_escaped(s) - - if config: + elif config: hook_id = config.hook.pytest_make_parametrize_id( config=config, val=val, argname=argname ) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 9b31d4e68..1b643d430 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -621,6 +621,14 @@ def raises(expected_exception, *args, **kwargs): ... >>> assert exc_info.type is ValueError + **Using with** ``pytest.mark.parametrize`` + + When using :ref:`pytest.mark.parametrize ref` + it is possible to parametrize tests such that + some runs raise an exception and others do not. + + See :ref:`parametrizing_conditional_raising` for an example. + **Legacy form** It is possible to specify a callable by passing a to-be-called lambda:: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 03b0761f2..eb35577f1 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -574,19 +574,20 @@ class TerminalReporter(object): return lines def pytest_collection_finish(self, session): - if self.config.option.collectonly: + if self.config.getoption("collectonly"): self._printcollecteditems(session.items) - if self.stats.get("failed"): - self._tw.sep("!", "collection failures") - for rep in self.stats.get("failed"): - rep.toterminal(self._tw) - return 1 - return 0 + lines = self.config.hook.pytest_report_collectionfinish( config=self.config, startdir=self.startdir, items=session.items ) self._write_report_lines_from_hooks(lines) + if self.config.getoption("collectonly"): + if self.stats.get("failed"): + self._tw.sep("!", "collection failures") + for rep in self.stats.get("failed"): + rep.toterminal(self._tw) + def _printcollecteditems(self, items): # to print out items and their parent collectors # we take care to leave out Instances aka () diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index e00636d46..58d79845b 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -87,6 +87,9 @@ def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self): @pytest.fixture(scope=scope, autouse=True) def fixture(self, request): + if getattr(self, "__unittest_skip__", None): + reason = self.__unittest_skip_why__ + pytest.skip(reason) if setup is not None: if pass_self: setup(self, request.function) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c8a391b78..95c419599 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -969,6 +969,20 @@ def test_import_plugin_unicode_name(testdir): assert r.ret == 0 +def test_pytest_plugins_as_module(testdir): + """Do not raise an error if pytest_plugins attribute is a module (#3899)""" + testdir.makepyfile( + **{ + "__init__.py": "", + "pytest_plugins.py": "", + "conftest.py": "from . import pytest_plugins", + "test_foo.py": "def test(): pass", + } + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines("* 1 passed in *") + + def test_deferred_hook_checking(testdir): """ Check hooks as late as possible (#1821). diff --git a/testing/example_scripts/unittest/test_setup_skip.py b/testing/example_scripts/unittest/test_setup_skip.py new file mode 100644 index 000000000..93f79bb3b --- /dev/null +++ b/testing/example_scripts/unittest/test_setup_skip.py @@ -0,0 +1,13 @@ +"""Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class.""" +import unittest + + +class Base(unittest.TestCase): + def setUp(self): + assert 0 + + +@unittest.skip("skip all tests") +class Test(Base): + def test_foo(self): + assert 0 diff --git a/testing/example_scripts/unittest/test_setup_skip_class.py b/testing/example_scripts/unittest/test_setup_skip_class.py new file mode 100644 index 000000000..4f251dcba --- /dev/null +++ b/testing/example_scripts/unittest/test_setup_skip_class.py @@ -0,0 +1,14 @@ +"""Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class.""" +import unittest + + +class Base(unittest.TestCase): + @classmethod + def setUpClass(cls): + assert 0 + + +@unittest.skip("skip all tests") +class Test(Base): + def test_foo(self): + assert 0 diff --git a/testing/example_scripts/unittest/test_setup_skip_module.py b/testing/example_scripts/unittest/test_setup_skip_module.py new file mode 100644 index 000000000..98befbe51 --- /dev/null +++ b/testing/example_scripts/unittest/test_setup_skip_module.py @@ -0,0 +1,12 @@ +"""setUpModule is always called, even if all tests in the module are skipped""" +import unittest + + +def setUpModule(): + assert 0 + + +@unittest.skip("skip all tests") +class Base(unittest.TestCase): + def test(self): + assert 0 diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 5c352efd1..fa22966d8 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -418,6 +418,21 @@ class TestMetafunc(object): ] ) + def test_parametrize_ids_returns_non_string(self, testdir): + testdir.makepyfile( + """\ + import pytest + + def ids(d): + return d + + @pytest.mark.parametrize("arg", ({1: 2}, {3, 4}), ids=ids) + def test(arg): + assert arg + """ + ) + assert testdir.runpytest().ret == 0 + def test_idmaker_with_ids(self): from _pytest.python import idmaker diff --git a/testing/python/raises.py b/testing/python/raises.py index 4ff0b51bc..f5827e9b0 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -94,6 +94,54 @@ class TestRaises(object): result = testdir.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) + def test_does_not_raise(self, testdir): + testdir.makepyfile( + """ + from contextlib import contextmanager + import pytest + + @contextmanager + def does_not_raise(): + yield + + @pytest.mark.parametrize('example_input,expectation', [ + (3, does_not_raise()), + (2, does_not_raise()), + (1, does_not_raise()), + (0, pytest.raises(ZeroDivisionError)), + ]) + def test_division(example_input, expectation): + '''Test how much I know division.''' + with expectation: + assert (6 / example_input) is not None + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*4 passed*"]) + + def test_does_not_raise_does_raise(self, testdir): + testdir.makepyfile( + """ + from contextlib import contextmanager + import pytest + + @contextmanager + def does_not_raise(): + yield + + @pytest.mark.parametrize('example_input,expectation', [ + (0, does_not_raise()), + (1, pytest.raises(ZeroDivisionError)), + ]) + def test_division(example_input, expectation): + '''Test how much I know division.''' + with expectation: + assert (6 / example_input) is not None + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*2 failed*"]) + def test_noclass(self): with pytest.raises(TypeError): pytest.raises("wrong", lambda: None) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 840fda2ca..a852277cc 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -68,38 +68,16 @@ def getmsg(f, extra_ns=None, must_pass=False): pytest.fail("function didn't raise at all") -def adjust_body_for_new_docstring_in_module_node(m): - """Module docstrings in 3.8 are part of Module node. - This was briefly in 3.7 as well but got reverted in beta 5. - - It's not in the body so we remove it so the following body items have - the same indexes on all Python versions: - - TODO: - - We have a complicated sys.version_info if in here to ease testing on - various Python 3.7 versions, but we should remove the 3.7 check after - 3.7 is released as stable to make this check more straightforward. - """ - if sys.version_info < (3, 8) and not ( - (3, 7) <= sys.version_info <= (3, 7, 0, "beta", 4) - ): - assert len(m.body) > 1 - assert isinstance(m.body[0], ast.Expr) - assert isinstance(m.body[0].value, ast.Str) - del m.body[0] - - class TestAssertionRewrite(object): def test_place_initial_imports(self): s = """'Doc string'\nother = stuff""" m = rewrite(s) - adjust_body_for_new_docstring_in_module_node(m) - for imp in m.body[0:2]: + assert isinstance(m.body[0], ast.Expr) + for imp in m.body[1:3]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 - assert isinstance(m.body[2], ast.Assign) + assert isinstance(m.body[3], ast.Assign) s = """from __future__ import division\nother_stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.ImportFrom) @@ -110,24 +88,24 @@ class TestAssertionRewrite(object): assert isinstance(m.body[3], ast.Expr) s = """'doc string'\nfrom __future__ import division""" m = rewrite(s) - adjust_body_for_new_docstring_in_module_node(m) - assert isinstance(m.body[0], ast.ImportFrom) - for imp in m.body[1:3]: + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[1], ast.ImportFrom) + for imp in m.body[2:4]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 s = """'doc string'\nfrom __future__ import division\nother""" m = rewrite(s) - adjust_body_for_new_docstring_in_module_node(m) - assert isinstance(m.body[0], ast.ImportFrom) - for imp in m.body[1:3]: + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[1], ast.ImportFrom) + for imp in m.body[2:4]: assert isinstance(imp, ast.Import) assert imp.lineno == 3 assert imp.col_offset == 0 - assert isinstance(m.body[3], ast.Expr) + assert isinstance(m.body[4], ast.Expr) s = """from . import relative\nother_stuff""" m = rewrite(s) - for imp in m.body[0:2]: + for imp in m.body[:2]: assert isinstance(imp, ast.Import) assert imp.lineno == 1 assert imp.col_offset == 0 @@ -136,9 +114,8 @@ class TestAssertionRewrite(object): def test_dont_rewrite(self): s = """'PYTEST_DONT_REWRITE'\nassert 14""" m = rewrite(s) - adjust_body_for_new_docstring_in_module_node(m) - assert len(m.body) == 1 - assert m.body[0].msg is None + assert len(m.body) == 2 + assert m.body[1].msg is None def test_dont_rewrite_plugin(self, testdir): contents = { diff --git a/testing/test_collection.py b/testing/test_collection.py index 36e8a69ce..329182b0f 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1144,3 +1144,16 @@ def test_collect_symlink_out_of_tree(testdir): ] ) assert result.ret == 0 + + +def test_collectignore_via_conftest(testdir, monkeypatch): + """collect_ignore in parent conftest skips importing child (issue #4592).""" + tests = testdir.mkpydir("tests") + tests.ensure("conftest.py").write("collect_ignore = ['ignore_me']") + + ignore_me = tests.mkdir("ignore_me") + ignore_me.ensure("__init__.py") + ignore_me.ensure("conftest.py").write("assert 0, 'should_not_be_called'") + + result = testdir.runpytest() + assert result.ret == EXIT_NOTESTSCOLLECTED diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 71e49fb42..798e8c16a 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -649,7 +649,10 @@ class TestTerminalFunctional(object): assert "===" not in s assert "passed" not in s - def test_report_collectionfinish_hook(self, testdir): + @pytest.mark.parametrize( + "params", [(), ("--collect-only",)], ids=["no-params", "collect-only"] + ) + def test_report_collectionfinish_hook(self, testdir, params): testdir.makeconftest( """ def pytest_report_collectionfinish(config, startdir, items): @@ -664,7 +667,7 @@ class TestTerminalFunctional(object): pass """ ) - result = testdir.runpytest() + result = testdir.runpytest(*params) result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"]) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 2c60cd271..fe33855fa 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1026,3 +1026,18 @@ def test_error_message_with_parametrized_fixtures(testdir): "*Function type: TestCaseFunction", ] ) + + +@pytest.mark.parametrize( + "test_name, expected_outcome", + [ + ("test_setup_skip.py", "1 skipped"), + ("test_setup_skip_class.py", "1 skipped"), + ("test_setup_skip_module.py", "1 error"), + ], +) +def test_setup_inheritance_skipping(testdir, test_name, expected_outcome): + """Issue #4700""" + testdir.copy_example("unittest/{}".format(test_name)) + result = testdir.runpytest() + result.stdout.fnmatch_lines("* {} in *".format(expected_outcome)) diff --git a/tox.ini b/tox.ini index 7cb430223..6c216fd71 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -minversion = 2.0 +isolated_build = True +minversion = 3.3 distshare = {homedir}/.tox/distshare # make sure to update environment list in travis.yml and appveyor.yml envlist = @@ -9,6 +10,7 @@ envlist = py35 py36 py37 + py38 pypy {py27,py37}-{pexpect,xdist,trial,numpy,pluggymaster} py27-nobyte @@ -165,7 +167,9 @@ commands = [testenv:py37-freeze] changedir = testing/freeze +# Disable PEP 517 with pip, which does not work with PyInstaller currently. deps = + --no-use-pep517 pyinstaller commands = {envpython} create_executable.py