From c2212b9794ec9b4458110b1059f86b58978bb265 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Fri, 19 Oct 2018 11:37:48 +0100 Subject: [PATCH 1/2] tox.ini: fix passing {posargs} in several environments CONTRIBUTING.rst claims the following: Or to only run tests in a particular test module on Python 3.6:: $ tox -e py36 -- testing/test_config.py But without this patch, this doesn't work: the arguments after -- are ignored and all tests are run. --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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. From 393b5edcd3c3431538ba81b3fe97595db25f0921 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Fri, 19 Oct 2018 11:31:25 +0100 Subject: [PATCH 2/2] Track visited directories when collecting This fixes trying to traverse exponentially many paths in the presence of symlink loops, and trying to run any tests discovered in that tree exponentially many times if collecting ever finishes. I wanted to also prevent visiting files more than once, but my first attempt broke --keep-duplicates. Fixes #624 --- AUTHORS | 1 + src/_pytest/main.py | 12 +++++++ testing/examples/test_issue624.py | 56 +++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 testing/examples/test_issue624.py 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)