1import argparse
2import os
3import re
4from typing import Optional
5
6import py.path
7
8import pytest
9from _pytest.config import ExitCode
10from _pytest.config import UsageError
11from _pytest.main import resolve_collection_argument
12from _pytest.main import validate_basetemp
13from _pytest.pathlib import Path
14from _pytest.pytester import Testdir
15
16
17@pytest.mark.parametrize(
18    "ret_exc",
19    (
20        pytest.param((None, ValueError)),
21        pytest.param((42, SystemExit)),
22        pytest.param((False, SystemExit)),
23    ),
24)
25def test_wrap_session_notify_exception(ret_exc, testdir):
26    returncode, exc = ret_exc
27    c1 = testdir.makeconftest(
28        """
29        import pytest
30
31        def pytest_sessionstart():
32            raise {exc}("boom")
33
34        def pytest_internalerror(excrepr, excinfo):
35            returncode = {returncode!r}
36            if returncode is not False:
37                pytest.exit("exiting after %s..." % excinfo.typename, returncode={returncode!r})
38    """.format(
39            returncode=returncode, exc=exc.__name__
40        )
41    )
42    result = testdir.runpytest()
43    if returncode:
44        assert result.ret == returncode
45    else:
46        assert result.ret == ExitCode.INTERNAL_ERROR
47    assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):"
48
49    if exc == SystemExit:
50        assert result.stdout.lines[-3:] == [
51            'INTERNALERROR>   File "{}", line 4, in pytest_sessionstart'.format(c1),
52            'INTERNALERROR>     raise SystemExit("boom")',
53            "INTERNALERROR> SystemExit: boom",
54        ]
55    else:
56        assert result.stdout.lines[-3:] == [
57            'INTERNALERROR>   File "{}", line 4, in pytest_sessionstart'.format(c1),
58            'INTERNALERROR>     raise ValueError("boom")',
59            "INTERNALERROR> ValueError: boom",
60        ]
61    if returncode is False:
62        assert result.stderr.lines == ["mainloop: caught unexpected SystemExit!"]
63    else:
64        assert result.stderr.lines == ["Exit: exiting after {}...".format(exc.__name__)]
65
66
67@pytest.mark.parametrize("returncode", (None, 42))
68def test_wrap_session_exit_sessionfinish(
69    returncode: Optional[int], testdir: Testdir
70) -> None:
71    testdir.makeconftest(
72        """
73        import pytest
74        def pytest_sessionfinish():
75            pytest.exit(msg="exit_pytest_sessionfinish", returncode={returncode})
76    """.format(
77            returncode=returncode
78        )
79    )
80    result = testdir.runpytest()
81    if returncode:
82        assert result.ret == returncode
83    else:
84        assert result.ret == ExitCode.NO_TESTS_COLLECTED
85    assert result.stdout.lines[-1] == "collected 0 items"
86    assert result.stderr.lines == ["Exit: exit_pytest_sessionfinish"]
87
88
89@pytest.mark.parametrize("basetemp", ["foo", "foo/bar"])
90def test_validate_basetemp_ok(tmp_path, basetemp, monkeypatch):
91    monkeypatch.chdir(str(tmp_path))
92    validate_basetemp(tmp_path / basetemp)
93
94
95@pytest.mark.parametrize("basetemp", ["", ".", ".."])
96def test_validate_basetemp_fails(tmp_path, basetemp, monkeypatch):
97    monkeypatch.chdir(str(tmp_path))
98    msg = "basetemp must not be empty, the current working directory or any parent directory of it"
99    with pytest.raises(argparse.ArgumentTypeError, match=msg):
100        if basetemp:
101            basetemp = tmp_path / basetemp
102        validate_basetemp(basetemp)
103
104
105def test_validate_basetemp_integration(testdir):
106    result = testdir.runpytest("--basetemp=.")
107    result.stderr.fnmatch_lines("*basetemp must not be*")
108
109
110class TestResolveCollectionArgument:
111    @pytest.fixture
112    def invocation_dir(self, testdir: Testdir) -> py.path.local:
113        testdir.syspathinsert(str(testdir.tmpdir / "src"))
114        testdir.chdir()
115
116        pkg = testdir.tmpdir.join("src/pkg").ensure_dir()
117        pkg.join("__init__.py").ensure()
118        pkg.join("test.py").ensure()
119        return testdir.tmpdir
120
121    @pytest.fixture
122    def invocation_path(self, invocation_dir: py.path.local) -> Path:
123        return Path(str(invocation_dir))
124
125    def test_file(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
126        """File and parts."""
127        assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == (
128            invocation_dir / "src/pkg/test.py",
129            [],
130        )
131        assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == (
132            invocation_dir / "src/pkg/test.py",
133            [""],
134        )
135        assert resolve_collection_argument(
136            invocation_path, "src/pkg/test.py::foo::bar"
137        ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"])
138        assert resolve_collection_argument(
139            invocation_path, "src/pkg/test.py::foo::bar::"
140        ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar", ""])
141
142    def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
143        """Directory and parts."""
144        assert resolve_collection_argument(invocation_path, "src/pkg") == (
145            invocation_dir / "src/pkg",
146            [],
147        )
148
149        with pytest.raises(
150            UsageError, match=r"directory argument cannot contain :: selection parts"
151        ):
152            resolve_collection_argument(invocation_path, "src/pkg::")
153
154        with pytest.raises(
155            UsageError, match=r"directory argument cannot contain :: selection parts"
156        ):
157            resolve_collection_argument(invocation_path, "src/pkg::foo::bar")
158
159    def test_pypath(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
160        """Dotted name and parts."""
161        assert resolve_collection_argument(
162            invocation_path, "pkg.test", as_pypath=True
163        ) == (invocation_dir / "src/pkg/test.py", [])
164        assert resolve_collection_argument(
165            invocation_path, "pkg.test::foo::bar", as_pypath=True
166        ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"])
167        assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == (
168            invocation_dir / "src/pkg",
169            [],
170        )
171
172        with pytest.raises(
173            UsageError, match=r"package argument cannot contain :: selection parts"
174        ):
175            resolve_collection_argument(
176                invocation_path, "pkg::foo::bar", as_pypath=True
177            )
178
179    def test_does_not_exist(self, invocation_path: Path) -> None:
180        """Given a file/module that does not exist raises UsageError."""
181        with pytest.raises(
182            UsageError, match=re.escape("file or directory not found: foobar")
183        ):
184            resolve_collection_argument(invocation_path, "foobar")
185
186        with pytest.raises(
187            UsageError,
188            match=re.escape(
189                "module or package not found: foobar (missing __init__.py?)"
190            ),
191        ):
192            resolve_collection_argument(invocation_path, "foobar", as_pypath=True)
193
194    def test_absolute_paths_are_resolved_correctly(
195        self, invocation_dir: py.path.local, invocation_path: Path
196    ) -> None:
197        """Absolute paths resolve back to absolute paths."""
198        full_path = str(invocation_dir / "src")
199        assert resolve_collection_argument(invocation_path, full_path) == (
200            py.path.local(os.path.abspath("src")),
201            [],
202        )
203
204        # ensure full paths given in the command-line without the drive letter resolve
205        # to the full path correctly (#7628)
206        drive, full_path_without_drive = os.path.splitdrive(full_path)
207        assert resolve_collection_argument(
208            invocation_path, full_path_without_drive
209        ) == (py.path.local(os.path.abspath("src")), [])
210
211
212def test_module_full_path_without_drive(testdir):
213    """Collect and run test using full path except for the drive letter (#7628).
214
215    Passing a full path without a drive letter would trigger a bug in py.path.local
216    where it would keep the full path without the drive letter around, instead of resolving
217    to the full path, resulting in fixtures node ids not matching against test node ids correctly.
218    """
219    testdir.makepyfile(
220        **{
221            "project/conftest.py": """
222                import pytest
223                @pytest.fixture
224                def fix(): return 1
225            """,
226        }
227    )
228
229    testdir.makepyfile(
230        **{
231            "project/tests/dummy_test.py": """
232                def test(fix):
233                    assert fix == 1
234            """
235        }
236    )
237    fn = testdir.tmpdir.join("project/tests/dummy_test.py")
238    assert fn.isfile()
239
240    drive, path = os.path.splitdrive(str(fn))
241
242    result = testdir.runpytest(path, "-v")
243    result.stdout.fnmatch_lines(
244        [
245            os.path.join("project", "tests", "dummy_test.py") + "::test PASSED *",
246            "* 1 passed in *",
247        ]
248    )
249