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

2

3 PYTEST_DONT_REWRITE

4"""

5 import collections.abc

6 import contextlib

7 import gc

8 import importlib

9 import os

10 import platform

11 import re

12 import shutil

13 import subprocess

14 import sys

15 import traceback

16 from fnmatch import fnmatch

17 from io import StringIO

18 from pathlib import Path

19 from typing import Any

20 from typing import Callable

21 from typing import Dict

22 from typing import Generator

23 from typing import IO

24 from typing import Iterable

25 from typing import List

26 from typing import Optional

27 from typing import overload

28 from typing import Sequence

29 from typing import TextIO

30 from typing import Tuple

31 from typing import Type

32 from typing import TYPE_CHECKING

33 from typing import Union

34 from weakref import WeakKeyDictionary

35

36 from iniconfig import IniConfig

37 from iniconfig import SectionWrapper

38

39 from _pytest import timing

40 from _pytest._code import Source

41 from _pytest.capture import _get_multicapture

42 from _pytest.compat import final

43 from _pytest.compat import NOTSET

44 from _pytest.compat import NotSetType

45 from _pytest.config import _PluggyPlugin

46 from _pytest.config import Config

47 from _pytest.config import ExitCode

48 from _pytest.config import hookimpl

49 from _pytest.config import main

50 from _pytest.config import PytestPluginManager

51 from _pytest.config.argparsing import Parser

52 from _pytest.deprecated import check_ispytest

53 from _pytest.fixtures import fixture

54 from _pytest.fixtures import FixtureRequest

55 from _pytest.main import Session

56 from _pytest.monkeypatch import MonkeyPatch

57 from _pytest.nodes import Collector

58 from _pytest.nodes import Item

59 from _pytest.outcomes import fail

60 from _pytest.outcomes import importorskip

61 from _pytest.outcomes import skip

62 from _pytest.pathlib import bestrelpath

63 from _pytest.pathlib import copytree

64 from _pytest.pathlib import make_numbered_dir

65 from _pytest.reports import CollectReport

66 from _pytest.reports import TestReport

67 from _pytest.tmpdir import TempPathFactory

68 from _pytest.warning_types import PytestWarning

69

70

71 if TYPE_CHECKING:

72 from typing_extensions import Final

73 from typing_extensions import Literal

74

75 import pexpect

76

77

78 pytest_plugins = ["pytester_assertions"]

79

80

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

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

83]

84

85

86 def 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

111 def 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

124 class 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

193 def _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

200 class 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

210 def 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

216 class 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

245 class 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

463 def 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")

470 def 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

480 def 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

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

498 snappaths = SysPathsSnapshot()

499 snapmods = SysModulesSnapshot()

500 yield

501 snapmods.restore()

502 snappaths.restore()

503

504

505@fixture

506 def _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".

515 rex_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".

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

518

519

520@final

521 class 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

627 class CwdSnapshot:

628 def __init__(self) -> None:

629 self.__saved = os.getcwd()

630

631 def restore(self) -> None:

632 os.chdir(self.__saved)

633

634

635 class 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

649 class 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

658 class 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

1546 class 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

1564 class 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)