Skip to content

Commit e4f022f

Browse files
authored
Merge pull request #11406 from nicoddemus/backport-11404-to-7.4.x
[7.4.x] Fix crash when passing a very long cmdline argument (#11404)
2 parents 6e49a74 + 63b0c6f commit e4f022f

File tree

7 files changed

+79
-15
lines changed

7 files changed

+79
-15
lines changed

changelog/11394.bugfix.rst

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

src/_pytest/config/__init__.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from _pytest.pathlib import import_path
5858
from _pytest.pathlib import ImportMode
5959
from _pytest.pathlib import resolve_package_path
60+
from _pytest.pathlib import safe_exists
6061
from _pytest.stash import Stash
6162
from _pytest.warning_types import PytestConfigWarning
6263
from _pytest.warning_types import warn_explicit_for
@@ -557,12 +558,8 @@ def _set_initial_conftests(
557558
anchor = absolutepath(current / path)
558559

559560
# Ensure we do not break if what appears to be an anchor
560-
# is in fact a very long option (#10169).
561-
try:
562-
anchor_exists = anchor.exists()
563-
except OSError: # pragma: no cover
564-
anchor_exists = False
565-
if anchor_exists:
561+
# is in fact a very long option (#10169, #11394).
562+
if safe_exists(anchor):
566563
self._try_load_conftest(anchor, importmode, rootpath)
567564
foundanchor = True
568565
if not foundanchor:

src/_pytest/config/findpaths.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from _pytest.outcomes import fail
1717
from _pytest.pathlib import absolutepath
1818
from _pytest.pathlib import commonpath
19+
from _pytest.pathlib import safe_exists
1920

2021
if TYPE_CHECKING:
2122
from . import Config
@@ -151,14 +152,6 @@ def get_dir_from_path(path: Path) -> Path:
151152
return path
152153
return path.parent
153154

154-
def safe_exists(path: Path) -> bool:
155-
# This can throw on paths that contain characters unrepresentable at the OS level,
156-
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
157-
try:
158-
return path.exists()
159-
except OSError:
160-
return False
161-
162155
# These look like paths but may not exist
163156
possible_paths = (
164157
absolutepath(get_file_part_from_node_id(arg))

src/_pytest/main.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from _pytest.pathlib import absolutepath
3737
from _pytest.pathlib import bestrelpath
3838
from _pytest.pathlib import fnmatch_ex
39+
from _pytest.pathlib import safe_exists
3940
from _pytest.pathlib import visit
4041
from _pytest.reports import CollectReport
4142
from _pytest.reports import TestReport
@@ -895,7 +896,7 @@ def resolve_collection_argument(
895896
strpath = search_pypath(strpath)
896897
fspath = invocation_path / strpath
897898
fspath = absolutepath(fspath)
898-
if not fspath.exists():
899+
if not safe_exists(fspath):
899900
msg = (
900901
"module or package not found: {arg} (missing __init__.py?)"
901902
if as_pypath

src/_pytest/pathlib.py

+10
Original file line numberDiff line numberDiff line change
@@ -791,3 +791,13 @@ def copytree(source: Path, target: Path) -> None:
791791
shutil.copyfile(x, newx)
792792
elif x.is_dir():
793793
newx.mkdir(exist_ok=True)
794+
795+
796+
def safe_exists(p: Path) -> bool:
797+
"""Like Path.exists(), but account for input arguments that might be too long (#11394)."""
798+
try:
799+
return p.exists()
800+
except (ValueError, OSError):
801+
# ValueError: stat: path too long for Windows
802+
# OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
803+
return False

testing/test_main.py

+31
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,34 @@ def test(fix):
262262
"* 1 passed in *",
263263
]
264264
)
265+
266+
267+
def test_very_long_cmdline_arg(pytester: Pytester) -> None:
268+
"""
269+
Regression test for #11394.
270+
271+
Note: we could not manage to actually reproduce the error with this code, we suspect
272+
GitHub runners are configured to support very long paths, however decided to leave
273+
the test in place in case this ever regresses in the future.
274+
"""
275+
pytester.makeconftest(
276+
"""
277+
import pytest
278+
279+
def pytest_addoption(parser):
280+
parser.addoption("--long-list", dest="long_list", action="store", default="all", help="List of things")
281+
282+
@pytest.fixture(scope="module")
283+
def specified_feeds(request):
284+
list_string = request.config.getoption("--long-list")
285+
return list_string.split(',')
286+
"""
287+
)
288+
pytester.makepyfile(
289+
"""
290+
def test_foo(specified_feeds):
291+
assert len(specified_feeds) == 100_000
292+
"""
293+
)
294+
result = pytester.runpytest("--long-list", ",".join(["helloworld"] * 100_000))
295+
result.stdout.fnmatch_lines("* 1 passed *")

testing/test_pathlib.py

+31
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import errno
12
import os.path
23
import pickle
34
import sys
@@ -24,6 +25,7 @@
2425
from _pytest.pathlib import maybe_delete_a_numbered_dir
2526
from _pytest.pathlib import module_name_from_path
2627
from _pytest.pathlib import resolve_package_path
28+
from _pytest.pathlib import safe_exists
2729
from _pytest.pathlib import symlink_or_skip
2830
from _pytest.pathlib import visit
2931
from _pytest.tmpdir import TempPathFactory
@@ -660,3 +662,32 @@ def __init__(self) -> None:
660662

661663
mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
662664
assert len(mod.instance.INSTANCES) == 1
665+
666+
667+
def test_safe_exists(tmp_path: Path) -> None:
668+
d = tmp_path.joinpath("some_dir")
669+
d.mkdir()
670+
assert safe_exists(d) is True
671+
672+
f = tmp_path.joinpath("some_file")
673+
f.touch()
674+
assert safe_exists(f) is True
675+
676+
# Use unittest.mock() as a context manager to have a very narrow
677+
# patch lifetime.
678+
p = tmp_path.joinpath("some long filename" * 100)
679+
with unittest.mock.patch.object(
680+
Path,
681+
"exists",
682+
autospec=True,
683+
side_effect=OSError(errno.ENAMETOOLONG, "name too long"),
684+
):
685+
assert safe_exists(p) is False
686+
687+
with unittest.mock.patch.object(
688+
Path,
689+
"exists",
690+
autospec=True,
691+
side_effect=ValueError("name too long"),
692+
):
693+
assert safe_exists(p) is False

0 commit comments

Comments
 (0)