Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/pytester.py: 27%

799 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1"""(Disabled by default) support for testing pytest and pytest plugins. 

2 

3PYTEST_DONT_REWRITE 

4""" 

5import collections.abc 

6import contextlib 

7import gc 

8import importlib 

9import os 

10import platform 

11import re 

12import shutil 

13import subprocess 

14import sys 

15import traceback 

16from fnmatch import fnmatch 

17from io import StringIO 

18from pathlib import Path 

19from typing import Any 

20from typing import Callable 

21from typing import Dict 

22from typing import Generator 

23from typing import IO 

24from typing import Iterable 

25from typing import List 

26from typing import Optional 

27from typing import overload 

28from typing import Sequence 

29from typing import TextIO 

30from typing import Tuple 

31from typing import Type 

32from typing import TYPE_CHECKING 

33from typing import Union 

34from weakref import WeakKeyDictionary 

35 

36from iniconfig import IniConfig 

37from iniconfig import SectionWrapper 

38 

39from _pytest import timing 

40from _pytest._code import Source 

41from _pytest.capture import _get_multicapture 

42from _pytest.compat import final 

43from _pytest.compat import NOTSET 

44from _pytest.compat import NotSetType 

45from _pytest.config import _PluggyPlugin 

46from _pytest.config import Config 

47from _pytest.config import ExitCode 

48from _pytest.config import hookimpl 

49from _pytest.config import main 

50from _pytest.config import PytestPluginManager 

51from _pytest.config.argparsing import Parser 

52from _pytest.deprecated import check_ispytest 

53from _pytest.fixtures import fixture 

54from _pytest.fixtures import FixtureRequest 

55from _pytest.main import Session 

56from _pytest.monkeypatch import MonkeyPatch 

57from _pytest.nodes import Collector 

58from _pytest.nodes import Item 

59from _pytest.outcomes import fail 

60from _pytest.outcomes import importorskip 

61from _pytest.outcomes import skip 

62from _pytest.pathlib import bestrelpath 

63from _pytest.pathlib import copytree 

64from _pytest.pathlib import make_numbered_dir 

65from _pytest.reports import CollectReport 

66from _pytest.reports import TestReport 

67from _pytest.tmpdir import TempPathFactory 

68from _pytest.warning_types import PytestWarning 

69 

70 

71if TYPE_CHECKING: 

72 from typing_extensions import Final 

73 from typing_extensions import Literal 

74 

75 import pexpect 

76 

77 

78pytest_plugins = ["pytester_assertions"] 

79 

80 

81IGNORE_PAM = [ # filenames added when obtaining details about the current user 

82 "/var/lib/sss/mc/passwd" 

83] 

84 

85 

86def pytest_addoption(parser: Parser) -> None: 

87 parser.addoption( 

88 "--lsof", 

89 action="store_true", 

90 dest="lsof", 

91 default=False, 

92 help="Run FD checks if lsof is available", 

93 ) 

94 

95 parser.addoption( 

96 "--runpytest", 

97 default="inprocess", 

98 dest="runpytest", 

99 choices=("inprocess", "subprocess"), 

100 help=( 

101 "Run pytest sub runs in tests using an 'inprocess' " 

102 "or 'subprocess' (python -m main) method" 

103 ), 

104 ) 

105 

106 parser.addini( 

107 "pytester_example_dir", help="Directory to take the pytester example files from" 

108 ) 

109 

110 

111def pytest_configure(config: Config) -> None: 

112 if config.getvalue("lsof"): 

113 checker = LsofFdLeakChecker() 

114 if checker.matching_platform(): 

115 config.pluginmanager.register(checker) 

116 

117 config.addinivalue_line( 

118 "markers", 

119 "pytester_example_path(*path_segments): join the given path " 

120 "segments to `pytester_example_dir` for this test.", 

121 ) 

122 

123 

124class LsofFdLeakChecker: 

125 def get_open_files(self) -> List[Tuple[str, str]]: 

126 out = subprocess.run( 

127 ("lsof", "-Ffn0", "-p", str(os.getpid())), 

128 stdout=subprocess.PIPE, 

129 stderr=subprocess.DEVNULL, 

130 check=True, 

131 text=True, 

132 ).stdout 

133 

134 def isopen(line: str) -> bool: 

135 return line.startswith("f") and ( 

136 "deleted" not in line 

137 and "mem" not in line 

138 and "txt" not in line 

139 and "cwd" not in line 

140 ) 

141 

142 open_files = [] 

143 

144 for line in out.split("\n"): 

145 if isopen(line): 

146 fields = line.split("\0") 

147 fd = fields[0][1:] 

148 filename = fields[1][1:] 

149 if filename in IGNORE_PAM: 

150 continue 

151 if filename.startswith("/"): 

152 open_files.append((fd, filename)) 

153 

154 return open_files 

155 

156 def matching_platform(self) -> bool: 

157 try: 

158 subprocess.run(("lsof", "-v"), check=True) 

159 except (OSError, subprocess.CalledProcessError): 

160 return False 

161 else: 

162 return True 

163 

164 @hookimpl(hookwrapper=True, tryfirst=True) 

165 def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: 

166 lines1 = self.get_open_files() 

167 yield 

168 if hasattr(sys, "pypy_version_info"): 

169 gc.collect() 

170 lines2 = self.get_open_files() 

171 

172 new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} 

173 leaked_files = [t for t in lines2 if t[0] in new_fds] 

174 if leaked_files: 

175 error = [ 

176 "***** %s FD leakage detected" % len(leaked_files), 

177 *(str(f) for f in leaked_files), 

178 "*** Before:", 

179 *(str(f) for f in lines1), 

180 "*** After:", 

181 *(str(f) for f in lines2), 

182 "***** %s FD leakage detected" % len(leaked_files), 

183 "*** function %s:%s: %s " % item.location, 

184 "See issue #2366", 

185 ] 

186 item.warn(PytestWarning("\n".join(error))) 

187 

188 

189# used at least by pytest-xdist plugin 

190 

191 

192@fixture 

193def _pytest(request: FixtureRequest) -> "PytestArg": 

194 """Return a helper which offers a gethookrecorder(hook) method which 

195 returns a HookRecorder instance which helps to make assertions about called 

196 hooks.""" 

197 return PytestArg(request) 

198 

199 

200class PytestArg: 

201 def __init__(self, request: FixtureRequest) -> None: 

202 self._request = request 

203 

204 def gethookrecorder(self, hook) -> "HookRecorder": 

205 hookrecorder = HookRecorder(hook._pm) 

206 self._request.addfinalizer(hookrecorder.finish_recording) 

207 return hookrecorder 

208 

209 

210def get_public_names(values: Iterable[str]) -> List[str]: 

211 """Only return names from iterator values without a leading underscore.""" 

212 return [x for x in values if x[0] != "_"] 

213 

214 

215@final 

216class RecordedHookCall: 

217 """A recorded call to a hook. 

218 

219 The arguments to the hook call are set as attributes. 

220 For example: 

221 

222 .. code-block:: python 

223 

224 calls = hook_recorder.getcalls("pytest_runtest_setup") 

225 # Suppose pytest_runtest_setup was called once with `item=an_item`. 

226 assert calls[0].item is an_item 

227 """ 

228 

229 def __init__(self, name: str, kwargs) -> None: 

230 self.__dict__.update(kwargs) 

231 self._name = name 

232 

233 def __repr__(self) -> str: 

234 d = self.__dict__.copy() 

235 del d["_name"] 

236 return f"<RecordedHookCall {self._name!r}(**{d!r})>" 

237 

238 if TYPE_CHECKING: 

239 # The class has undetermined attributes, this tells mypy about it. 

240 def __getattr__(self, key: str): 

241 ... 

242 

243 

244@final 

245class HookRecorder: 

246 """Record all hooks called in a plugin manager. 

247 

248 Hook recorders are created by :class:`Pytester`. 

249 

250 This wraps all the hook calls in the plugin manager, recording each call 

251 before propagating the normal calls. 

252 """ 

253 

254 def __init__( 

255 self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False 

256 ) -> None: 

257 check_ispytest(_ispytest) 

258 

259 self._pluginmanager = pluginmanager 

260 self.calls: List[RecordedHookCall] = [] 

261 self.ret: Optional[Union[int, ExitCode]] = None 

262 

263 def before(hook_name: str, hook_impls, kwargs) -> None: 

264 self.calls.append(RecordedHookCall(hook_name, kwargs)) 

265 

266 def after(outcome, hook_name: str, hook_impls, kwargs) -> None: 

267 pass 

268 

269 self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after) 

270 

271 def finish_recording(self) -> None: 

272 self._undo_wrapping() 

273 

274 def getcalls(self, names: Union[str, Iterable[str]]) -> List[RecordedHookCall]: 

275 """Get all recorded calls to hooks with the given names (or name).""" 

276 if isinstance(names, str): 

277 names = names.split() 

278 return [call for call in self.calls if call._name in names] 

279 

280 def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None: 

281 __tracebackhide__ = True 

282 i = 0 

283 entries = list(entries) 

284 backlocals = sys._getframe(1).f_locals 

285 while entries: 

286 name, check = entries.pop(0) 

287 for ind, call in enumerate(self.calls[i:]): 

288 if call._name == name: 

289 print("NAMEMATCH", name, call) 

290 if eval(check, backlocals, call.__dict__): 

291 print("CHECKERMATCH", repr(check), "->", call) 

292 else: 

293 print("NOCHECKERMATCH", repr(check), "-", call) 

294 continue 

295 i += ind + 1 

296 break 

297 print("NONAMEMATCH", name, "with", call) 

298 else: 

299 fail(f"could not find {name!r} check {check!r}") 

300 

301 def popcall(self, name: str) -> RecordedHookCall: 

302 __tracebackhide__ = True 

303 for i, call in enumerate(self.calls): 

304 if call._name == name: 

305 del self.calls[i] 

306 return call 

307 lines = [f"could not find call {name!r}, in:"] 

308 lines.extend([" %s" % x for x in self.calls]) 

309 fail("\n".join(lines)) 

310 

311 def getcall(self, name: str) -> RecordedHookCall: 

312 values = self.getcalls(name) 

313 assert len(values) == 1, (name, values) 

314 return values[0] 

315 

316 # functionality for test reports 

317 

318 @overload 

319 def getreports( 

320 self, 

321 names: "Literal['pytest_collectreport']", 

322 ) -> Sequence[CollectReport]: 

323 ... 

324 

325 @overload 

326 def getreports( 

327 self, 

328 names: "Literal['pytest_runtest_logreport']", 

329 ) -> Sequence[TestReport]: 

330 ... 

331 

332 @overload 

333 def getreports( 

334 self, 

335 names: Union[str, Iterable[str]] = ( 

336 "pytest_collectreport", 

337 "pytest_runtest_logreport", 

338 ), 

339 ) -> Sequence[Union[CollectReport, TestReport]]: 

340 ... 

341 

342 def getreports( 

343 self, 

344 names: Union[str, Iterable[str]] = ( 

345 "pytest_collectreport", 

346 "pytest_runtest_logreport", 

347 ), 

348 ) -> Sequence[Union[CollectReport, TestReport]]: 

349 return [x.report for x in self.getcalls(names)] 

350 

351 def matchreport( 

352 self, 

353 inamepart: str = "", 

354 names: Union[str, Iterable[str]] = ( 

355 "pytest_runtest_logreport", 

356 "pytest_collectreport", 

357 ), 

358 when: Optional[str] = None, 

359 ) -> Union[CollectReport, TestReport]: 

360 """Return a testreport whose dotted import path matches.""" 

361 values = [] 

362 for rep in self.getreports(names=names): 

363 if not when and rep.when != "call" and rep.passed: 

364 # setup/teardown passing reports - let's ignore those 

365 continue 

366 if when and rep.when != when: 

367 continue 

368 if not inamepart or inamepart in rep.nodeid.split("::"): 

369 values.append(rep) 

370 if not values: 

371 raise ValueError( 

372 "could not find test report matching %r: " 

373 "no test reports at all!" % (inamepart,) 

374 ) 

375 if len(values) > 1: 

376 raise ValueError( 

377 "found 2 or more testreports matching {!r}: {}".format( 

378 inamepart, values 

379 ) 

380 ) 

381 return values[0] 

382 

383 @overload 

384 def getfailures( 

385 self, 

386 names: "Literal['pytest_collectreport']", 

387 ) -> Sequence[CollectReport]: 

388 ... 

389 

390 @overload 

391 def getfailures( 

392 self, 

393 names: "Literal['pytest_runtest_logreport']", 

394 ) -> Sequence[TestReport]: 

395 ... 

396 

397 @overload 

398 def getfailures( 

399 self, 

400 names: Union[str, Iterable[str]] = ( 

401 "pytest_collectreport", 

402 "pytest_runtest_logreport", 

403 ), 

404 ) -> Sequence[Union[CollectReport, TestReport]]: 

405 ... 

406 

407 def getfailures( 

408 self, 

409 names: Union[str, Iterable[str]] = ( 

410 "pytest_collectreport", 

411 "pytest_runtest_logreport", 

412 ), 

413 ) -> Sequence[Union[CollectReport, TestReport]]: 

414 return [rep for rep in self.getreports(names) if rep.failed] 

415 

416 def getfailedcollections(self) -> Sequence[CollectReport]: 

417 return self.getfailures("pytest_collectreport") 

418 

419 def listoutcomes( 

420 self, 

421 ) -> Tuple[ 

422 Sequence[TestReport], 

423 Sequence[Union[CollectReport, TestReport]], 

424 Sequence[Union[CollectReport, TestReport]], 

425 ]: 

426 passed = [] 

427 skipped = [] 

428 failed = [] 

429 for rep in self.getreports( 

430 ("pytest_collectreport", "pytest_runtest_logreport") 

431 ): 

432 if rep.passed: 

433 if rep.when == "call": 

434 assert isinstance(rep, TestReport) 

435 passed.append(rep) 

436 elif rep.skipped: 

437 skipped.append(rep) 

438 else: 

439 assert rep.failed, f"Unexpected outcome: {rep!r}" 

440 failed.append(rep) 

441 return passed, skipped, failed 

442 

443 def countoutcomes(self) -> List[int]: 

444 return [len(x) for x in self.listoutcomes()] 

445 

446 def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: 

447 __tracebackhide__ = True 

448 from _pytest.pytester_assertions import assertoutcome 

449 

450 outcomes = self.listoutcomes() 

451 assertoutcome( 

452 outcomes, 

453 passed=passed, 

454 skipped=skipped, 

455 failed=failed, 

456 ) 

457 

458 def clear(self) -> None: 

459 self.calls[:] = [] 

460 

461 

462@fixture 

463def linecomp() -> "LineComp": 

464 """A :class: `LineComp` instance for checking that an input linearly 

465 contains a sequence of strings.""" 

466 return LineComp() 

467 

468 

469@fixture(name="LineMatcher") 

470def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: 

471 """A reference to the :class: `LineMatcher`. 

472 

473 This is instantiable with a list of lines (without their trailing newlines). 

474 This is useful for testing large texts, such as the output of commands. 

475 """ 

476 return LineMatcher 

477 

478 

479@fixture 

480def pytester( 

481 request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch 

482) -> "Pytester": 

483 """ 

484 Facilities to write tests/configuration files, execute pytest in isolation, and match 

485 against expected output, perfect for black-box testing of pytest plugins. 

486 

487 It attempts to isolate the test run from external factors as much as possible, modifying 

488 the current working directory to ``path`` and environment variables during initialization. 

489 

490 It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path` 

491 fixture but provides methods which aid in testing pytest itself. 

492 """ 

493 return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True) 

494 

495 

496@fixture 

497def _sys_snapshot() -> Generator[None, None, None]: 

498 snappaths = SysPathsSnapshot() 

499 snapmods = SysModulesSnapshot() 

500 yield 

501 snapmods.restore() 

502 snappaths.restore() 

503 

504 

505@fixture 

506def _config_for_test() -> Generator[Config, None, None]: 

507 from _pytest.config import get_config 

508 

509 config = get_config() 

510 yield config 

511 config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles. 

512 

513 

514# Regex to match the session duration string in the summary: "74.34s". 

515rex_session_duration = re.compile(r"\d+\.\d\ds") 

516# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped". 

517rex_outcome = re.compile(r"(\d+) (\w+)") 

518 

519 

520@final 

521class RunResult: 

522 """The result of running a command from :class:`~pytest.Pytester`.""" 

523 

524 def __init__( 

525 self, 

526 ret: Union[int, ExitCode], 

527 outlines: List[str], 

528 errlines: List[str], 

529 duration: float, 

530 ) -> None: 

531 try: 

532 self.ret: Union[int, ExitCode] = ExitCode(ret) 

533 """The return value.""" 

534 except ValueError: 

535 self.ret = ret 

536 self.outlines = outlines 

537 """List of lines captured from stdout.""" 

538 self.errlines = errlines 

539 """List of lines captured from stderr.""" 

540 self.stdout = LineMatcher(outlines) 

541 """:class:`~pytest.LineMatcher` of stdout. 

542 

543 Use e.g. :func:`str(stdout) <pytest.LineMatcher.__str__()>` to reconstruct stdout, or the commonly used 

544 :func:`stdout.fnmatch_lines() <pytest.LineMatcher.fnmatch_lines()>` method. 

545 """ 

546 self.stderr = LineMatcher(errlines) 

547 """:class:`~pytest.LineMatcher` of stderr.""" 

548 self.duration = duration 

549 """Duration in seconds.""" 

550 

551 def __repr__(self) -> str: 

552 return ( 

553 "<RunResult ret=%s len(stdout.lines)=%d len(stderr.lines)=%d duration=%.2fs>" 

554 % (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration) 

555 ) 

556 

557 def parseoutcomes(self) -> Dict[str, int]: 

558 """Return a dictionary of outcome noun -> count from parsing the terminal 

559 output that the test process produced. 

560 

561 The returned nouns will always be in plural form:: 

562 

563 ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== 

564 

565 Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``. 

566 """ 

567 return self.parse_summary_nouns(self.outlines) 

568 

569 @classmethod 

570 def parse_summary_nouns(cls, lines) -> Dict[str, int]: 

571 """Extract the nouns from a pytest terminal summary line. 

572 

573 It always returns the plural noun for consistency:: 

574 

575 ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== 

576 

577 Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``. 

578 """ 

579 for line in reversed(lines): 

580 if rex_session_duration.search(line): 

581 outcomes = rex_outcome.findall(line) 

582 ret = {noun: int(count) for (count, noun) in outcomes} 

583 break 

584 else: 

585 raise ValueError("Pytest terminal summary report not found") 

586 

587 to_plural = { 

588 "warning": "warnings", 

589 "error": "errors", 

590 } 

591 return {to_plural.get(k, k): v for k, v in ret.items()} 

592 

593 def assert_outcomes( 

594 self, 

595 passed: int = 0, 

596 skipped: int = 0, 

597 failed: int = 0, 

598 errors: int = 0, 

599 xpassed: int = 0, 

600 xfailed: int = 0, 

601 warnings: Optional[int] = None, 

602 deselected: Optional[int] = None, 

603 ) -> None: 

604 """ 

605 Assert that the specified outcomes appear with the respective 

606 numbers (0 means it didn't occur) in the text output from a test run. 

607 

608 ``warnings`` and ``deselected`` are only checked if not None. 

609 """ 

610 __tracebackhide__ = True 

611 from _pytest.pytester_assertions import assert_outcomes 

612 

613 outcomes = self.parseoutcomes() 

614 assert_outcomes( 

615 outcomes, 

616 passed=passed, 

617 skipped=skipped, 

618 failed=failed, 

619 errors=errors, 

620 xpassed=xpassed, 

621 xfailed=xfailed, 

622 warnings=warnings, 

623 deselected=deselected, 

624 ) 

625 

626 

627class CwdSnapshot: 

628 def __init__(self) -> None: 

629 self.__saved = os.getcwd() 

630 

631 def restore(self) -> None: 

632 os.chdir(self.__saved) 

633 

634 

635class SysModulesSnapshot: 

636 def __init__(self, preserve: Optional[Callable[[str], bool]] = None) -> None: 

637 self.__preserve = preserve 

638 self.__saved = dict(sys.modules) 

639 

640 def restore(self) -> None: 

641 if self.__preserve: 

642 self.__saved.update( 

643 (k, m) for k, m in sys.modules.items() if self.__preserve(k) 

644 ) 

645 sys.modules.clear() 

646 sys.modules.update(self.__saved) 

647 

648 

649class SysPathsSnapshot: 

650 def __init__(self) -> None: 

651 self.__saved = list(sys.path), list(sys.meta_path) 

652 

653 def restore(self) -> None: 

654 sys.path[:], sys.meta_path[:] = self.__saved 

655 

656 

657@final 

658class Pytester: 

659 """ 

660 Facilities to write tests/configuration files, execute pytest in isolation, and match 

661 against expected output, perfect for black-box testing of pytest plugins. 

662 

663 It attempts to isolate the test run from external factors as much as possible, modifying 

664 the current working directory to :attr:`path` and environment variables during initialization. 

665 """ 

666 

667 __test__ = False 

668 

669 CLOSE_STDIN: "Final" = NOTSET 

670 

671 class TimeoutExpired(Exception): 

672 pass 

673 

674 def __init__( 

675 self, 

676 request: FixtureRequest, 

677 tmp_path_factory: TempPathFactory, 

678 monkeypatch: MonkeyPatch, 

679 *, 

680 _ispytest: bool = False, 

681 ) -> None: 

682 check_ispytest(_ispytest) 

683 self._request = request 

684 self._mod_collections: WeakKeyDictionary[ 

685 Collector, List[Union[Item, Collector]] 

686 ] = WeakKeyDictionary() 

687 if request.function: 

688 name: str = request.function.__name__ 

689 else: 

690 name = request.node.name 

691 self._name = name 

692 self._path: Path = tmp_path_factory.mktemp(name, numbered=True) 

693 #: A list of plugins to use with :py:meth:`parseconfig` and 

694 #: :py:meth:`runpytest`. Initially this is an empty list but plugins can 

695 #: be added to the list. The type of items to add to the list depends on 

696 #: the method using them so refer to them for details. 

697 self.plugins: List[Union[str, _PluggyPlugin]] = [] 

698 self._cwd_snapshot = CwdSnapshot() 

699 self._sys_path_snapshot = SysPathsSnapshot() 

700 self._sys_modules_snapshot = self.__take_sys_modules_snapshot() 

701 self.chdir() 

702 self._request.addfinalizer(self._finalize) 

703 self._method = self._request.config.getoption("--runpytest") 

704 self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) 

705 

706 self._monkeypatch = mp = monkeypatch 

707 mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot)) 

708 # Ensure no unexpected caching via tox. 

709 mp.delenv("TOX_ENV_DIR", raising=False) 

710 # Discard outer pytest options. 

711 mp.delenv("PYTEST_ADDOPTS", raising=False) 

712 # Ensure no user config is used. 

713 tmphome = str(self.path) 

714 mp.setenv("HOME", tmphome) 

715 mp.setenv("USERPROFILE", tmphome) 

716 # Do not use colors for inner runs by default. 

717 mp.setenv("PY_COLORS", "0") 

718 

719 @property 

720 def path(self) -> Path: 

721 """Temporary directory path used to create files/run tests from, etc.""" 

722 return self._path 

723 

724 def __repr__(self) -> str: 

725 return f"<Pytester {self.path!r}>" 

726 

727 def _finalize(self) -> None: 

728 """ 

729 Clean up global state artifacts. 

730 

731 Some methods modify the global interpreter state and this tries to 

732 clean this up. It does not remove the temporary directory however so 

733 it can be looked at after the test run has finished. 

734 """ 

735 self._sys_modules_snapshot.restore() 

736 self._sys_path_snapshot.restore() 

737 self._cwd_snapshot.restore() 

738 

739 def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: 

740 # Some zope modules used by twisted-related tests keep internal state 

741 # and can't be deleted; we had some trouble in the past with 

742 # `zope.interface` for example. 

743 # 

744 # Preserve readline due to https://bugs.python.org/issue41033. 

745 # pexpect issues a SIGWINCH. 

746 def preserve_module(name): 

747 return name.startswith(("zope", "readline")) 

748 

749 return SysModulesSnapshot(preserve=preserve_module) 

750 

751 def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: 

752 """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`.""" 

753 pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) 

754 self._request.addfinalizer(reprec.finish_recording) 

755 return reprec 

756 

757 def chdir(self) -> None: 

758 """Cd into the temporary directory. 

759 

760 This is done automatically upon instantiation. 

761 """ 

762 os.chdir(self.path) 

763 

764 def _makefile( 

765 self, 

766 ext: str, 

767 lines: Sequence[Union[Any, bytes]], 

768 files: Dict[str, str], 

769 encoding: str = "utf-8", 

770 ) -> Path: 

771 items = list(files.items()) 

772 

773 if ext and not ext.startswith("."): 

774 raise ValueError( 

775 f"pytester.makefile expects a file extension, try .{ext} instead of {ext}" 

776 ) 

777 

778 def to_text(s: Union[Any, bytes]) -> str: 

779 return s.decode(encoding) if isinstance(s, bytes) else str(s) 

780 

781 if lines: 

782 source = "\n".join(to_text(x) for x in lines) 

783 basename = self._name 

784 items.insert(0, (basename, source)) 

785 

786 ret = None 

787 for basename, value in items: 

788 p = self.path.joinpath(basename).with_suffix(ext) 

789 p.parent.mkdir(parents=True, exist_ok=True) 

790 source_ = Source(value) 

791 source = "\n".join(to_text(line) for line in source_.lines) 

792 p.write_text(source.strip(), encoding=encoding) 

793 if ret is None: 

794 ret = p 

795 assert ret is not None 

796 return ret 

797 

798 def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: 

799 r"""Create new text file(s) in the test directory. 

800 

801 :param ext: 

802 The extension the file(s) should use, including the dot, e.g. `.py`. 

803 :param args: 

804 All args are treated as strings and joined using newlines. 

805 The result is written as contents to the file. The name of the 

806 file is based on the test function requesting this fixture. 

807 :param kwargs: 

808 Each keyword is the name of a file, while the value of it will 

809 be written as contents of the file. 

810 :returns: 

811 The first created file. 

812 

813 Examples: 

814 

815 .. code-block:: python 

816 

817 pytester.makefile(".txt", "line1", "line2") 

818 

819 pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") 

820 

821 To create binary files, use :meth:`pathlib.Path.write_bytes` directly: 

822 

823 .. code-block:: python 

824 

825 filename = pytester.path.joinpath("foo.bin") 

826 filename.write_bytes(b"...") 

827 """ 

828 return self._makefile(ext, args, kwargs) 

829 

830 def makeconftest(self, source: str) -> Path: 

831 """Write a contest.py file. 

832 

833 :param source: The contents. 

834 :returns: The conftest.py file. 

835 """ 

836 return self.makepyfile(conftest=source) 

837 

838 def makeini(self, source: str) -> Path: 

839 """Write a tox.ini file. 

840 

841 :param source: The contents. 

842 :returns: The tox.ini file. 

843 """ 

844 return self.makefile(".ini", tox=source) 

845 

846 def getinicfg(self, source: str) -> SectionWrapper: 

847 """Return the pytest section from the tox.ini config file.""" 

848 p = self.makeini(source) 

849 return IniConfig(str(p))["pytest"] 

850 

851 def makepyprojecttoml(self, source: str) -> Path: 

852 """Write a pyproject.toml file. 

853 

854 :param source: The contents. 

855 :returns: The pyproject.ini file. 

856 

857 .. versionadded:: 6.0 

858 """ 

859 return self.makefile(".toml", pyproject=source) 

860 

861 def makepyfile(self, *args, **kwargs) -> Path: 

862 r"""Shortcut for .makefile() with a .py extension. 

863 

864 Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting 

865 existing files. 

866 

867 Examples: 

868 

869 .. code-block:: python 

870 

871 def test_something(pytester): 

872 # Initial file is created test_something.py. 

873 pytester.makepyfile("foobar") 

874 # To create multiple files, pass kwargs accordingly. 

875 pytester.makepyfile(custom="foobar") 

876 # At this point, both 'test_something.py' & 'custom.py' exist in the test directory. 

877 

878 """ 

879 return self._makefile(".py", args, kwargs) 

880 

881 def maketxtfile(self, *args, **kwargs) -> Path: 

882 r"""Shortcut for .makefile() with a .txt extension. 

883 

884 Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting 

885 existing files. 

886 

887 Examples: 

888 

889 .. code-block:: python 

890 

891 def test_something(pytester): 

892 # Initial file is created test_something.txt. 

893 pytester.maketxtfile("foobar") 

894 # To create multiple files, pass kwargs accordingly. 

895 pytester.maketxtfile(custom="foobar") 

896 # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory. 

897 

898 """ 

899 return self._makefile(".txt", args, kwargs) 

900 

901 def syspathinsert( 

902 self, path: Optional[Union[str, "os.PathLike[str]"]] = None 

903 ) -> None: 

904 """Prepend a directory to sys.path, defaults to :attr:`path`. 

905 

906 This is undone automatically when this object dies at the end of each 

907 test. 

908 

909 :param path: 

910 The path. 

911 """ 

912 if path is None: 

913 path = self.path 

914 

915 self._monkeypatch.syspath_prepend(str(path)) 

916 

917 def mkdir(self, name: Union[str, "os.PathLike[str]"]) -> Path: 

918 """Create a new (sub)directory. 

919 

920 :param name: 

921 The name of the directory, relative to the pytester path. 

922 :returns: 

923 The created directory. 

924 """ 

925 p = self.path / name 

926 p.mkdir() 

927 return p 

928 

929 def mkpydir(self, name: Union[str, "os.PathLike[str]"]) -> Path: 

930 """Create a new python package. 

931 

932 This creates a (sub)directory with an empty ``__init__.py`` file so it 

933 gets recognised as a Python package. 

934 """ 

935 p = self.path / name 

936 p.mkdir() 

937 p.joinpath("__init__.py").touch() 

938 return p 

939 

940 def copy_example(self, name: Optional[str] = None) -> Path: 

941 """Copy file from project's directory into the testdir. 

942 

943 :param name: 

944 The name of the file to copy. 

945 :return: 

946 Path to the copied directory (inside ``self.path``). 

947 """ 

948 example_dir_ = self._request.config.getini("pytester_example_dir") 

949 if example_dir_ is None: 

950 raise ValueError("pytester_example_dir is unset, can't copy examples") 

951 example_dir: Path = self._request.config.rootpath / example_dir_ 

952 

953 for extra_element in self._request.node.iter_markers("pytester_example_path"): 

954 assert extra_element.args 

955 example_dir = example_dir.joinpath(*extra_element.args) 

956 

957 if name is None: 

958 func_name = self._name 

959 maybe_dir = example_dir / func_name 

960 maybe_file = example_dir / (func_name + ".py") 

961 

962 if maybe_dir.is_dir(): 

963 example_path = maybe_dir 

964 elif maybe_file.is_file(): 

965 example_path = maybe_file 

966 else: 

967 raise LookupError( 

968 f"{func_name} can't be found as module or package in {example_dir}" 

969 ) 

970 else: 

971 example_path = example_dir.joinpath(name) 

972 

973 if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): 

974 copytree(example_path, self.path) 

975 return self.path 

976 elif example_path.is_file(): 

977 result = self.path.joinpath(example_path.name) 

978 shutil.copy(example_path, result) 

979 return result 

980 else: 

981 raise LookupError( 

982 f'example "{example_path}" is not found as a file or directory' 

983 ) 

984 

985 def getnode( 

986 self, config: Config, arg: Union[str, "os.PathLike[str]"] 

987 ) -> Union[Collector, Item]: 

988 """Get the collection node of a file. 

989 

990 :param config: 

991 A pytest config. 

992 See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it. 

993 :param arg: 

994 Path to the file. 

995 :returns: 

996 The node. 

997 """ 

998 session = Session.from_config(config) 

999 assert "::" not in str(arg) 

1000 p = Path(os.path.abspath(arg)) 

1001 config.hook.pytest_sessionstart(session=session) 

1002 res = session.perform_collect([str(p)], genitems=False)[0] 

1003 config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) 

1004 return res 

1005 

1006 def getpathnode( 

1007 self, path: Union[str, "os.PathLike[str]"] 

1008 ) -> Union[Collector, Item]: 

1009 """Return the collection node of a file. 

1010 

1011 This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to 

1012 create the (configured) pytest Config instance. 

1013 

1014 :param path: 

1015 Path to the file. 

1016 :returns: 

1017 The node. 

1018 """ 

1019 path = Path(path) 

1020 config = self.parseconfigure(path) 

1021 session = Session.from_config(config) 

1022 x = bestrelpath(session.path, path) 

1023 config.hook.pytest_sessionstart(session=session) 

1024 res = session.perform_collect([x], genitems=False)[0] 

1025 config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) 

1026 return res 

1027 

1028 def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]: 

1029 """Generate all test items from a collection node. 

1030 

1031 This recurses into the collection node and returns a list of all the 

1032 test items contained within. 

1033 

1034 :param colitems: 

1035 The collection nodes. 

1036 :returns: 

1037 The collected items. 

1038 """ 

1039 session = colitems[0].session 

1040 result: List[Item] = [] 

1041 for colitem in colitems: 

1042 result.extend(session.genitems(colitem)) 

1043 return result 

1044 

1045 def runitem(self, source: str) -> Any: 

1046 """Run the "test_func" Item. 

1047 

1048 The calling test instance (class containing the test method) must 

1049 provide a ``.getrunner()`` method which should return a runner which 

1050 can run the test protocol for a single item, e.g. 

1051 :py:func:`_pytest.runner.runtestprotocol`. 

1052 """ 

1053 # used from runner functional tests 

1054 item = self.getitem(source) 

1055 # the test class where we are called from wants to provide the runner 

1056 testclassinstance = self._request.instance 

1057 runner = testclassinstance.getrunner() 

1058 return runner(item) 

1059 

1060 def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder: 

1061 """Run a test module in process using ``pytest.main()``. 

1062 

1063 This run writes "source" into a temporary file and runs 

1064 ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance 

1065 for the result. 

1066 

1067 :param source: The source code of the test module. 

1068 :param cmdlineargs: Any extra command line arguments to use. 

1069 """ 

1070 p = self.makepyfile(source) 

1071 values = list(cmdlineargs) + [p] 

1072 return self.inline_run(*values) 

1073 

1074 def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]: 

1075 """Run ``pytest.main(['--collectonly'])`` in-process. 

1076 

1077 Runs the :py:func:`pytest.main` function to run all of pytest inside 

1078 the test process itself like :py:meth:`inline_run`, but returns a 

1079 tuple of the collected items and a :py:class:`HookRecorder` instance. 

1080 """ 

1081 rec = self.inline_run("--collect-only", *args) 

1082 items = [x.item for x in rec.getcalls("pytest_itemcollected")] 

1083 return items, rec 

1084 

1085 def inline_run( 

1086 self, 

1087 *args: Union[str, "os.PathLike[str]"], 

1088 plugins=(), 

1089 no_reraise_ctrlc: bool = False, 

1090 ) -> HookRecorder: 

1091 """Run ``pytest.main()`` in-process, returning a HookRecorder. 

1092 

1093 Runs the :py:func:`pytest.main` function to run all of pytest inside 

1094 the test process itself. This means it can return a 

1095 :py:class:`HookRecorder` instance which gives more detailed results 

1096 from that run than can be done by matching stdout/stderr from 

1097 :py:meth:`runpytest`. 

1098 

1099 :param args: 

1100 Command line arguments to pass to :py:func:`pytest.main`. 

1101 :param plugins: 

1102 Extra plugin instances the ``pytest.main()`` instance should use. 

1103 :param no_reraise_ctrlc: 

1104 Typically we reraise keyboard interrupts from the child run. If 

1105 True, the KeyboardInterrupt exception is captured. 

1106 """ 

1107 # (maybe a cpython bug?) the importlib cache sometimes isn't updated 

1108 # properly between file creation and inline_run (especially if imports 

1109 # are interspersed with file creation) 

1110 importlib.invalidate_caches() 

1111 

1112 plugins = list(plugins) 

1113 finalizers = [] 

1114 try: 

1115 # Any sys.module or sys.path changes done while running pytest 

1116 # inline should be reverted after the test run completes to avoid 

1117 # clashing with later inline tests run within the same pytest test, 

1118 # e.g. just because they use matching test module names. 

1119 finalizers.append(self.__take_sys_modules_snapshot().restore) 

1120 finalizers.append(SysPathsSnapshot().restore) 

1121 

1122 # Important note: 

1123 # - our tests should not leave any other references/registrations 

1124 # laying around other than possibly loaded test modules 

1125 # referenced from sys.modules, as nothing will clean those up 

1126 # automatically 

1127 

1128 rec = [] 

1129 

1130 class Collect: 

1131 def pytest_configure(x, config: Config) -> None: 

1132 rec.append(self.make_hook_recorder(config.pluginmanager)) 

1133 

1134 plugins.append(Collect()) 

1135 ret = main([str(x) for x in args], plugins=plugins) 

1136 if len(rec) == 1: 

1137 reprec = rec.pop() 

1138 else: 

1139 

1140 class reprec: # type: ignore 

1141 pass 

1142 

1143 reprec.ret = ret 

1144 

1145 # Typically we reraise keyboard interrupts from the child run 

1146 # because it's our user requesting interruption of the testing. 

1147 if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc: 

1148 calls = reprec.getcalls("pytest_keyboard_interrupt") 

1149 if calls and calls[-1].excinfo.type == KeyboardInterrupt: 

1150 raise KeyboardInterrupt() 

1151 return reprec 

1152 finally: 

1153 for finalizer in finalizers: 

1154 finalizer() 

1155 

1156 def runpytest_inprocess( 

1157 self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any 

1158 ) -> RunResult: 

1159 """Return result of running pytest in-process, providing a similar 

1160 interface to what self.runpytest() provides.""" 

1161 syspathinsert = kwargs.pop("syspathinsert", False) 

1162 

1163 if syspathinsert: 

1164 self.syspathinsert() 

1165 now = timing.time() 

1166 capture = _get_multicapture("sys") 

1167 capture.start_capturing() 

1168 try: 

1169 try: 

1170 reprec = self.inline_run(*args, **kwargs) 

1171 except SystemExit as e: 

1172 ret = e.args[0] 

1173 try: 

1174 ret = ExitCode(e.args[0]) 

1175 except ValueError: 

1176 pass 

1177 

1178 class reprec: # type: ignore 

1179 ret = ret 

1180 

1181 except Exception: 

1182 traceback.print_exc() 

1183 

1184 class reprec: # type: ignore 

1185 ret = ExitCode(3) 

1186 

1187 finally: 

1188 out, err = capture.readouterr() 

1189 capture.stop_capturing() 

1190 sys.stdout.write(out) 

1191 sys.stderr.write(err) 

1192 

1193 assert reprec.ret is not None 

1194 res = RunResult( 

1195 reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now 

1196 ) 

1197 res.reprec = reprec # type: ignore 

1198 return res 

1199 

1200 def runpytest( 

1201 self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any 

1202 ) -> RunResult: 

1203 """Run pytest inline or in a subprocess, depending on the command line 

1204 option "--runpytest" and return a :py:class:`~pytest.RunResult`.""" 

1205 new_args = self._ensure_basetemp(args) 

1206 if self._method == "inprocess": 

1207 return self.runpytest_inprocess(*new_args, **kwargs) 

1208 elif self._method == "subprocess": 

1209 return self.runpytest_subprocess(*new_args, **kwargs) 

1210 raise RuntimeError(f"Unrecognized runpytest option: {self._method}") 

1211 

1212 def _ensure_basetemp( 

1213 self, args: Sequence[Union[str, "os.PathLike[str]"]] 

1214 ) -> List[Union[str, "os.PathLike[str]"]]: 

1215 new_args = list(args) 

1216 for x in new_args: 

1217 if str(x).startswith("--basetemp"): 

1218 break 

1219 else: 

1220 new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp")) 

1221 return new_args 

1222 

1223 def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config: 

1224 """Return a new pytest :class:`pytest.Config` instance from given 

1225 commandline args. 

1226 

1227 This invokes the pytest bootstrapping code in _pytest.config to create a 

1228 new :py:class:`pytest.PytestPluginManager` and call the 

1229 :hook:`pytest_cmdline_parse` hook to create a new :class:`pytest.Config` 

1230 instance. 

1231 

1232 If :attr:`plugins` has been populated they should be plugin modules 

1233 to be registered with the plugin manager. 

1234 """ 

1235 import _pytest.config 

1236 

1237 new_args = self._ensure_basetemp(args) 

1238 new_args = [str(x) for x in new_args] 

1239 

1240 config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type] 

1241 # we don't know what the test will do with this half-setup config 

1242 # object and thus we make sure it gets unconfigured properly in any 

1243 # case (otherwise capturing could still be active, for example) 

1244 self._request.addfinalizer(config._ensure_unconfigure) 

1245 return config 

1246 

1247 def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config: 

1248 """Return a new pytest configured Config instance. 

1249 

1250 Returns a new :py:class:`pytest.Config` instance like 

1251 :py:meth:`parseconfig`, but also calls the :hook:`pytest_configure` 

1252 hook. 

1253 """ 

1254 config = self.parseconfig(*args) 

1255 config._do_configure() 

1256 return config 

1257 

1258 def getitem( 

1259 self, source: Union[str, "os.PathLike[str]"], funcname: str = "test_func" 

1260 ) -> Item: 

1261 """Return the test item for a test function. 

1262 

1263 Writes the source to a python file and runs pytest's collection on 

1264 the resulting module, returning the test item for the requested 

1265 function name. 

1266 

1267 :param source: 

1268 The module source. 

1269 :param funcname: 

1270 The name of the test function for which to return a test item. 

1271 :returns: 

1272 The test item. 

1273 """ 

1274 items = self.getitems(source) 

1275 for item in items: 

1276 if item.name == funcname: 

1277 return item 

1278 assert 0, "{!r} item not found in module:\n{}\nitems: {}".format( 

1279 funcname, source, items 

1280 ) 

1281 

1282 def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]: 

1283 """Return all test items collected from the module. 

1284 

1285 Writes the source to a Python file and runs pytest's collection on 

1286 the resulting module, returning all test items contained within. 

1287 """ 

1288 modcol = self.getmodulecol(source) 

1289 return self.genitems([modcol]) 

1290 

1291 def getmodulecol( 

1292 self, 

1293 source: Union[str, "os.PathLike[str]"], 

1294 configargs=(), 

1295 *, 

1296 withinit: bool = False, 

1297 ): 

1298 """Return the module collection node for ``source``. 

1299 

1300 Writes ``source`` to a file using :py:meth:`makepyfile` and then 

1301 runs the pytest collection on it, returning the collection node for the 

1302 test module. 

1303 

1304 :param source: 

1305 The source code of the module to collect. 

1306 

1307 :param configargs: 

1308 Any extra arguments to pass to :py:meth:`parseconfigure`. 

1309 

1310 :param withinit: 

1311 Whether to also write an ``__init__.py`` file to the same 

1312 directory to ensure it is a package. 

1313 """ 

1314 if isinstance(source, os.PathLike): 

1315 path = self.path.joinpath(source) 

1316 assert not withinit, "not supported for paths" 

1317 else: 

1318 kw = {self._name: str(source)} 

1319 path = self.makepyfile(**kw) 

1320 if withinit: 

1321 self.makepyfile(__init__="#") 

1322 self.config = config = self.parseconfigure(path, *configargs) 

1323 return self.getnode(config, path) 

1324 

1325 def collect_by_name( 

1326 self, modcol: Collector, name: str 

1327 ) -> Optional[Union[Item, Collector]]: 

1328 """Return the collection node for name from the module collection. 

1329 

1330 Searches a module collection node for a collection node matching the 

1331 given name. 

1332 

1333 :param modcol: A module collection node; see :py:meth:`getmodulecol`. 

1334 :param name: The name of the node to return. 

1335 """ 

1336 if modcol not in self._mod_collections: 

1337 self._mod_collections[modcol] = list(modcol.collect()) 

1338 for colitem in self._mod_collections[modcol]: 

1339 if colitem.name == name: 

1340 return colitem 

1341 return None 

1342 

1343 def popen( 

1344 self, 

1345 cmdargs: Sequence[Union[str, "os.PathLike[str]"]], 

1346 stdout: Union[int, TextIO] = subprocess.PIPE, 

1347 stderr: Union[int, TextIO] = subprocess.PIPE, 

1348 stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN, 

1349 **kw, 

1350 ): 

1351 """Invoke :py:class:`subprocess.Popen`. 

1352 

1353 Calls :py:class:`subprocess.Popen` making sure the current working 

1354 directory is in ``PYTHONPATH``. 

1355 

1356 You probably want to use :py:meth:`run` instead. 

1357 """ 

1358 env = os.environ.copy() 

1359 env["PYTHONPATH"] = os.pathsep.join( 

1360 filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) 

1361 ) 

1362 kw["env"] = env 

1363 

1364 if stdin is self.CLOSE_STDIN: 

1365 kw["stdin"] = subprocess.PIPE 

1366 elif isinstance(stdin, bytes): 

1367 kw["stdin"] = subprocess.PIPE 

1368 else: 

1369 kw["stdin"] = stdin 

1370 

1371 popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) 

1372 if stdin is self.CLOSE_STDIN: 

1373 assert popen.stdin is not None 

1374 popen.stdin.close() 

1375 elif isinstance(stdin, bytes): 

1376 assert popen.stdin is not None 

1377 popen.stdin.write(stdin) 

1378 

1379 return popen 

1380 

1381 def run( 

1382 self, 

1383 *cmdargs: Union[str, "os.PathLike[str]"], 

1384 timeout: Optional[float] = None, 

1385 stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN, 

1386 ) -> RunResult: 

1387 """Run a command with arguments. 

1388 

1389 Run a process using :py:class:`subprocess.Popen` saving the stdout and 

1390 stderr. 

1391 

1392 :param cmdargs: 

1393 The sequence of arguments to pass to :py:class:`subprocess.Popen`, 

1394 with path-like objects being converted to :py:class:`str` 

1395 automatically. 

1396 :param timeout: 

1397 The period in seconds after which to timeout and raise 

1398 :py:class:`Pytester.TimeoutExpired`. 

1399 :param stdin: 

1400 Optional standard input. 

1401 

1402 - If it is :py:attr:`CLOSE_STDIN` (Default), then this method calls 

1403 :py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and 

1404 the standard input is closed immediately after the new command is 

1405 started. 

1406 

1407 - If it is of type :py:class:`bytes`, these bytes are sent to the 

1408 standard input of the command. 

1409 

1410 - Otherwise, it is passed through to :py:class:`subprocess.Popen`. 

1411 For further information in this case, consult the document of the 

1412 ``stdin`` parameter in :py:class:`subprocess.Popen`. 

1413 :returns: 

1414 The result. 

1415 """ 

1416 __tracebackhide__ = True 

1417 

1418 cmdargs = tuple(os.fspath(arg) for arg in cmdargs) 

1419 p1 = self.path.joinpath("stdout") 

1420 p2 = self.path.joinpath("stderr") 

1421 print("running:", *cmdargs) 

1422 print(" in:", Path.cwd()) 

1423 

1424 with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2: 

1425 now = timing.time() 

1426 popen = self.popen( 

1427 cmdargs, 

1428 stdin=stdin, 

1429 stdout=f1, 

1430 stderr=f2, 

1431 close_fds=(sys.platform != "win32"), 

1432 ) 

1433 if popen.stdin is not None: 

1434 popen.stdin.close() 

1435 

1436 def handle_timeout() -> None: 

1437 __tracebackhide__ = True 

1438 

1439 timeout_message = ( 

1440 "{seconds} second timeout expired running:" 

1441 " {command}".format(seconds=timeout, command=cmdargs) 

1442 ) 

1443 

1444 popen.kill() 

1445 popen.wait() 

1446 raise self.TimeoutExpired(timeout_message) 

1447 

1448 if timeout is None: 

1449 ret = popen.wait() 

1450 else: 

1451 try: 

1452 ret = popen.wait(timeout) 

1453 except subprocess.TimeoutExpired: 

1454 handle_timeout() 

1455 

1456 with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2: 

1457 out = f1.read().splitlines() 

1458 err = f2.read().splitlines() 

1459 

1460 self._dump_lines(out, sys.stdout) 

1461 self._dump_lines(err, sys.stderr) 

1462 

1463 with contextlib.suppress(ValueError): 

1464 ret = ExitCode(ret) 

1465 return RunResult(ret, out, err, timing.time() - now) 

1466 

1467 def _dump_lines(self, lines, fp): 

1468 try: 

1469 for line in lines: 

1470 print(line, file=fp) 

1471 except UnicodeEncodeError: 

1472 print(f"couldn't print to {fp} because of encoding") 

1473 

1474 def _getpytestargs(self) -> Tuple[str, ...]: 

1475 return sys.executable, "-mpytest" 

1476 

1477 def runpython(self, script: "os.PathLike[str]") -> RunResult: 

1478 """Run a python script using sys.executable as interpreter.""" 

1479 return self.run(sys.executable, script) 

1480 

1481 def runpython_c(self, command: str) -> RunResult: 

1482 """Run ``python -c "command"``.""" 

1483 return self.run(sys.executable, "-c", command) 

1484 

1485 def runpytest_subprocess( 

1486 self, *args: Union[str, "os.PathLike[str]"], timeout: Optional[float] = None 

1487 ) -> RunResult: 

1488 """Run pytest as a subprocess with given arguments. 

1489 

1490 Any plugins added to the :py:attr:`plugins` list will be added using the 

1491 ``-p`` command line option. Additionally ``--basetemp`` is used to put 

1492 any temporary files and directories in a numbered directory prefixed 

1493 with "runpytest-" to not conflict with the normal numbered pytest 

1494 location for temporary files and directories. 

1495 

1496 :param args: 

1497 The sequence of arguments to pass to the pytest subprocess. 

1498 :param timeout: 

1499 The period in seconds after which to timeout and raise 

1500 :py:class:`Pytester.TimeoutExpired`. 

1501 :returns: 

1502 The result. 

1503 """ 

1504 __tracebackhide__ = True 

1505 p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) 

1506 args = ("--basetemp=%s" % p,) + args 

1507 plugins = [x for x in self.plugins if isinstance(x, str)] 

1508 if plugins: 

1509 args = ("-p", plugins[0]) + args 

1510 args = self._getpytestargs() + args 

1511 return self.run(*args, timeout=timeout) 

1512 

1513 def spawn_pytest( 

1514 self, string: str, expect_timeout: float = 10.0 

1515 ) -> "pexpect.spawn": 

1516 """Run pytest using pexpect. 

1517 

1518 This makes sure to use the right pytest and sets up the temporary 

1519 directory locations. 

1520 

1521 The pexpect child is returned. 

1522 """ 

1523 basetemp = self.path / "temp-pexpect" 

1524 basetemp.mkdir(mode=0o700) 

1525 invoke = " ".join(map(str, self._getpytestargs())) 

1526 cmd = f"{invoke} --basetemp={basetemp} {string}" 

1527 return self.spawn(cmd, expect_timeout=expect_timeout) 

1528 

1529 def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": 

1530 """Run a command using pexpect. 

1531 

1532 The pexpect child is returned. 

1533 """ 

1534 pexpect = importorskip("pexpect", "3.0") 

1535 if hasattr(sys, "pypy_version_info") and "64" in platform.machine(): 

1536 skip("pypy-64 bit not supported") 

1537 if not hasattr(pexpect, "spawn"): 

1538 skip("pexpect.spawn not available") 

1539 logfile = self.path.joinpath("spawn.out").open("wb") 

1540 

1541 child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout) 

1542 self._request.addfinalizer(logfile.close) 

1543 return child 

1544 

1545 

1546class LineComp: 

1547 def __init__(self) -> None: 

1548 self.stringio = StringIO() 

1549 """:class:`python:io.StringIO()` instance used for input.""" 

1550 

1551 def assert_contains_lines(self, lines2: Sequence[str]) -> None: 

1552 """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value. 

1553 

1554 Lines are matched using :func:`LineMatcher.fnmatch_lines <pytest.LineMatcher.fnmatch_lines>`. 

1555 """ 

1556 __tracebackhide__ = True 

1557 val = self.stringio.getvalue() 

1558 self.stringio.truncate(0) 

1559 self.stringio.seek(0) 

1560 lines1 = val.split("\n") 

1561 LineMatcher(lines1).fnmatch_lines(lines2) 

1562 

1563 

1564class LineMatcher: 

1565 """Flexible matching of text. 

1566 

1567 This is a convenience class to test large texts like the output of 

1568 commands. 

1569 

1570 The constructor takes a list of lines without their trailing newlines, i.e. 

1571 ``text.splitlines()``. 

1572 """ 

1573 

1574 def __init__(self, lines: List[str]) -> None: 

1575 self.lines = lines 

1576 self._log_output: List[str] = [] 

1577 

1578 def __str__(self) -> str: 

1579 """Return the entire original text. 

1580 

1581 .. versionadded:: 6.2 

1582 You can use :meth:`str` in older versions. 

1583 """ 

1584 return "\n".join(self.lines) 

1585 

1586 def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]: 

1587 if isinstance(lines2, str): 

1588 lines2 = Source(lines2) 

1589 if isinstance(lines2, Source): 

1590 lines2 = lines2.strip().lines 

1591 return lines2 

1592 

1593 def fnmatch_lines_random(self, lines2: Sequence[str]) -> None: 

1594 """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`).""" 

1595 __tracebackhide__ = True 

1596 self._match_lines_random(lines2, fnmatch) 

1597 

1598 def re_match_lines_random(self, lines2: Sequence[str]) -> None: 

1599 """Check lines exist in the output in any order (using :func:`python:re.match`).""" 

1600 __tracebackhide__ = True 

1601 self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name))) 

1602 

1603 def _match_lines_random( 

1604 self, lines2: Sequence[str], match_func: Callable[[str, str], bool] 

1605 ) -> None: 

1606 __tracebackhide__ = True 

1607 lines2 = self._getlines(lines2) 

1608 for line in lines2: 

1609 for x in self.lines: 

1610 if line == x or match_func(x, line): 

1611 self._log("matched: ", repr(line)) 

1612 break 

1613 else: 

1614 msg = "line %r not found in output" % line 

1615 self._log(msg) 

1616 self._fail(msg) 

1617 

1618 def get_lines_after(self, fnline: str) -> Sequence[str]: 

1619 """Return all lines following the given line in the text. 

1620 

1621 The given line can contain glob wildcards. 

1622 """ 

1623 for i, line in enumerate(self.lines): 

1624 if fnline == line or fnmatch(line, fnline): 

1625 return self.lines[i + 1 :] 

1626 raise ValueError("line %r not found in output" % fnline) 

1627 

1628 def _log(self, *args) -> None: 

1629 self._log_output.append(" ".join(str(x) for x in args)) 

1630 

1631 @property 

1632 def _log_text(self) -> str: 

1633 return "\n".join(self._log_output) 

1634 

1635 def fnmatch_lines( 

1636 self, lines2: Sequence[str], *, consecutive: bool = False 

1637 ) -> None: 

1638 """Check lines exist in the output (using :func:`python:fnmatch.fnmatch`). 

1639 

1640 The argument is a list of lines which have to match and can use glob 

1641 wildcards. If they do not match a pytest.fail() is called. The 

1642 matches and non-matches are also shown as part of the error message. 

1643 

1644 :param lines2: String patterns to match. 

1645 :param consecutive: Match lines consecutively? 

1646 """ 

1647 __tracebackhide__ = True 

1648 self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive) 

1649 

1650 def re_match_lines( 

1651 self, lines2: Sequence[str], *, consecutive: bool = False 

1652 ) -> None: 

1653 """Check lines exist in the output (using :func:`python:re.match`). 

1654 

1655 The argument is a list of lines which have to match using ``re.match``. 

1656 If they do not match a pytest.fail() is called. 

1657 

1658 The matches and non-matches are also shown as part of the error message. 

1659 

1660 :param lines2: string patterns to match. 

1661 :param consecutive: match lines consecutively? 

1662 """ 

1663 __tracebackhide__ = True 

1664 self._match_lines( 

1665 lines2, 

1666 lambda name, pat: bool(re.match(pat, name)), 

1667 "re.match", 

1668 consecutive=consecutive, 

1669 ) 

1670 

1671 def _match_lines( 

1672 self, 

1673 lines2: Sequence[str], 

1674 match_func: Callable[[str, str], bool], 

1675 match_nickname: str, 

1676 *, 

1677 consecutive: bool = False, 

1678 ) -> None: 

1679 """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. 

1680 

1681 :param Sequence[str] lines2: 

1682 List of string patterns to match. The actual format depends on 

1683 ``match_func``. 

1684 :param match_func: 

1685 A callable ``match_func(line, pattern)`` where line is the 

1686 captured line from stdout/stderr and pattern is the matching 

1687 pattern. 

1688 :param str match_nickname: 

1689 The nickname for the match function that will be logged to stdout 

1690 when a match occurs. 

1691 :param consecutive: 

1692 Match lines consecutively? 

1693 """ 

1694 if not isinstance(lines2, collections.abc.Sequence): 

1695 raise TypeError(f"invalid type for lines2: {type(lines2).__name__}") 

1696 lines2 = self._getlines(lines2) 

1697 lines1 = self.lines[:] 

1698 extralines = [] 

1699 __tracebackhide__ = True 

1700 wnick = len(match_nickname) + 1 

1701 started = False 

1702 for line in lines2: 

1703 nomatchprinted = False 

1704 while lines1: 

1705 nextline = lines1.pop(0) 

1706 if line == nextline: 

1707 self._log("exact match:", repr(line)) 

1708 started = True 

1709 break 

1710 elif match_func(nextline, line): 

1711 self._log("%s:" % match_nickname, repr(line)) 

1712 self._log( 

1713 "{:>{width}}".format("with:", width=wnick), repr(nextline) 

1714 ) 

1715 started = True 

1716 break 

1717 else: 

1718 if consecutive and started: 

1719 msg = f"no consecutive match: {line!r}" 

1720 self._log(msg) 

1721 self._log( 

1722 "{:>{width}}".format("with:", width=wnick), repr(nextline) 

1723 ) 

1724 self._fail(msg) 

1725 if not nomatchprinted: 

1726 self._log( 

1727 "{:>{width}}".format("nomatch:", width=wnick), repr(line) 

1728 ) 

1729 nomatchprinted = True 

1730 self._log("{:>{width}}".format("and:", width=wnick), repr(nextline)) 

1731 extralines.append(nextline) 

1732 else: 

1733 msg = f"remains unmatched: {line!r}" 

1734 self._log(msg) 

1735 self._fail(msg) 

1736 self._log_output = [] 

1737 

1738 def no_fnmatch_line(self, pat: str) -> None: 

1739 """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. 

1740 

1741 :param str pat: The pattern to match lines. 

1742 """ 

1743 __tracebackhide__ = True 

1744 self._no_match_line(pat, fnmatch, "fnmatch") 

1745 

1746 def no_re_match_line(self, pat: str) -> None: 

1747 """Ensure captured lines do not match the given pattern, using ``re.match``. 

1748 

1749 :param str pat: The regular expression to match lines. 

1750 """ 

1751 __tracebackhide__ = True 

1752 self._no_match_line( 

1753 pat, lambda name, pat: bool(re.match(pat, name)), "re.match" 

1754 ) 

1755 

1756 def _no_match_line( 

1757 self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str 

1758 ) -> None: 

1759 """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``. 

1760 

1761 :param str pat: The pattern to match lines. 

1762 """ 

1763 __tracebackhide__ = True 

1764 nomatch_printed = False 

1765 wnick = len(match_nickname) + 1 

1766 for line in self.lines: 

1767 if match_func(line, pat): 

1768 msg = f"{match_nickname}: {pat!r}" 

1769 self._log(msg) 

1770 self._log("{:>{width}}".format("with:", width=wnick), repr(line)) 

1771 self._fail(msg) 

1772 else: 

1773 if not nomatch_printed: 

1774 self._log("{:>{width}}".format("nomatch:", width=wnick), repr(pat)) 

1775 nomatch_printed = True 

1776 self._log("{:>{width}}".format("and:", width=wnick), repr(line)) 

1777 self._log_output = [] 

1778 

1779 def _fail(self, msg: str) -> None: 

1780 __tracebackhide__ = True 

1781 log_text = self._log_text 

1782 self._log_output = [] 

1783 fail(log_text) 

1784 

1785 def str(self) -> str: 

1786 """Return the entire original text.""" 

1787 return str(self)