diff --git a/.travis.yml b/.travis.yml index 66d06fccd..9010ae1c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -sudo: false language: python dist: xenial stages: @@ -26,7 +25,7 @@ env: matrix: allow_failures: - python: '3.8-dev' - env: TOXENV=py38 + env: TOXENV=py38-xdist jobs: include: @@ -34,14 +33,12 @@ jobs: - env: TOXENV=pypy PYTEST_NO_COVERAGE=1 python: 'pypy-5.4' dist: trusty - - env: TOXENV=py34 + - env: TOXENV=py34-xdist python: '3.4' - - env: TOXENV=py35 + - env: TOXENV=py35-xdist python: '3.5' - - env: TOXENV=py36 + - env: TOXENV=py36-xdist python: '3.6' - - env: TOXENV=py38 - python: '3.8-dev' - env: TOXENV=py37 - &test-macos language: generic @@ -50,15 +47,20 @@ jobs: sudo: required install: - python -m pip install --pre tox - env: TOXENV=py27 + env: TOXENV=py27-xdist - <<: *test-macos - env: TOXENV=py37 + env: TOXENV=py37-xdist before_install: - brew update - brew upgrade python - brew unlink python - brew link python + # Jobs only run via Travis cron jobs (currently daily). + - env: TOXENV=py38-xdist + python: '3.8-dev' + if: type = cron + - stage: baseline env: TOXENV=py27-pexpect,py27-trial,py27-numpy - env: TOXENV=py37-xdist diff --git a/changelog/4782.bugfix.rst b/changelog/4782.bugfix.rst new file mode 100644 index 000000000..12e08d00c --- /dev/null +++ b/changelog/4782.bugfix.rst @@ -0,0 +1 @@ +Fix ``AssertionError`` with collection of broken symlinks with packages. diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 186e12853..b119adcf0 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -88,23 +88,30 @@ and if you need to have access to the actual exception info you may use:: the actual exception raised. The main attributes of interest are ``.type``, ``.value`` and ``.traceback``. -.. versionchanged:: 3.0 +You can pass a ``match`` keyword parameter to the context-manager to test +that a regular expression matches on the string representation of an exception +(similar to the ``TestCase.assertRaisesRegexp`` method from ``unittest``):: -In the context manager form you may use the keyword argument -``message`` to specify a custom failure message:: + import pytest - >>> with raises(ZeroDivisionError, message="Expecting ZeroDivisionError"): - ... pass - ... Failed: Expecting ZeroDivisionError + def myfunc(): + raise ValueError("Exception 123 raised") -If you want to write test code that works on Python 2.4 as well, -you may also use two other ways to test for an expected exception:: + def test_match(): + with pytest.raises(ValueError, match=r'.* 123 .*'): + myfunc() + +The regexp parameter of the ``match`` method is matched with the ``re.search`` +function, so in the above example ``match='123'`` would have worked as +well. + +There's an alternate form of the ``pytest.raises`` function where you pass +a function that will be executed with the given ``*args`` and ``**kwargs`` and +assert that the given exception is raised:: pytest.raises(ExpectedException, func, *args, **kwargs) -which will execute the specified function with args and kwargs and -assert that the given ``ExpectedException`` is raised. The reporter will -provide you with helpful output in case of failures such as *no +The reporter will provide you with helpful output in case of failures such as *no exception* or *wrong exception*. Note that it is also possible to specify a "raises" argument to @@ -121,23 +128,6 @@ exceptions your own code is deliberately raising, whereas using like documenting unfixed bugs (where the test describes what "should" happen) or bugs in dependencies. -Also, the context manager form accepts a ``match`` keyword parameter to test -that a regular expression matches on the string representation of an exception -(like the ``TestCase.assertRaisesRegexp`` method from ``unittest``):: - - import pytest - - def myfunc(): - raise ValueError("Exception 123 raised") - - def test_match(): - with pytest.raises(ValueError, match=r'.* 123 .*'): - myfunc() - -The regexp parameter of the ``match`` method is matched with the ``re.search`` -function. So in the above example ``match='123'`` would have worked as -well. - .. _`assertwarns`: diff --git a/doc/en/talks.rst b/doc/en/talks.rst index aa1fb00e7..be0c6fb9f 100644 --- a/doc/en/talks.rst +++ b/doc/en/talks.rst @@ -25,6 +25,9 @@ Talks and blog postings - pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyconES 2017 (`slides in english `_, `video in spanish `_) +- `pytest advanced, Andrew Svetlov (Russian, PyCon Russia, 2016) + `_. + - `Pythonic testing, Igor Starikov (Russian, PyNsk, November 2016) `_. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index b49de86fb..6e639d872 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -618,7 +618,12 @@ class Session(nodes.FSCollector): yield y def _collectfile(self, path, handle_dupes=True): - assert path.isfile() + assert path.isfile(), "%r is not a file (isdir=%r, exists=%r, islink=%r)" % ( + path, + path.isdir(), + path.exists(), + path.islink(), + ) ihook = self.gethookproxy(path) if not self.isinitpath(path): if ihook.pytest_ignore_collect(path=path, config=self.config): diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 00ec80894..ce02e70cd 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -113,11 +113,12 @@ class Node(object): :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning. - Example usage:: + Example usage: .. code-block:: python node.warn(PytestWarning("some message")) + """ from _pytest.warning_types import PytestWarning diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 48962d137..215015d27 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -599,7 +599,12 @@ class Package(Module): return proxy def _collectfile(self, path, handle_dupes=True): - assert path.isfile() + assert path.isfile(), "%r is not a file (isdir=%r, exists=%r, islink=%r)" % ( + path, + path.isdir(), + path.exists(), + path.islink(), + ) ihook = self.gethookproxy(path) if not self.isinitpath(path): if ihook.pytest_ignore_collect(path=path, config=self.config): @@ -632,7 +637,8 @@ class Package(Module): pkg_prefixes = set() for path in this_path.visit(rec=self._recurse, bf=True, sort=True): # We will visit our own __init__.py file, in which case we skip it. - if path.isfile(): + is_file = path.isfile() + if is_file: if path.basename == "__init__.py" and path.dirpath() == this_path: continue @@ -643,12 +649,14 @@ class Package(Module): ): continue - if path.isdir(): - if path.join("__init__.py").check(file=1): - pkg_prefixes.add(path) - else: + if is_file: for x in self._collectfile(path): yield x + elif not path.isdir(): + # Broken symlink or invalid/missing file. + continue + elif path.join("__init__.py").check(file=1): + pkg_prefixes.add(path) def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b2e418030..eda0c0905 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -280,7 +280,9 @@ class TerminalReporter(object): def write_fspath_result(self, nodeid, res, **markup): fspath = self.config.rootdir.join(nodeid.split("::")[0]) - if fspath != self.currentfspath: + # NOTE: explicitly check for None to work around py bug, and for less + # overhead in general (https://github.com/pytest-dev/py/pull/207). + if self.currentfspath is None or fspath != self.currentfspath: if self.currentfspath is not None and self._show_progress_info: self._write_progress_information_filling_space() self.currentfspath = fspath diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 843c8ca37..4d109cc2b 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -31,7 +31,7 @@ class TempPathFactory(object): # using os.path.abspath() to get absolute path instead of resolve() as it # does not work the same in all platforms (see #4427) # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012) - convert=attr.converters.optional( + converter=attr.converters.optional( lambda p: Path(os.path.abspath(six.text_type(p))) ) ) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index a852277cc..bfb6acfab 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1308,10 +1308,17 @@ class TestEarlyRewriteBailout(object): @pytest.mark.skipif( sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" ) - def test_cwd_changed(self, testdir): + def test_cwd_changed(self, testdir, monkeypatch): + # Setup conditions for py's fspath trying to import pathlib on py34 + # always (previously triggered via xdist only). + # Ref: https://github.com/pytest-dev/py/pull/207 + monkeypatch.setattr(sys, "path", [""] + sys.path) + if "pathlib" in sys.modules: + del sys.modules["pathlib"] + testdir.makepyfile( **{ - "test_bar.py": """ + "test_setup_nonexisting_cwd.py": """ import os import shutil import tempfile @@ -1320,7 +1327,7 @@ class TestEarlyRewriteBailout(object): os.chdir(d) shutil.rmtree(d) """, - "test_foo.py": """ + "test_test.py": """ def test(): pass """, diff --git a/testing/test_collection.py b/testing/test_collection.py index 3eb398b9f..97c46d8c2 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1206,3 +1206,30 @@ def test_collect_pkg_init_and_file_in_args(testdir): "*2 passed in*", ] ) + + +@pytest.mark.skipif( + not hasattr(py.path.local, "mksymlinkto"), + reason="symlink not available on this platform", +) +@pytest.mark.parametrize("use_pkg", (True, False)) +def test_collect_sub_with_symlinks(use_pkg, testdir): + sub = testdir.mkdir("sub") + if use_pkg: + sub.ensure("__init__.py") + sub.ensure("test_file.py").write("def test_file(): pass") + + # Create a broken symlink. + sub.join("test_broken.py").mksymlinkto("test_doesnotexist.py") + + # Symlink that gets collected. + sub.join("test_symlink.py").mksymlinkto("test_file.py") + + result = testdir.runpytest("-v", str(sub)) + result.stdout.fnmatch_lines( + [ + "sub/test_file.py::test_file PASSED*", + "sub/test_symlink.py::test_file PASSED*", + "*2 passed in*", + ] + )