2024-01-28 21:12:42 +08:00
|
|
|
# mypy: allow-untyped-defs
|
2020-06-09 08:54:22 +08:00
|
|
|
import argparse
|
2020-08-15 21:31:17 +08:00
|
|
|
import os
|
2020-10-03 23:08:14 +08:00
|
|
|
from pathlib import Path
|
2020-08-15 21:31:17 +08:00
|
|
|
import re
|
2022-02-11 23:20:42 +08:00
|
|
|
import sys
|
2020-02-03 05:23:41 +08:00
|
|
|
from typing import Optional
|
|
|
|
|
2020-02-11 05:43:30 +08:00
|
|
|
from _pytest.config import ExitCode
|
2020-08-15 21:31:17 +08:00
|
|
|
from _pytest.config import UsageError
|
2024-03-01 17:47:30 +08:00
|
|
|
from _pytest.main import CollectionArgument
|
2020-08-15 21:31:17 +08:00
|
|
|
from _pytest.main import resolve_collection_argument
|
2020-06-09 08:54:22 +08:00
|
|
|
from _pytest.main import validate_basetemp
|
2020-12-14 21:54:59 +08:00
|
|
|
from _pytest.pytester import Pytester
|
2019-11-22 01:42:37 +08:00
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"ret_exc",
|
|
|
|
(
|
|
|
|
pytest.param((None, ValueError)),
|
|
|
|
pytest.param((42, SystemExit)),
|
|
|
|
pytest.param((False, SystemExit)),
|
|
|
|
),
|
|
|
|
)
|
2020-12-16 12:16:05 +08:00
|
|
|
def test_wrap_session_notify_exception(ret_exc, pytester: Pytester) -> None:
|
2019-11-22 01:42:37 +08:00
|
|
|
returncode, exc = ret_exc
|
2020-12-16 12:16:05 +08:00
|
|
|
c1 = pytester.makeconftest(
|
2019-11-22 01:42:37 +08:00
|
|
|
f"""
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
def pytest_sessionstart():
|
|
|
|
raise {exc.__name__}("boom")
|
|
|
|
|
|
|
|
def pytest_internalerror(excrepr, excinfo):
|
|
|
|
returncode = {returncode!r}
|
|
|
|
if returncode is not False:
|
|
|
|
pytest.exit("exiting after %s..." % excinfo.typename, returncode={returncode!r})
|
|
|
|
"""
|
|
|
|
)
|
2020-12-16 12:16:05 +08:00
|
|
|
result = pytester.runpytest()
|
2019-11-22 01:42:37 +08:00
|
|
|
if returncode:
|
|
|
|
assert result.ret == returncode
|
|
|
|
else:
|
|
|
|
assert result.ret == ExitCode.INTERNAL_ERROR
|
|
|
|
assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):"
|
|
|
|
|
2022-02-11 23:20:42 +08:00
|
|
|
end_lines = (
|
|
|
|
result.stdout.lines[-4:]
|
2022-07-13 23:06:33 +08:00
|
|
|
if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
|
2022-02-11 23:20:42 +08:00
|
|
|
else result.stdout.lines[-3:]
|
|
|
|
)
|
|
|
|
|
2019-11-22 01:42:37 +08:00
|
|
|
if exc == SystemExit:
|
2022-02-11 23:20:42 +08:00
|
|
|
assert end_lines == [
|
2020-10-03 04:16:22 +08:00
|
|
|
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
|
2019-11-22 01:42:37 +08:00
|
|
|
'INTERNALERROR> raise SystemExit("boom")',
|
2022-02-11 23:20:42 +08:00
|
|
|
*(
|
|
|
|
("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",)
|
2022-07-13 23:06:33 +08:00
|
|
|
if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
|
2022-02-11 23:20:42 +08:00
|
|
|
else ()
|
|
|
|
),
|
2019-11-22 01:42:37 +08:00
|
|
|
"INTERNALERROR> SystemExit: boom",
|
|
|
|
]
|
|
|
|
else:
|
2022-02-11 23:20:42 +08:00
|
|
|
assert end_lines == [
|
2020-10-03 04:16:22 +08:00
|
|
|
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
|
2019-11-22 01:42:37 +08:00
|
|
|
'INTERNALERROR> raise ValueError("boom")',
|
2022-02-11 23:20:42 +08:00
|
|
|
*(
|
|
|
|
("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",)
|
2022-07-13 23:06:33 +08:00
|
|
|
if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
|
2022-02-11 23:20:42 +08:00
|
|
|
else ()
|
|
|
|
),
|
2019-11-22 01:42:37 +08:00
|
|
|
"INTERNALERROR> ValueError: boom",
|
|
|
|
]
|
|
|
|
if returncode is False:
|
|
|
|
assert result.stderr.lines == ["mainloop: caught unexpected SystemExit!"]
|
|
|
|
else:
|
2020-10-03 04:16:22 +08:00
|
|
|
assert result.stderr.lines == [f"Exit: exiting after {exc.__name__}..."]
|
2020-02-03 05:23:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("returncode", (None, 42))
|
|
|
|
def test_wrap_session_exit_sessionfinish(
|
2020-12-16 12:16:05 +08:00
|
|
|
returncode: Optional[int], pytester: Pytester
|
2020-02-03 05:23:41 +08:00
|
|
|
) -> None:
|
2020-12-16 12:16:05 +08:00
|
|
|
pytester.makeconftest(
|
2020-02-03 05:23:41 +08:00
|
|
|
f"""
|
|
|
|
import pytest
|
|
|
|
def pytest_sessionfinish():
|
2021-11-08 22:31:14 +08:00
|
|
|
pytest.exit(reason="exit_pytest_sessionfinish", returncode={returncode})
|
2020-02-03 05:23:41 +08:00
|
|
|
"""
|
|
|
|
)
|
2020-12-16 12:16:05 +08:00
|
|
|
result = pytester.runpytest()
|
2020-02-03 05:23:41 +08:00
|
|
|
if returncode:
|
|
|
|
assert result.ret == returncode
|
|
|
|
else:
|
|
|
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
|
|
|
assert result.stdout.lines[-1] == "collected 0 items"
|
|
|
|
assert result.stderr.lines == ["Exit: exit_pytest_sessionfinish"]
|
2020-06-09 08:54:22 +08:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("basetemp", ["foo", "foo/bar"])
|
|
|
|
def test_validate_basetemp_ok(tmp_path, basetemp, monkeypatch):
|
|
|
|
monkeypatch.chdir(str(tmp_path))
|
|
|
|
validate_basetemp(tmp_path / basetemp)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("basetemp", ["", ".", ".."])
|
|
|
|
def test_validate_basetemp_fails(tmp_path, basetemp, monkeypatch):
|
|
|
|
monkeypatch.chdir(str(tmp_path))
|
|
|
|
msg = "basetemp must not be empty, the current working directory or any parent directory of it"
|
|
|
|
with pytest.raises(argparse.ArgumentTypeError, match=msg):
|
|
|
|
if basetemp:
|
|
|
|
basetemp = tmp_path / basetemp
|
|
|
|
validate_basetemp(basetemp)
|
|
|
|
|
|
|
|
|
2020-12-16 12:16:05 +08:00
|
|
|
def test_validate_basetemp_integration(pytester: Pytester) -> None:
|
|
|
|
result = pytester.runpytest("--basetemp=.")
|
2020-06-09 08:54:22 +08:00
|
|
|
result.stderr.fnmatch_lines("*basetemp must not be*")
|
2020-08-15 21:31:17 +08:00
|
|
|
|
|
|
|
|
|
|
|
class TestResolveCollectionArgument:
|
|
|
|
@pytest.fixture
|
2020-12-14 21:54:59 +08:00
|
|
|
def invocation_path(self, pytester: Pytester) -> Path:
|
|
|
|
pytester.syspathinsert(pytester.path / "src")
|
|
|
|
pytester.chdir()
|
2020-08-15 21:31:17 +08:00
|
|
|
|
2020-12-14 21:54:59 +08:00
|
|
|
pkg = pytester.path.joinpath("src/pkg")
|
|
|
|
pkg.mkdir(parents=True)
|
|
|
|
pkg.joinpath("__init__.py").touch()
|
|
|
|
pkg.joinpath("test.py").touch()
|
|
|
|
return pytester.path
|
2020-08-06 19:46:24 +08:00
|
|
|
|
2020-12-14 21:54:59 +08:00
|
|
|
def test_file(self, invocation_path: Path) -> None:
|
2020-08-15 21:31:17 +08:00
|
|
|
"""File and parts."""
|
2024-03-01 17:47:30 +08:00
|
|
|
assert resolve_collection_argument(
|
|
|
|
invocation_path, "src/pkg/test.py"
|
|
|
|
) == CollectionArgument(
|
|
|
|
path=invocation_path / "src/pkg/test.py",
|
|
|
|
parts=[],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name=None,
|
2020-08-15 21:31:17 +08:00
|
|
|
)
|
2024-03-01 17:47:30 +08:00
|
|
|
assert resolve_collection_argument(
|
|
|
|
invocation_path, "src/pkg/test.py::"
|
|
|
|
) == CollectionArgument(
|
|
|
|
path=invocation_path / "src/pkg/test.py",
|
|
|
|
parts=[""],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name=None,
|
2020-08-15 21:31:17 +08:00
|
|
|
)
|
2020-08-06 19:46:24 +08:00
|
|
|
assert resolve_collection_argument(
|
|
|
|
invocation_path, "src/pkg/test.py::foo::bar"
|
2024-03-01 17:47:30 +08:00
|
|
|
) == CollectionArgument(
|
|
|
|
path=invocation_path / "src/pkg/test.py",
|
|
|
|
parts=["foo", "bar"],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name=None,
|
2024-03-01 17:47:30 +08:00
|
|
|
)
|
2020-08-06 19:46:24 +08:00
|
|
|
assert resolve_collection_argument(
|
|
|
|
invocation_path, "src/pkg/test.py::foo::bar::"
|
2024-03-01 17:47:30 +08:00
|
|
|
) == CollectionArgument(
|
|
|
|
path=invocation_path / "src/pkg/test.py",
|
|
|
|
parts=["foo", "bar", ""],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name=None,
|
2024-03-01 17:47:30 +08:00
|
|
|
)
|
2020-08-15 21:31:17 +08:00
|
|
|
|
2020-12-14 21:54:59 +08:00
|
|
|
def test_dir(self, invocation_path: Path) -> None:
|
2020-08-15 21:31:17 +08:00
|
|
|
"""Directory and parts."""
|
2024-03-01 17:47:30 +08:00
|
|
|
assert resolve_collection_argument(
|
|
|
|
invocation_path, "src/pkg"
|
|
|
|
) == CollectionArgument(
|
|
|
|
path=invocation_path / "src/pkg",
|
|
|
|
parts=[],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name=None,
|
2020-08-06 19:46:24 +08:00
|
|
|
)
|
2020-08-15 21:31:17 +08:00
|
|
|
|
2020-08-21 15:59:55 +08:00
|
|
|
with pytest.raises(
|
|
|
|
UsageError, match=r"directory argument cannot contain :: selection parts"
|
|
|
|
):
|
2020-08-06 19:46:24 +08:00
|
|
|
resolve_collection_argument(invocation_path, "src/pkg::")
|
2020-08-21 15:59:55 +08:00
|
|
|
|
|
|
|
with pytest.raises(
|
|
|
|
UsageError, match=r"directory argument cannot contain :: selection parts"
|
|
|
|
):
|
2020-08-06 19:46:24 +08:00
|
|
|
resolve_collection_argument(invocation_path, "src/pkg::foo::bar")
|
2020-08-21 15:59:55 +08:00
|
|
|
|
2020-12-14 21:54:59 +08:00
|
|
|
def test_pypath(self, invocation_path: Path) -> None:
|
2020-08-15 21:31:17 +08:00
|
|
|
"""Dotted name and parts."""
|
|
|
|
assert resolve_collection_argument(
|
2020-08-06 19:46:24 +08:00
|
|
|
invocation_path, "pkg.test", as_pypath=True
|
2024-03-01 17:47:30 +08:00
|
|
|
) == CollectionArgument(
|
|
|
|
path=invocation_path / "src/pkg/test.py",
|
|
|
|
parts=[],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name="pkg.test",
|
2024-03-01 17:47:30 +08:00
|
|
|
)
|
2020-08-06 19:46:24 +08:00
|
|
|
assert resolve_collection_argument(
|
|
|
|
invocation_path, "pkg.test::foo::bar", as_pypath=True
|
2024-03-01 17:47:30 +08:00
|
|
|
) == CollectionArgument(
|
|
|
|
path=invocation_path / "src/pkg/test.py",
|
|
|
|
parts=["foo", "bar"],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name="pkg.test",
|
2024-03-01 17:47:30 +08:00
|
|
|
)
|
|
|
|
assert resolve_collection_argument(
|
|
|
|
invocation_path, "pkg", as_pypath=True
|
|
|
|
) == CollectionArgument(
|
|
|
|
path=invocation_path / "src/pkg",
|
|
|
|
parts=[],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name="pkg",
|
2020-08-15 21:31:17 +08:00
|
|
|
)
|
2020-08-21 15:59:55 +08:00
|
|
|
|
|
|
|
with pytest.raises(
|
|
|
|
UsageError, match=r"package argument cannot contain :: selection parts"
|
|
|
|
):
|
2020-08-06 19:46:24 +08:00
|
|
|
resolve_collection_argument(
|
|
|
|
invocation_path, "pkg::foo::bar", as_pypath=True
|
|
|
|
)
|
2020-08-15 21:31:17 +08:00
|
|
|
|
2022-02-08 09:17:14 +08:00
|
|
|
def test_parametrized_name_with_colons(self, invocation_path: Path) -> None:
|
2024-03-01 17:47:30 +08:00
|
|
|
assert resolve_collection_argument(
|
2022-02-08 09:17:14 +08:00
|
|
|
invocation_path, "src/pkg/test.py::test[a::b]"
|
2024-03-01 17:47:30 +08:00
|
|
|
) == CollectionArgument(
|
|
|
|
path=invocation_path / "src/pkg/test.py",
|
|
|
|
parts=["test[a::b]"],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name=None,
|
2022-02-08 09:17:14 +08:00
|
|
|
)
|
|
|
|
|
2020-08-06 19:46:24 +08:00
|
|
|
def test_does_not_exist(self, invocation_path: Path) -> None:
|
2020-08-15 21:31:17 +08:00
|
|
|
"""Given a file/module that does not exist raises UsageError."""
|
|
|
|
with pytest.raises(
|
|
|
|
UsageError, match=re.escape("file or directory not found: foobar")
|
|
|
|
):
|
2020-08-06 19:46:24 +08:00
|
|
|
resolve_collection_argument(invocation_path, "foobar")
|
2020-08-15 21:31:17 +08:00
|
|
|
|
|
|
|
with pytest.raises(
|
|
|
|
UsageError,
|
|
|
|
match=re.escape(
|
|
|
|
"module or package not found: foobar (missing __init__.py?)"
|
|
|
|
),
|
|
|
|
):
|
2020-08-06 19:46:24 +08:00
|
|
|
resolve_collection_argument(invocation_path, "foobar", as_pypath=True)
|
2020-08-15 21:31:17 +08:00
|
|
|
|
2020-12-14 21:54:59 +08:00
|
|
|
def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> None:
|
2020-08-15 21:31:17 +08:00
|
|
|
"""Absolute paths resolve back to absolute paths."""
|
2020-12-14 21:54:59 +08:00
|
|
|
full_path = str(invocation_path / "src")
|
2024-03-01 17:47:30 +08:00
|
|
|
assert resolve_collection_argument(
|
|
|
|
invocation_path, full_path
|
|
|
|
) == CollectionArgument(
|
|
|
|
path=Path(os.path.abspath("src")),
|
|
|
|
parts=[],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name=None,
|
2020-08-15 21:31:17 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
# ensure full paths given in the command-line without the drive letter resolve
|
|
|
|
# to the full path correctly (#7628)
|
|
|
|
drive, full_path_without_drive = os.path.splitdrive(full_path)
|
2020-08-06 19:46:24 +08:00
|
|
|
assert resolve_collection_argument(
|
|
|
|
invocation_path, full_path_without_drive
|
2024-03-01 17:47:30 +08:00
|
|
|
) == CollectionArgument(
|
|
|
|
path=Path(os.path.abspath("src")),
|
|
|
|
parts=[],
|
2024-03-01 17:59:26 +08:00
|
|
|
module_name=None,
|
2024-03-01 17:47:30 +08:00
|
|
|
)
|
2020-08-15 21:31:17 +08:00
|
|
|
|
|
|
|
|
2020-12-16 12:16:05 +08:00
|
|
|
def test_module_full_path_without_drive(pytester: Pytester) -> None:
|
2020-08-15 21:31:17 +08:00
|
|
|
"""Collect and run test using full path except for the drive letter (#7628).
|
|
|
|
|
2021-02-22 16:43:52 +08:00
|
|
|
Passing a full path without a drive letter would trigger a bug in legacy_path
|
2020-08-15 21:31:17 +08:00
|
|
|
where it would keep the full path without the drive letter around, instead of resolving
|
|
|
|
to the full path, resulting in fixtures node ids not matching against test node ids correctly.
|
|
|
|
"""
|
2020-12-16 12:16:05 +08:00
|
|
|
pytester.makepyfile(
|
2020-08-15 21:31:17 +08:00
|
|
|
**{
|
|
|
|
"project/conftest.py": """
|
|
|
|
import pytest
|
|
|
|
@pytest.fixture
|
|
|
|
def fix(): return 1
|
|
|
|
""",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2020-12-16 12:16:05 +08:00
|
|
|
pytester.makepyfile(
|
2020-08-15 21:31:17 +08:00
|
|
|
**{
|
|
|
|
"project/tests/dummy_test.py": """
|
|
|
|
def test(fix):
|
|
|
|
assert fix == 1
|
|
|
|
"""
|
|
|
|
}
|
|
|
|
)
|
2020-12-16 12:16:05 +08:00
|
|
|
fn = pytester.path.joinpath("project/tests/dummy_test.py")
|
|
|
|
assert fn.is_file()
|
2020-08-15 21:31:17 +08:00
|
|
|
|
|
|
|
drive, path = os.path.splitdrive(str(fn))
|
|
|
|
|
2020-12-16 12:16:05 +08:00
|
|
|
result = pytester.runpytest(path, "-v")
|
2020-08-15 21:31:17 +08:00
|
|
|
result.stdout.fnmatch_lines(
|
|
|
|
[
|
|
|
|
os.path.join("project", "tests", "dummy_test.py") + "::test PASSED *",
|
|
|
|
"* 1 passed in *",
|
|
|
|
]
|
|
|
|
)
|
2023-09-07 23:49:25 +08:00
|
|
|
|
|
|
|
|
|
|
|
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 *")
|