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/src/_pytest/main.py b/src/_pytest/main.py index 81a597985..cb2c6cfe4 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -597,7 +597,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/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/testing/test_collection.py b/testing/test_collection.py index 5fe313f98..d78c21f63 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1186,3 +1186,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*", + ] + )