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
« 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.
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
36from iniconfig import IniConfig
37from iniconfig import SectionWrapper
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
71if TYPE_CHECKING:
72 from typing_extensions import Final
73 from typing_extensions import Literal
75 import pexpect
78pytest_plugins = ["pytester_assertions"]
81IGNORE_PAM = [ # filenames added when obtaining details about the current user
82 "/var/lib/sss/mc/passwd"
83]
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 )
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 )
106 parser.addini(
107 "pytester_example_dir", help="Directory to take the pytester example files from"
108 )
111def pytest_configure(config: Config) -> None:
112 if config.getvalue("lsof"):
113 checker = LsofFdLeakChecker()
114 if checker.matching_platform():
115 config.pluginmanager.register(checker)
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 )
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
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 )
142 open_files = []
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))
154 return open_files
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
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()
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)))
189# used at least by pytest-xdist plugin
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)
200class PytestArg:
201 def __init__(self, request: FixtureRequest) -> None:
202 self._request = request
204 def gethookrecorder(self, hook) -> "HookRecorder":
205 hookrecorder = HookRecorder(hook._pm)
206 self._request.addfinalizer(hookrecorder.finish_recording)
207 return hookrecorder
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] != "_"]
215@final
216class RecordedHookCall:
217 """A recorded call to a hook.
219 The arguments to the hook call are set as attributes.
220 For example:
222 .. code-block:: python
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 """
229 def __init__(self, name: str, kwargs) -> None:
230 self.__dict__.update(kwargs)
231 self._name = name
233 def __repr__(self) -> str:
234 d = self.__dict__.copy()
235 del d["_name"]
236 return f"<RecordedHookCall {self._name!r}(**{d!r})>"
238 if TYPE_CHECKING:
239 # The class has undetermined attributes, this tells mypy about it.
240 def __getattr__(self, key: str):
241 ...
244@final
245class HookRecorder:
246 """Record all hooks called in a plugin manager.
248 Hook recorders are created by :class:`Pytester`.
250 This wraps all the hook calls in the plugin manager, recording each call
251 before propagating the normal calls.
252 """
254 def __init__(
255 self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False
256 ) -> None:
257 check_ispytest(_ispytest)
259 self._pluginmanager = pluginmanager
260 self.calls: List[RecordedHookCall] = []
261 self.ret: Optional[Union[int, ExitCode]] = None
263 def before(hook_name: str, hook_impls, kwargs) -> None:
264 self.calls.append(RecordedHookCall(hook_name, kwargs))
266 def after(outcome, hook_name: str, hook_impls, kwargs) -> None:
267 pass
269 self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after)
271 def finish_recording(self) -> None:
272 self._undo_wrapping()
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]
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}")
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))
311 def getcall(self, name: str) -> RecordedHookCall:
312 values = self.getcalls(name)
313 assert len(values) == 1, (name, values)
314 return values[0]
316 # functionality for test reports
318 @overload
319 def getreports(
320 self,
321 names: "Literal['pytest_collectreport']",
322 ) -> Sequence[CollectReport]:
323 ...
325 @overload
326 def getreports(
327 self,
328 names: "Literal['pytest_runtest_logreport']",
329 ) -> Sequence[TestReport]:
330 ...
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 ...
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)]
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]
383 @overload
384 def getfailures(
385 self,
386 names: "Literal['pytest_collectreport']",
387 ) -> Sequence[CollectReport]:
388 ...
390 @overload
391 def getfailures(
392 self,
393 names: "Literal['pytest_runtest_logreport']",
394 ) -> Sequence[TestReport]:
395 ...
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 ...
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]
416 def getfailedcollections(self) -> Sequence[CollectReport]:
417 return self.getfailures("pytest_collectreport")
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
443 def countoutcomes(self) -> List[int]:
444 return [len(x) for x in self.listoutcomes()]
446 def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None:
447 __tracebackhide__ = True
448 from _pytest.pytester_assertions import assertoutcome
450 outcomes = self.listoutcomes()
451 assertoutcome(
452 outcomes,
453 passed=passed,
454 skipped=skipped,
455 failed=failed,
456 )
458 def clear(self) -> None:
459 self.calls[:] = []
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()
469@fixture(name="LineMatcher")
470def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
471 """A reference to the :class: `LineMatcher`.
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
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.
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.
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)
496@fixture
497def _sys_snapshot() -> Generator[None, None, None]:
498 snappaths = SysPathsSnapshot()
499 snapmods = SysModulesSnapshot()
500 yield
501 snapmods.restore()
502 snappaths.restore()
505@fixture
506def _config_for_test() -> Generator[Config, None, None]:
507 from _pytest.config import get_config
509 config = get_config()
510 yield config
511 config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles.
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+)")
520@final
521class RunResult:
522 """The result of running a command from :class:`~pytest.Pytester`."""
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.
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."""
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 )
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.
561 The returned nouns will always be in plural form::
563 ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
565 Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
566 """
567 return self.parse_summary_nouns(self.outlines)
569 @classmethod
570 def parse_summary_nouns(cls, lines) -> Dict[str, int]:
571 """Extract the nouns from a pytest terminal summary line.
573 It always returns the plural noun for consistency::
575 ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
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")
587 to_plural = {
588 "warning": "warnings",
589 "error": "errors",
590 }
591 return {to_plural.get(k, k): v for k, v in ret.items()}
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.
608 ``warnings`` and ``deselected`` are only checked if not None.
609 """
610 __tracebackhide__ = True
611 from _pytest.pytester_assertions import assert_outcomes
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 )
627class CwdSnapshot:
628 def __init__(self) -> None:
629 self.__saved = os.getcwd()
631 def restore(self) -> None:
632 os.chdir(self.__saved)
635class SysModulesSnapshot:
636 def __init__(self, preserve: Optional[Callable[[str], bool]] = None) -> None:
637 self.__preserve = preserve
638 self.__saved = dict(sys.modules)
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)
649class SysPathsSnapshot:
650 def __init__(self) -> None:
651 self.__saved = list(sys.path), list(sys.meta_path)
653 def restore(self) -> None:
654 sys.path[:], sys.meta_path[:] = self.__saved
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.
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 """
667 __test__ = False
669 CLOSE_STDIN: "Final" = NOTSET
671 class TimeoutExpired(Exception):
672 pass
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)
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")
719 @property
720 def path(self) -> Path:
721 """Temporary directory path used to create files/run tests from, etc."""
722 return self._path
724 def __repr__(self) -> str:
725 return f"<Pytester {self.path!r}>"
727 def _finalize(self) -> None:
728 """
729 Clean up global state artifacts.
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()
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"))
749 return SysModulesSnapshot(preserve=preserve_module)
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
757 def chdir(self) -> None:
758 """Cd into the temporary directory.
760 This is done automatically upon instantiation.
761 """
762 os.chdir(self.path)
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())
773 if ext and not ext.startswith("."):
774 raise ValueError(
775 f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"
776 )
778 def to_text(s: Union[Any, bytes]) -> str:
779 return s.decode(encoding) if isinstance(s, bytes) else str(s)
781 if lines:
782 source = "\n".join(to_text(x) for x in lines)
783 basename = self._name
784 items.insert(0, (basename, source))
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
798 def makefile(self, ext: str, *args: str, **kwargs: str) -> Path:
799 r"""Create new text file(s) in the test directory.
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.
813 Examples:
815 .. code-block:: python
817 pytester.makefile(".txt", "line1", "line2")
819 pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n")
821 To create binary files, use :meth:`pathlib.Path.write_bytes` directly:
823 .. code-block:: python
825 filename = pytester.path.joinpath("foo.bin")
826 filename.write_bytes(b"...")
827 """
828 return self._makefile(ext, args, kwargs)
830 def makeconftest(self, source: str) -> Path:
831 """Write a contest.py file.
833 :param source: The contents.
834 :returns: The conftest.py file.
835 """
836 return self.makepyfile(conftest=source)
838 def makeini(self, source: str) -> Path:
839 """Write a tox.ini file.
841 :param source: The contents.
842 :returns: The tox.ini file.
843 """
844 return self.makefile(".ini", tox=source)
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"]
851 def makepyprojecttoml(self, source: str) -> Path:
852 """Write a pyproject.toml file.
854 :param source: The contents.
855 :returns: The pyproject.ini file.
857 .. versionadded:: 6.0
858 """
859 return self.makefile(".toml", pyproject=source)
861 def makepyfile(self, *args, **kwargs) -> Path:
862 r"""Shortcut for .makefile() with a .py extension.
864 Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
865 existing files.
867 Examples:
869 .. code-block:: python
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.
878 """
879 return self._makefile(".py", args, kwargs)
881 def maketxtfile(self, *args, **kwargs) -> Path:
882 r"""Shortcut for .makefile() with a .txt extension.
884 Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
885 existing files.
887 Examples:
889 .. code-block:: python
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.
898 """
899 return self._makefile(".txt", args, kwargs)
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`.
906 This is undone automatically when this object dies at the end of each
907 test.
909 :param path:
910 The path.
911 """
912 if path is None:
913 path = self.path
915 self._monkeypatch.syspath_prepend(str(path))
917 def mkdir(self, name: Union[str, "os.PathLike[str]"]) -> Path:
918 """Create a new (sub)directory.
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
929 def mkpydir(self, name: Union[str, "os.PathLike[str]"]) -> Path:
930 """Create a new python package.
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
940 def copy_example(self, name: Optional[str] = None) -> Path:
941 """Copy file from project's directory into the testdir.
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_
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)
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")
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)
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 )
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.
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
1006 def getpathnode(
1007 self, path: Union[str, "os.PathLike[str]"]
1008 ) -> Union[Collector, Item]:
1009 """Return the collection node of a file.
1011 This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
1012 create the (configured) pytest Config instance.
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
1028 def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]:
1029 """Generate all test items from a collection node.
1031 This recurses into the collection node and returns a list of all the
1032 test items contained within.
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
1045 def runitem(self, source: str) -> Any:
1046 """Run the "test_func" Item.
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)
1060 def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder:
1061 """Run a test module in process using ``pytest.main()``.
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.
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)
1074 def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
1075 """Run ``pytest.main(['--collectonly'])`` in-process.
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
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.
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`.
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()
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)
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
1128 rec = []
1130 class Collect:
1131 def pytest_configure(x, config: Config) -> None:
1132 rec.append(self.make_hook_recorder(config.pluginmanager))
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:
1140 class reprec: # type: ignore
1141 pass
1143 reprec.ret = ret
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()
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)
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
1178 class reprec: # type: ignore
1179 ret = ret
1181 except Exception:
1182 traceback.print_exc()
1184 class reprec: # type: ignore
1185 ret = ExitCode(3)
1187 finally:
1188 out, err = capture.readouterr()
1189 capture.stop_capturing()
1190 sys.stdout.write(out)
1191 sys.stderr.write(err)
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
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}")
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
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.
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.
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
1237 new_args = self._ensure_basetemp(args)
1238 new_args = [str(x) for x in new_args]
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
1247 def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config:
1248 """Return a new pytest configured Config instance.
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
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.
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.
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 )
1282 def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]:
1283 """Return all test items collected from the module.
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])
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``.
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.
1304 :param source:
1305 The source code of the module to collect.
1307 :param configargs:
1308 Any extra arguments to pass to :py:meth:`parseconfigure`.
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)
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.
1330 Searches a module collection node for a collection node matching the
1331 given name.
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
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`.
1353 Calls :py:class:`subprocess.Popen` making sure the current working
1354 directory is in ``PYTHONPATH``.
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
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
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)
1379 return popen
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.
1389 Run a process using :py:class:`subprocess.Popen` saving the stdout and
1390 stderr.
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.
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.
1407 - If it is of type :py:class:`bytes`, these bytes are sent to the
1408 standard input of the command.
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
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())
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()
1436 def handle_timeout() -> None:
1437 __tracebackhide__ = True
1439 timeout_message = (
1440 "{seconds} second timeout expired running:"
1441 " {command}".format(seconds=timeout, command=cmdargs)
1442 )
1444 popen.kill()
1445 popen.wait()
1446 raise self.TimeoutExpired(timeout_message)
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()
1456 with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2:
1457 out = f1.read().splitlines()
1458 err = f2.read().splitlines()
1460 self._dump_lines(out, sys.stdout)
1461 self._dump_lines(err, sys.stderr)
1463 with contextlib.suppress(ValueError):
1464 ret = ExitCode(ret)
1465 return RunResult(ret, out, err, timing.time() - now)
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")
1474 def _getpytestargs(self) -> Tuple[str, ...]:
1475 return sys.executable, "-mpytest"
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)
1481 def runpython_c(self, command: str) -> RunResult:
1482 """Run ``python -c "command"``."""
1483 return self.run(sys.executable, "-c", command)
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.
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.
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)
1513 def spawn_pytest(
1514 self, string: str, expect_timeout: float = 10.0
1515 ) -> "pexpect.spawn":
1516 """Run pytest using pexpect.
1518 This makes sure to use the right pytest and sets up the temporary
1519 directory locations.
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)
1529 def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
1530 """Run a command using pexpect.
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")
1541 child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout)
1542 self._request.addfinalizer(logfile.close)
1543 return child
1546class LineComp:
1547 def __init__(self) -> None:
1548 self.stringio = StringIO()
1549 """:class:`python:io.StringIO()` instance used for input."""
1551 def assert_contains_lines(self, lines2: Sequence[str]) -> None:
1552 """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value.
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)
1564class LineMatcher:
1565 """Flexible matching of text.
1567 This is a convenience class to test large texts like the output of
1568 commands.
1570 The constructor takes a list of lines without their trailing newlines, i.e.
1571 ``text.splitlines()``.
1572 """
1574 def __init__(self, lines: List[str]) -> None:
1575 self.lines = lines
1576 self._log_output: List[str] = []
1578 def __str__(self) -> str:
1579 """Return the entire original text.
1581 .. versionadded:: 6.2
1582 You can use :meth:`str` in older versions.
1583 """
1584 return "\n".join(self.lines)
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
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)
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)))
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)
1618 def get_lines_after(self, fnline: str) -> Sequence[str]:
1619 """Return all lines following the given line in the text.
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)
1628 def _log(self, *args) -> None:
1629 self._log_output.append(" ".join(str(x) for x in args))
1631 @property
1632 def _log_text(self) -> str:
1633 return "\n".join(self._log_output)
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`).
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.
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)
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`).
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.
1658 The matches and non-matches are also shown as part of the error message.
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 )
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``.
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 = []
1738 def no_fnmatch_line(self, pat: str) -> None:
1739 """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
1741 :param str pat: The pattern to match lines.
1742 """
1743 __tracebackhide__ = True
1744 self._no_match_line(pat, fnmatch, "fnmatch")
1746 def no_re_match_line(self, pat: str) -> None:
1747 """Ensure captured lines do not match the given pattern, using ``re.match``.
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 )
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``.
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 = []
1779 def _fail(self, msg: str) -> None:
1780 __tracebackhide__ = True
1781 log_text = self._log_text
1782 self._log_output = []
1783 fail(log_text)
1785 def str(self) -> str:
1786 """Return the entire original text."""
1787 return str(self)