Skip to content

Commit

Permalink
Fix crash when passing a very long cmdline argument (#11404)
Browse files Browse the repository at this point in the history
Fixes #11394

(cherry picked from commit 28ccf47)
  • Loading branch information
nicoddemus committed Sep 7, 2023
1 parent 6e49a74 commit 884b911
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 1 deletion.
1 change: 1 addition & 0 deletions changelog/11394.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed crash when parsing long command line arguments that might be interpreted as files.
3 changes: 2 additions & 1 deletion src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import safe_exists
from _pytest.pathlib import visit
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
Expand Down Expand Up @@ -895,7 +896,7 @@ def resolve_collection_argument(
strpath = search_pypath(strpath)
fspath = invocation_path / strpath
fspath = absolutepath(fspath)
if not fspath.exists():
if not safe_exists(fspath):
msg = (
"module or package not found: {arg} (missing __init__.py?)"
if as_pypath
Expand Down
11 changes: 11 additions & 0 deletions src/_pytest/pathlib.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import atexit
import contextlib
import errno
import fnmatch
import importlib.util
import itertools
Expand Down Expand Up @@ -791,3 +792,13 @@ def copytree(source: Path, target: Path) -> None:
shutil.copyfile(x, newx)
elif x.is_dir():
newx.mkdir(exist_ok=True)


def safe_exists(p: Path) -> bool:
"""Like Path.exists(), but account for input arguments that might be too long (#11394)."""
try:
return p.exists()
except OSError as e:

Check warning on line 801 in src/_pytest/pathlib.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L801

Added line #L801 was not covered by tests
if e.errno == errno.ENAMETOOLONG:
return False
raise

Check warning on line 804 in src/_pytest/pathlib.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L803-L804

Added lines #L803 - L804 were not covered by tests
31 changes: 31 additions & 0 deletions testing/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,34 @@ def test(fix):
"* 1 passed in *",
]
)


def test_very_long_cmdline_arg(pytester: Pytester) -> None:
"""
Regression test for #11394.
Note: we could not manage to actually reproduce the error with this code, we suspect
GitHub runners are configured to support very long paths, however decided to leave
the test in place in case this ever regresses in the future.
"""
pytester.makeconftest(
"""
import pytest
def pytest_addoption(parser):
parser.addoption("--long-list", dest="long_list", action="store", default="all", help="List of things")
@pytest.fixture(scope="module")
def specified_feeds(request):
list_string = request.config.getoption("--long-list")
return list_string.split(',')
"""
)
pytester.makepyfile(
"""
def test_foo(specified_feeds):
assert len(specified_feeds) == 100_000
"""
)
result = pytester.runpytest("--long-list", ",".join(["helloworld"] * 100_000))
result.stdout.fnmatch_lines("* 1 passed *")
32 changes: 32 additions & 0 deletions testing/test_pathlib.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import errno
import os.path
import pickle
import sys
Expand All @@ -24,6 +25,7 @@
from _pytest.pathlib import maybe_delete_a_numbered_dir
from _pytest.pathlib import module_name_from_path
from _pytest.pathlib import resolve_package_path
from _pytest.pathlib import safe_exists
from _pytest.pathlib import symlink_or_skip
from _pytest.pathlib import visit
from _pytest.tmpdir import TempPathFactory
Expand Down Expand Up @@ -660,3 +662,33 @@ def __init__(self) -> None:

mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
assert len(mod.instance.INSTANCES) == 1


def test_safe_exists(tmp_path: Path) -> None:
d = tmp_path.joinpath("some_dir")
d.mkdir()
assert safe_exists(d) is True

f = tmp_path.joinpath("some_file")
f.touch()
assert safe_exists(f) is True

# Use unittest.mock() as a context manager to have a very narrow
# patch lifetime.
p = tmp_path.joinpath("some long filename" * 100)
with unittest.mock.patch.object(
Path,
"exists",
autospec=True,
side_effect=OSError(errno.ENAMETOOLONG, "name too long"),
):
assert safe_exists(p) is False

with unittest.mock.patch.object(
Path,
"exists",
autospec=True,
side_effect=OSError(errno.EIO, "another kind of error"),
):
with pytest.raises(OSError):
_ = safe_exists(p)

0 comments on commit 884b911

Please sign in to comment.