diff --git a/changelog/1495.doc.rst b/changelog/1495.doc.rst new file mode 100644 index 00000000000..ab7231333c4 --- /dev/null +++ b/changelog/1495.doc.rst @@ -0,0 +1 @@ +Document common doctest fixture directory tree structure pitfalls diff --git a/changelog/4265.bugfix.rst b/changelog/4265.bugfix.rst new file mode 100644 index 00000000000..7b40737c3e4 --- /dev/null +++ b/changelog/4265.bugfix.rst @@ -0,0 +1 @@ +Validate arguments from the ``PYTEST_ADDOPTS`` environment variable and the ``addopts`` ini option separately. diff --git a/changelog/4500.bugfix.rst b/changelog/4500.bugfix.rst new file mode 100644 index 00000000000..b84b6b117d5 --- /dev/null +++ b/changelog/4500.bugfix.rst @@ -0,0 +1 @@ +When a fixture yields and a log call is made after the test runs, and, if the test is interrupted, capture attributes are ``None``. diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 52edd4cf2be..125ed3aa704 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -154,6 +154,9 @@ which can then be used in your doctests directly:: """ pass +Note that like the normal ``conftest.py``, the fixtures are discovered in the directory tree conftest is in. +Meaning that if you put your doctest with your source code, the relevant conftest.py needs to be in the same directory tree. +Fixtures will not be discovered in a sibling directory tree! Output format ------------- diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 5d1cd00f4f4..d5c5c573510 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -156,4 +156,4 @@ More details can be found in the `original PR =1.0.4 -# pinning sphinx to 1.4.* due to search issues with rtd: -# https://github.com/rtfd/readthedocs-sphinx-ext/issues/25 -sphinx ==1.4.* +pygments-pytest>=1.1.0 +sphinx>=1.8.2 sphinxcontrib-trio diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 679eb4d16cd..53369094926 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -117,7 +117,10 @@ def stop_global_capturing(self): self._global_capturing = None def resume_global_capture(self): - self._global_capturing.resume_capturing() + # During teardown of the python process, and on rare occasions, capture + # attributes can be `None` while trying to resume global capture. + if self._global_capturing is not None: + self._global_capturing.resume_capturing() def suspend_global_capture(self, in_=False): cap = getattr(self, "_global_capturing", None) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1897f523be2..13944099ce6 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -777,12 +777,21 @@ def _mark_plugins_for_rewrite(self, hook): for name in _iter_rewritable_modules(package_files): hook.mark_rewrite(name) + def _validate_args(self, args): + """Validate known args.""" + self._parser.parse_known_and_unknown_args( + args, namespace=copy.copy(self.option) + ) + return args + def _preparse(self, args, addopts=True): if addopts: - args[:] = shlex.split(os.environ.get("PYTEST_ADDOPTS", "")) + args + env_addopts = os.environ.get("PYTEST_ADDOPTS", "") + if len(env_addopts): + args[:] = self._validate_args(shlex.split(env_addopts)) + args self._initini(args) if addopts: - args[:] = self.getini("addopts") + args + args[:] = self._validate_args(self.getini("addopts")) + args self._checkversion() self._consider_importhook(args) self.pluginmanager.consider_preparse(args) diff --git a/testing/python/raises.py b/testing/python/raises.py index e3a0c4a0595..52ad6cfa685 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -197,7 +197,8 @@ class ClassLooksIterableException(Exception): pass with pytest.raises( - Failed, match="DID NOT RAISE " + Failed, + match=r"DID NOT RAISE ", ): pytest.raises(ClassLooksIterableException, lambda: None) diff --git a/testing/test_capture.py b/testing/test_capture.py index 17bb8296750..43cd700d3bf 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -302,14 +302,14 @@ def test_logging_and_immediate_setupteardown(self, testdir): """\ import logging def setup_function(function): - logging.warn("hello1") + logging.warning("hello1") def test_logging(): - logging.warn("hello2") + logging.warning("hello2") assert 0 def teardown_function(function): - logging.warn("hello3") + logging.warning("hello3") assert 0 """ ) @@ -328,14 +328,14 @@ def test_logging_and_crossscope_fixtures(self, testdir): """\ import logging def setup_module(function): - logging.warn("hello1") + logging.warning("hello1") def test_logging(): - logging.warn("hello2") + logging.warning("hello2") assert 0 def teardown_module(function): - logging.warn("hello3") + logging.warning("hello3") assert 0 """ ) @@ -354,7 +354,7 @@ def test_conftestlogging_is_shown(self, testdir): """\ import logging logging.basicConfig() - logging.warn("hello435") + logging.warning("hello435") """ ) # make sure that logging is still captured in tests @@ -375,7 +375,7 @@ def test_conftestlogging_and_test_logging(self, testdir): """\ def test_hello(): import logging - logging.warn("hello433") + logging.warning("hello433") assert 0 """ ) @@ -385,6 +385,40 @@ def test_hello(): assert "something" not in result.stderr.str() assert "operation on closed file" not in result.stderr.str() + def test_logging_after_cap_stopped(self, testdir): + testdir.makeconftest( + """\ + import pytest + import logging + + log = logging.getLogger(__name__) + + @pytest.fixture + def log_on_teardown(): + yield + log.warning('Logging on teardown') + """ + ) + # make sure that logging is still captured in tests + p = testdir.makepyfile( + """\ + def test_hello(log_on_teardown): + import logging + logging.warning("hello433") + assert 1 + raise KeyboardInterrupt() + """ + ) + result = testdir.runpytest_subprocess(p, "--log-cli-level", "info") + assert result.ret != 0 + result.stdout.fnmatch_lines( + ["*WARNING*hello433*", "*WARNING*Logging on teardown*"] + ) + assert ( + "AttributeError: 'NoneType' object has no attribute 'resume_capturing'" + not in result.stderr.str() + ) + class TestCaptureFixture(object): @pytest.mark.parametrize("opt", [[], ["-s"]]) @@ -1300,13 +1334,13 @@ def test_capturing_and_logging_fundamentals(testdir, method): Capture=capture.%s) cap.start_capturing() - logging.warn("hello1") + logging.warning("hello1") outerr = cap.readouterr() print("suspend, captured %%s" %%(outerr,)) - logging.warn("hello2") + logging.warning("hello2") cap.pop_outerr_to_orig() - logging.warn("hello3") + logging.warning("hello3") outerr = cap.readouterr() print("suspend2, captured %%s" %% (outerr,)) diff --git a/testing/test_config.py b/testing/test_config.py index fcb886d5317..012b8936c3a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1083,6 +1083,33 @@ def test_addopts_before_initini(self, monkeypatch): config._preparse([], addopts=True) assert config._override_ini == ["cache_dir=%s" % cache_dir] + def test_addopts_from_env_not_concatenated(self, monkeypatch): + """PYTEST_ADDOPTS should not take values from normal args (#4265).""" + from _pytest.config import get_config + + monkeypatch.setenv("PYTEST_ADDOPTS", "-o") + config = get_config() + with pytest.raises(SystemExit) as excinfo: + config._preparse(["cache_dir=ignored"], addopts=True) + assert excinfo.value.args[0] == _pytest.main.EXIT_USAGEERROR + + def test_addopts_from_ini_not_concatenated(self, testdir): + """addopts from ini should not take values from normal args (#4265).""" + testdir.makeini( + """ + [pytest] + addopts=-o + """ + ) + result = testdir.runpytest("cache_dir=ignored") + result.stderr.fnmatch_lines( + [ + "%s: error: argument -o/--override-ini: expected one argument" + % (testdir.request.config._parser.optparser.prog,) + ] + ) + assert result.ret == _pytest.main.EXIT_USAGEERROR + def test_override_ini_does_not_contain_paths(self): """Check that -o no longer swallows all options after it (#3103)""" from _pytest.config import get_config