diff --git a/AUTHORS b/AUTHORS index c63c0a00591..0ffbe3edbf1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -223,6 +223,7 @@ Virgil Dupras Vitaly Lashmanov Vlad Dragos Wil Cooley +Will Thompson William Lee Wim Glenn Wouter van Ackooy diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 8d4176aeafe..17f44e16d9a 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -392,6 +392,8 @@ def __init__(self, config): self._initialpaths = frozenset() # Keep track of any collected nodes in here, so we don't duplicate fixtures self._node_cache = {} + # Keep track of visited directories in here, so we don't end up in a symlink-induced loop. + self._visited = set() self.config.pluginmanager.register(self, name="session") @@ -558,7 +560,17 @@ def _collectfile(self, path): return () return ihook.pytest_collect_file(path=path, parent=self) + def _check_visited(self, path): + st = path.stat() + key = (st.dev, st.ino) + if key in self._visited: + return True + self._visited.add(key) + return False + def _recurse(self, path): + if self._check_visited(path): + return False ihook = self.gethookproxy(path.dirpath()) if ihook.pytest_ignore_collect(path=path, config=self.config): return diff --git a/testing/examples/test_issue624.py b/testing/examples/test_issue624.py new file mode 100644 index 00000000000..19690999db0 --- /dev/null +++ b/testing/examples/test_issue624.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function +import sys + +import six + +import py +import pytest + + +@pytest.mark.skipif( + not hasattr(py.path.local, "mksymlinkto"), + reason="symlink not available on this platform", +) +def test_624(testdir): + """ + Runs tests in the following directory tree: + + testdir/ + test_noop.py + symlink-0 -> . + symlink-1 -> . + + On Linux, the maximum number of symlinks in a path is 40, after which ELOOP + is returned when trying to read the path. This means that if we walk the + directory tree naively, following symlinks, naively, this will attempt to + visit test_noop.py via 2 ** 41 paths: + + testdir/symlink-0/test_noop.py + testdir/symlink-1/test_noop.py + testdir/symlink-0/symlink-0/test_noop.py + testdir/symlink-0/symlink-1/test_noop.py + .. and eventually .. + testdir/symlink-0/.. 2 ** 39 more combinations ../test_noop.py + testdir/symlink-1/.. 2 ** 39 more combinations ../test_noop.py + + Instead, we should stop recursing when we reach a directory we've seen + before. In this test, this means visiting the test once at the root, and + once via a symlink, then stopping. + """ + + test_noop_py = testdir.makepyfile(test_noop="def test_noop():\n pass") + + # dummy check that we can actually create symlinks: on Windows `py.path.mksymlinkto` is + # available, but normal users require special admin privileges to create symlinks. + if sys.platform == "win32": + try: + (testdir.tmpdir / ".dummy").mksymlinkto(test_noop_py) + except OSError as e: + pytest.skip(six.text_type(e.args[0])) + + for i in range(2): + (testdir.tmpdir / "symlink-{}".format(i)).mksymlinkto(testdir.tmpdir) + + result = testdir.runpytest() + result.assert_outcomes(passed=2) diff --git a/tox.ini b/tox.ini index 86b3b94581e..dbfd4eef5c4 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ envlist = [testenv] commands = - {env:_PYTEST_TOX_COVERAGE_RUN:} pytest --lsof + {env:_PYTEST_TOX_COVERAGE_RUN:} pytest --lsof {posargs} coverage: coverage combine coverage: coverage report passenv = USER USERNAME COVERAGE_* TRAVIS @@ -41,7 +41,7 @@ deps = py27: mock nose commands = - pytest -n auto --runpytest=subprocess + pytest -n auto --runpytest=subprocess {posargs} [testenv:linting] @@ -58,7 +58,7 @@ deps = hypothesis>=3.56 {env:_PYTEST_TOX_EXTRA_DEP:} commands = - {env:_PYTEST_TOX_COVERAGE_RUN:} pytest -n auto + {env:_PYTEST_TOX_COVERAGE_RUN:} pytest -n auto {posargs} [testenv:py36-xdist] # NOTE: copied from above due to https://github.com/tox-dev/tox/issues/706.