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