Fix crash when passing a very long cmdline argument (#11404)

Fixes #11394
This commit is contained in:
Bruno Oliveira 2023-09-07 12:49:25 -03:00 committed by GitHub
parent 333e4eba6b
commit 28ccf476b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 1 deletions

View File

@ -0,0 +1 @@
Fixed crash when parsing long command line arguments that might be interpreted as files.

View File

@ -36,6 +36,7 @@ from _pytest.outcomes import exit
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
@ -888,7 +889,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

View File

@ -1,5 +1,6 @@
import atexit
import contextlib
import errno
import fnmatch
import importlib.util
import itertools
@ -773,3 +774,13 @@ def bestrelpath(directory: Path, dest: Path) -> str:
# Forward from base to dest.
*reldest.parts,
)
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:
if e.errno == errno.ENAMETOOLONG:
return False
raise

View File

@ -262,3 +262,34 @@ def test_module_full_path_without_drive(pytester: Pytester) -> None:
"* 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 *")

View File

@ -1,3 +1,4 @@
import errno
import os.path
import pickle
import sys
@ -24,6 +25,7 @@ from _pytest.pathlib import insert_missing_modules
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
@ -660,3 +662,33 @@ class TestImportLibMode:
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)