1"""Per-test stdout/stderr capturing mechanism."""

2 import contextlib

3 import functools

4 import io

5 import os

6 import sys

7 from io import UnsupportedOperation

8 from tempfile import TemporaryFile

9 from typing import Any

10 from typing import AnyStr

11 from typing import Generator

12 from typing import Generic

13 from typing import Iterator

14 from typing import Optional

15 from typing import TextIO

16 from typing import Tuple

17 from typing import TYPE_CHECKING

18 from typing import Union

19

20 from _pytest.compat import final

21 from _pytest.config import Config

22 from _pytest.config import hookimpl

23 from _pytest.config.argparsing import Parser

24 from _pytest.deprecated import check_ispytest

25 from _pytest.fixtures import fixture

26 from _pytest.fixtures import SubRequest

27 from _pytest.nodes import Collector

28 from _pytest.nodes import File

29 from _pytest.nodes import Item

30

31 if TYPE_CHECKING:

32 from typing_extensions import Literal

33

34 _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]

35

36

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

38 group = parser.getgroup("general")

39 group._addoption(

40 "--capture",

41 action="store",

42 default="fd",

43 metavar="method",

44 choices=["fd", "sys", "no", "tee-sys"],

45 help="Per-test capturing method: one of fd|sys|no|tee-sys",

46 )

47 group._addoption(

48 "-s",

49 action="store_const",

50 const="no",

51 dest="capture",

52 help="Shortcut for --capture=no",

53 )

54

55

56 def _colorama_workaround() -> None:

57 """Ensure colorama is imported so that it attaches to the correct stdio

58 handles on Windows.

59

60 colorama uses the terminal on import time. So if something does the

61 first import of colorama while I/O capture is active, colorama will

62 fail in various ways.

63 """

64 if sys.platform.startswith("win32"):

65 try:

66 import colorama # noqa: F401

67 except ImportError:

68 pass

69

70

71 def _windowsconsoleio_workaround(stream: TextIO) -> None:

72 """Workaround for Windows Unicode console handling.

73

74 Python 3.6 implemented Unicode console handling for Windows. This works

75 by reading/writing to the raw console handle using

76 ``{Read,Write}ConsoleW``.

77

78 The problem is that we are going to ``dup2`` over the stdio file

79 descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the

80 handles used by Python to write to the console. Though there is still some

81 weirdness and the console handle seems to only be closed randomly and not

82 on the first call to ``CloseHandle``, or maybe it gets reopened with the

83 same handle value when we suspend capturing.

84

85 The workaround in this case will reopen stdio with a different fd which

86 also means a different handle by replicating the logic in

87 "Py_lifecycle.c:initstdio/create_stdio".

88

89 :param stream:

90 In practice ``sys.stdout`` or ``sys.stderr``, but given

91 here as parameter for unittesting purposes.

92

93 See https://github.com/pytest-dev/py/issues/103.

94 """

95 if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):

96 return

97

98 # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).

99 if not hasattr(stream, "buffer"): # type: ignore[unreachable]

100 return

101

102 buffered = hasattr(stream.buffer, "raw")

103 raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]

104

105 if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]

106 return

107

108 def _reopen_stdio(f, mode):

109 if not buffered and mode[0] == "w":

110 buffering = 0

111 else:

112 buffering = -1

113

114 return io.TextIOWrapper(

115 open(os.dup(f.fileno()), mode, buffering),

116 f.encoding,

117 f.errors,

118 f.newlines,

119 f.line_buffering,

120 )

121

122 sys.stdin = _reopen_stdio(sys.stdin, "rb")

123 sys.stdout = _reopen_stdio(sys.stdout, "wb")

124 sys.stderr = _reopen_stdio(sys.stderr, "wb")

125

126

127@hookimpl(hookwrapper=True)

128 def pytest_load_initial_conftests(early_config: Config):

129 ns = early_config.known_args_namespace

130 if ns.capture == "fd":

131 _windowsconsoleio_workaround(sys.stdout)

132 _colorama_workaround()

133 pluginmanager = early_config.pluginmanager

134 capman = CaptureManager(ns.capture)

135 pluginmanager.register(capman, "capturemanager")

136

137 # Make sure that capturemanager is properly reset at final shutdown.

138 early_config.add_cleanup(capman.stop_global_capturing)

139

140 # Finally trigger conftest loading but while capturing (issue #93).

141 capman.start_global_capturing()

142 outcome = yield

143 capman.suspend_global_capture()

144 if outcome.excinfo is not None:

145 out, err = capman.read_global_capture()

146 sys.stdout.write(out)

147 sys.stderr.write(err)

148

149

150# IO Helpers.

151

152

153 class EncodedFile(io.TextIOWrapper):

154 __slots__ = ()

155

156 @property

157 def name(self) -> str:

158 # Ensure that file.name is a string. Workaround for a Python bug

159 # fixed in >=3.7.4: https://bugs.python.org/issue36015

160 return repr(self.buffer)

161

162 @property

163 def mode(self) -> str:

164 # TextIOWrapper doesn't expose a mode, but at least some of our

165 # tests check it.

166 return self.buffer.mode.replace("b", "")

167

168

169 class CaptureIO(io.TextIOWrapper):

170 def __init__(self) -> None:

171 super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)

172

173 def getvalue(self) -> str:

174 assert isinstance(self.buffer, io.BytesIO)

175 return self.buffer.getvalue().decode("UTF-8")

176

177

178 class TeeCaptureIO(CaptureIO):

179 def __init__(self, other: TextIO) -> None:

180 self._other = other

181 super().__init__()

182

183 def write(self, s: str) -> int:

184 super().write(s)

185 return self._other.write(s)

186

187

188 class DontReadFromInput:

189 encoding = None

190

191 def read(self, *args):

192 raise OSError(

193 "pytest: reading from stdin while output is captured! Consider using `-s`."

194 )

195

196 readline = read

197 readlines = read

198 __next__ = read

199

200 def __iter__(self):

201 return self

202

203 def fileno(self) -> int:

204 raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")

205

206 def flush(self) -> None:

207 raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()")

208

209 def isatty(self) -> bool:

210 return False

211

212 def close(self) -> None:

213 pass

214

215 def readable(self) -> bool:

216 return False

217

218 def seek(self, offset: int) -> int:

219 raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")

220

221 def seekable(self) -> bool:

222 return False

223

224 def tell(self) -> int:

225 raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")

226

227 def truncate(self, size: int) -> None:

228 raise UnsupportedOperation("cannont truncate stdin")

229

230 def write(self, *args) -> None:

231 raise UnsupportedOperation("cannot write to stdin")

232

233 def writelines(self, *args) -> None:

234 raise UnsupportedOperation("Cannot write to stdin")

235

236 def writable(self) -> bool:

237 return False

238

239 @property

240 def buffer(self):

241 return self

242

243

244# Capture classes.

245

246

247 patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}

248

249

250 class NoCapture:

251 EMPTY_BUFFER = None

252 __init__ = start = done = suspend = resume = lambda *args: None

253

254

255 class SysCaptureBinary:

256

257 EMPTY_BUFFER = b""

258

259 def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:

260 name = patchsysdict[fd]

261 self._old = getattr(sys, name)

262 self.name = name

263 if tmpfile is None:

264 if name == "stdin":

265 tmpfile = DontReadFromInput()

266 else:

267 tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old)

268 self.tmpfile = tmpfile

269 self._state = "initialized"

270

271 def repr(self, class_name: str) -> str:

272 return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(

273 class_name,

274 self.name,

275 hasattr(self, "_old") and repr(self._old) or "<UNSET>",

276 self._state,

277 self.tmpfile,

278 )

279

280 def __repr__(self) -> str:

281 return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(

282 self.__class__.__name__,

283 self.name,

284 hasattr(self, "_old") and repr(self._old) or "<UNSET>",

285 self._state,

286 self.tmpfile,

287 )

288

289 def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:

290 assert (

291 self._state in states

292 ), "cannot {} in state {!r}: expected one of {}".format(

293 op, self._state, ", ".join(states)

294 )

295

296 def start(self) -> None:

297 self._assert_state("start", ("initialized",))

298 setattr(sys, self.name, self.tmpfile)

299 self._state = "started"

300

301 def snap(self):

302 self._assert_state("snap", ("started", "suspended"))

303 self.tmpfile.seek(0)

304 res = self.tmpfile.buffer.read()

305 self.tmpfile.seek(0)

306 self.tmpfile.truncate()

307 return res

308

309 def done(self) -> None:

310 self._assert_state("done", ("initialized", "started", "suspended", "done"))

311 if self._state == "done":

312 return

313 setattr(sys, self.name, self._old)

314 del self._old

315 self.tmpfile.close()

316 self._state = "done"

317

318 def suspend(self) -> None:

319 self._assert_state("suspend", ("started", "suspended"))

320 setattr(sys, self.name, self._old)

321 self._state = "suspended"

322

323 def resume(self) -> None:

324 self._assert_state("resume", ("started", "suspended"))

325 if self._state == "started":

326 return

327 setattr(sys, self.name, self.tmpfile)

328 self._state = "started"

329

330 def writeorg(self, data) -> None:

331 self._assert_state("writeorg", ("started", "suspended"))

332 self._old.flush()

333 self._old.buffer.write(data)

334 self._old.buffer.flush()

335

336

337 class SysCapture(SysCaptureBinary):

338 EMPTY_BUFFER = "" # type: ignore[assignment]

339

340 def snap(self):

341 res = self.tmpfile.getvalue()

342 self.tmpfile.seek(0)

343 self.tmpfile.truncate()

344 return res

345

346 def writeorg(self, data):

347 self._assert_state("writeorg", ("started", "suspended"))

348 self._old.write(data)

349 self._old.flush()

350

351

352 class FDCaptureBinary:

353 """Capture IO to/from a given OS-level file descriptor.

354

355 snap() produces `bytes`.

356 """

357

358 EMPTY_BUFFER = b""

359

360 def __init__(self, targetfd: int) -> None:

361 self.targetfd = targetfd

362

363 try:

364 os.fstat(targetfd)

365 except OSError:

366 # FD capturing is conceptually simple -- create a temporary file,

367 # redirect the FD to it, redirect back when done. But when the

368 # target FD is invalid it throws a wrench into this lovely scheme.

369 #

370 # Tests themselves shouldn't care if the FD is valid, FD capturing

371 # should work regardless of external circumstances. So falling back

372 # to just sys capturing is not a good option.

373 #

374 # Further complications are the need to support suspend() and the

375 # possibility of FD reuse (e.g. the tmpfile getting the very same

376 # target FD). The following approach is robust, I believe.

377 self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)

378 os.dup2(self.targetfd_invalid, targetfd)

379 else:

380 self.targetfd_invalid = None

381 self.targetfd_save = os.dup(targetfd)

382

383 if targetfd == 0:

384 self.tmpfile = open(os.devnull, encoding="utf-8")

385 self.syscapture = SysCapture(targetfd)

386 else:

387 self.tmpfile = EncodedFile(

388 TemporaryFile(buffering=0),

389 encoding="utf-8",

390 errors="replace",

391 newline="",

392 write_through=True,

393 )

394 if targetfd in patchsysdict:

395 self.syscapture = SysCapture(targetfd, self.tmpfile)

396 else:

397 self.syscapture = NoCapture()

398

399 self._state = "initialized"

400

401 def __repr__(self) -> str:

402 return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format(

403 self.__class__.__name__,

404 self.targetfd,

405 self.targetfd_save,

406 self._state,

407 self.tmpfile,

408 )

409

410 def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:

411 assert (

412 self._state in states

413 ), "cannot {} in state {!r}: expected one of {}".format(

414 op, self._state, ", ".join(states)

415 )

416

417 def start(self) -> None:

418 """Start capturing on targetfd using memorized tmpfile."""

419 self._assert_state("start", ("initialized",))

420 os.dup2(self.tmpfile.fileno(), self.targetfd)

421 self.syscapture.start()

422 self._state = "started"

423

424 def snap(self):

425 self._assert_state("snap", ("started", "suspended"))

426 self.tmpfile.seek(0)

427 res = self.tmpfile.buffer.read()

428 self.tmpfile.seek(0)

429 self.tmpfile.truncate()

430 return res

431

432 def done(self) -> None:

433 """Stop capturing, restore streams, return original capture file,

434 seeked to position zero."""

435 self._assert_state("done", ("initialized", "started", "suspended", "done"))

436 if self._state == "done":

437 return

438 os.dup2(self.targetfd_save, self.targetfd)

439 os.close(self.targetfd_save)

440 if self.targetfd_invalid is not None:

441 if self.targetfd_invalid != self.targetfd:

442 os.close(self.targetfd)

443 os.close(self.targetfd_invalid)

444 self.syscapture.done()

445 self.tmpfile.close()

446 self._state = "done"

447

448 def suspend(self) -> None:

449 self._assert_state("suspend", ("started", "suspended"))

450 if self._state == "suspended":

451 return

452 self.syscapture.suspend()

453 os.dup2(self.targetfd_save, self.targetfd)

454 self._state = "suspended"

455

456 def resume(self) -> None:

457 self._assert_state("resume", ("started", "suspended"))

458 if self._state == "started":

459 return

460 self.syscapture.resume()

461 os.dup2(self.tmpfile.fileno(), self.targetfd)

462 self._state = "started"

463

464 def writeorg(self, data):

465 """Write to original file descriptor."""

466 self._assert_state("writeorg", ("started", "suspended"))

467 os.write(self.targetfd_save, data)

468

469

470 class FDCapture(FDCaptureBinary):

471 """Capture IO to/from a given OS-level file descriptor.

472

473 snap() produces text.

474 """

475

476 # Ignore type because it doesn't match the type in the superclass (bytes).

477 EMPTY_BUFFER = "" # type: ignore

478

479 def snap(self):

480 self._assert_state("snap", ("started", "suspended"))

481 self.tmpfile.seek(0)

482 res = self.tmpfile.read()

483 self.tmpfile.seek(0)

484 self.tmpfile.truncate()

485 return res

486

487 def writeorg(self, data):

488 """Write to original file descriptor."""

489 super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream

490

491

492# MultiCapture

493

494

495# This class was a namedtuple, but due to mypy limitation[0] it could not be

496# made generic, so was replaced by a regular class which tries to emulate the

497# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can

498# make it a namedtuple again.

499# [0]: https://github.com/python/mypy/issues/685

500@final

501@functools.total_ordering

502 class CaptureResult(Generic[AnyStr]):

503 """The result of :method:`CaptureFixture.readouterr`."""

504

505 __slots__ = ("out", "err")

506

507 def __init__(self, out: AnyStr, err: AnyStr) -> None:

508 self.out: AnyStr = out

509 self.err: AnyStr = err

510

511 def __len__(self) -> int:

512 return 2

513

514 def __iter__(self) -> Iterator[AnyStr]:

515 return iter((self.out, self.err))

516

517 def __getitem__(self, item: int) -> AnyStr:

518 return tuple(self)[item]

519

520 def _replace(

521 self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None

522 ) -> "CaptureResult[AnyStr]":

523 return CaptureResult(

524 out=self.out if out is None else out, err=self.err if err is None else err

525 )

526

527 def count(self, value: AnyStr) -> int:

528 return tuple(self).count(value)

529

530 def index(self, value) -> int:

531 return tuple(self).index(value)

532

533 def __eq__(self, other: object) -> bool:

534 if not isinstance(other, (CaptureResult, tuple)):

535 return NotImplemented

536 return tuple(self) == tuple(other)

537

538 def __hash__(self) -> int:

539 return hash(tuple(self))

540

541 def __lt__(self, other: object) -> bool:

542 if not isinstance(other, (CaptureResult, tuple)):

543 return NotImplemented

544 return tuple(self) < tuple(other)

545

546 def __repr__(self) -> str:

547 return f"CaptureResult(out={self.out!r}, err={self.err!r})"

548

549

550 class MultiCapture(Generic[AnyStr]):

551 _state = None

552 _in_suspended = False

553

554 def __init__(self, in_, out, err) -> None:

555 self.in_ = in_

556 self.out = out

557 self.err = err

558

559 def __repr__(self) -> str:

560 return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(

561 self.out,

562 self.err,

563 self.in_,

564 self._state,

565 self._in_suspended,

566 )

567

568 def start_capturing(self) -> None:

569 self._state = "started"

570 if self.in_:

571 self.in_.start()

572 if self.out:

573 self.out.start()

574 if self.err:

575 self.err.start()

576

577 def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]:

578 """Pop current snapshot out/err capture and flush to orig streams."""

579 out, err = self.readouterr()

580 if out:

581 self.out.writeorg(out)

582 if err:

583 self.err.writeorg(err)

584 return out, err

585

586 def suspend_capturing(self, in_: bool = False) -> None:

587 self._state = "suspended"

588 if self.out:

589 self.out.suspend()

590 if self.err:

591 self.err.suspend()

592 if in_ and self.in_:

593 self.in_.suspend()

594 self._in_suspended = True

595

596 def resume_capturing(self) -> None:

597 self._state = "started"

598 if self.out:

599 self.out.resume()

600 if self.err:

601 self.err.resume()

602 if self._in_suspended:

603 self.in_.resume()

604 self._in_suspended = False

605

606 def stop_capturing(self) -> None:

607 """Stop capturing and reset capturing streams."""

608 if self._state == "stopped":

609 raise ValueError("was already stopped")

610 self._state = "stopped"

611 if self.out:

612 self.out.done()

613 if self.err:

614 self.err.done()

615 if self.in_:

616 self.in_.done()

617

618 def is_started(self) -> bool:

619 """Whether actively capturing -- not suspended or stopped."""

620 return self._state == "started"

621

622 def readouterr(self) -> CaptureResult[AnyStr]:

623 out = self.out.snap() if self.out else ""

624 err = self.err.snap() if self.err else ""

625 return CaptureResult(out, err)

626

627

628 def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:

629 if method == "fd":

630 return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))

631 elif method == "sys":

632 return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2))

633 elif method == "no":

634 return MultiCapture(in_=None, out=None, err=None)

635 elif method == "tee-sys":

636 return MultiCapture(

637 in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)

638 )

639 raise ValueError(f"unknown capturing method: {method!r}")

640

641

642# CaptureManager and CaptureFixture

643

644

645 class CaptureManager:

646 """The capture plugin.

647

648 Manages that the appropriate capture method is enabled/disabled during

649 collection and each test phase (setup, call, teardown). After each of

650 those points, the captured output is obtained and attached to the

651 collection/runtest report.

652

653 There are two levels of capture:

654

655 * global: enabled by default and can be suppressed by the ``-s``

656 option. This is always enabled/disabled during collection and each test

657 phase.

658

659 * fixture: when a test function or one of its fixture depend on the

660 ``capsys`` or ``capfd`` fixtures. In this case special handling is

661 needed to ensure the fixtures take precedence over the global capture.

662 """

663

664 def __init__(self, method: "_CaptureMethod") -> None:

665 self._method = method

666 self._global_capturing: Optional[MultiCapture[str]] = None

667 self._capture_fixture: Optional[CaptureFixture[Any]] = None

668

669 def __repr__(self) -> str:

670 return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(

671 self._method, self._global_capturing, self._capture_fixture

672 )

673

674 def is_capturing(self) -> Union[str, bool]:

675 if self.is_globally_capturing():

676 return "global"

677 if self._capture_fixture:

678 return "fixture %s" % self._capture_fixture.request.fixturename

679 return False

680

681 # Global capturing control

682

683 def is_globally_capturing(self) -> bool:

684 return self._method != "no"

685

686 def start_global_capturing(self) -> None:

687 assert self._global_capturing is None

688 self._global_capturing = _get_multicapture(self._method)

689 self._global_capturing.start_capturing()

690

691 def stop_global_capturing(self) -> None:

692 if self._global_capturing is not None:

693 self._global_capturing.pop_outerr_to_orig()

694 self._global_capturing.stop_capturing()

695 self._global_capturing = None

696

697 def resume_global_capture(self) -> None:

698 # During teardown of the python process, and on rare occasions, capture

699 # attributes can be `None` while trying to resume global capture.

700 if self._global_capturing is not None:

701 self._global_capturing.resume_capturing()

702

703 def suspend_global_capture(self, in_: bool = False) -> None:

704 if self._global_capturing is not None:

705 self._global_capturing.suspend_capturing(in_=in_)

706

707 def suspend(self, in_: bool = False) -> None:

708 # Need to undo local capsys-et-al if it exists before disabling global capture.

709 self.suspend_fixture()

710 self.suspend_global_capture(in_)

711

712 def resume(self) -> None:

713 self.resume_global_capture()

714 self.resume_fixture()

715

716 def read_global_capture(self) -> CaptureResult[str]:

717 assert self._global_capturing is not None

718 return self._global_capturing.readouterr()

719

720 # Fixture Control

721

722 def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None:

723 if self._capture_fixture:

724 current_fixture = self._capture_fixture.request.fixturename

725 requested_fixture = capture_fixture.request.fixturename

726 capture_fixture.request.raiseerror(

727 "cannot use {} and {} at the same time".format(

728 requested_fixture, current_fixture

729 )

730 )

731 self._capture_fixture = capture_fixture

732

733 def unset_fixture(self) -> None:

734 self._capture_fixture = None

735

736 def activate_fixture(self) -> None:

737 """If the current item is using ``capsys`` or ``capfd``, activate

738 them so they take precedence over the global capture."""

739 if self._capture_fixture:

740 self._capture_fixture._start()

741

742 def deactivate_fixture(self) -> None:

743 """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""

744 if self._capture_fixture:

745 self._capture_fixture.close()

746

747 def suspend_fixture(self) -> None:

748 if self._capture_fixture:

749 self._capture_fixture._suspend()

750

751 def resume_fixture(self) -> None:

752 if self._capture_fixture:

753 self._capture_fixture._resume()

754

755 # Helper context managers

756

757 @contextlib.contextmanager

758 def global_and_fixture_disabled(self) -> Generator[None, None, None]:

759 """Context manager to temporarily disable global and current fixture capturing."""

760 do_fixture = self._capture_fixture and self._capture_fixture._is_started()

761 if do_fixture:

762 self.suspend_fixture()

763 do_global = self._global_capturing and self._global_capturing.is_started()

764 if do_global:

765 self.suspend_global_capture()

766 try:

767 yield

768 finally:

769 if do_global:

770 self.resume_global_capture()

771 if do_fixture:

772 self.resume_fixture()

773

774 @contextlib.contextmanager

775 def item_capture(self, when: str, item: Item) -> Generator[None, None, None]:

776 self.resume_global_capture()

777 self.activate_fixture()

778 try:

779 yield

780 finally:

781 self.deactivate_fixture()

782 self.suspend_global_capture(in_=False)

783

784 out, err = self.read_global_capture()

785 item.add_report_section(when, "stdout", out)

786 item.add_report_section(when, "stderr", err)

787

788 # Hooks

789

790 @hookimpl(hookwrapper=True)

791 def pytest_make_collect_report(self, collector: Collector):

792 if isinstance(collector, File):

793 self.resume_global_capture()

794 outcome = yield

795 self.suspend_global_capture()

796 out, err = self.read_global_capture()

797 rep = outcome.get_result()

798 if out:

799 rep.sections.append(("Captured stdout", out))

800 if err:

801 rep.sections.append(("Captured stderr", err))

802 else:

803 yield

804

805 @hookimpl(hookwrapper=True)

806 def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:

807 with self.item_capture("setup", item):

808 yield

809

810 @hookimpl(hookwrapper=True)

811 def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:

812 with self.item_capture("call", item):

813 yield

814

815 @hookimpl(hookwrapper=True)

816 def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:

817 with self.item_capture("teardown", item):

818 yield

819

820 @hookimpl(tryfirst=True)

821 def pytest_keyboard_interrupt(self) -> None:

822 self.stop_global_capturing()

823

824 @hookimpl(tryfirst=True)

825 def pytest_internalerror(self) -> None:

826 self.stop_global_capturing()

827

828

829 class CaptureFixture(Generic[AnyStr]):

830 """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,

831 :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""

832

833 def __init__(

834 self, captureclass, request: SubRequest, *, _ispytest: bool = False

835 ) -> None:

836 check_ispytest(_ispytest)

837 self.captureclass = captureclass

838 self.request = request

839 self._capture: Optional[MultiCapture[AnyStr]] = None

840 self._captured_out = self.captureclass.EMPTY_BUFFER

841 self._captured_err = self.captureclass.EMPTY_BUFFER

842

843 def _start(self) -> None:

844 if self._capture is None:

845 self._capture = MultiCapture(

846 in_=None,

847 out=self.captureclass(1),

848 err=self.captureclass(2),

849 )

850 self._capture.start_capturing()

851

852 def close(self) -> None:

853 if self._capture is not None:

854 out, err = self._capture.pop_outerr_to_orig()

855 self._captured_out += out

856 self._captured_err += err

857 self._capture.stop_capturing()

858 self._capture = None

859

860 def readouterr(self) -> CaptureResult[AnyStr]:

861 """Read and return the captured output so far, resetting the internal

862 buffer.

863

864 :returns:

865 The captured content as a namedtuple with ``out`` and ``err``

866 string attributes.

867 """

868 captured_out, captured_err = self._captured_out, self._captured_err

869 if self._capture is not None:

870 out, err = self._capture.readouterr()

871 captured_out += out

872 captured_err += err

873 self._captured_out = self.captureclass.EMPTY_BUFFER

874 self._captured_err = self.captureclass.EMPTY_BUFFER

875 return CaptureResult(captured_out, captured_err)

876

877 def _suspend(self) -> None:

878 """Suspend this fixture's own capturing temporarily."""

879 if self._capture is not None:

880 self._capture.suspend_capturing()

881

882 def _resume(self) -> None:

883 """Resume this fixture's own capturing temporarily."""

884 if self._capture is not None:

885 self._capture.resume_capturing()

886

887 def _is_started(self) -> bool:

888 """Whether actively capturing -- not disabled or closed."""

889 if self._capture is not None:

890 return self._capture.is_started()

891 return False

892

893 @contextlib.contextmanager

894 def disabled(self) -> Generator[None, None, None]:

895 """Temporarily disable capturing while inside the ``with`` block."""

896 capmanager = self.request.config.pluginmanager.getplugin("capturemanager")

897 with capmanager.global_and_fixture_disabled():

898 yield

899

900

901# The fixtures.

902

903

904@fixture

905 def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:

906 r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.

907

908 The captured output is made available via ``capsys.readouterr()`` method

909 calls, which return a ``(out, err)`` namedtuple.

910 ``out`` and ``err`` will be ``text`` objects.

911

912 Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.

913

914 Example:

915

916 .. code-block:: python

917

918 def test_output(capsys):

919 print("hello")

920 captured = capsys.readouterr()

921 assert captured.out == "hello\n"

922 """

923 capman = request.config.pluginmanager.getplugin("capturemanager")

924 capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)

925 capman.set_fixture(capture_fixture)

926 capture_fixture._start()

927 yield capture_fixture

928 capture_fixture.close()

929 capman.unset_fixture()

930

931

932@fixture

933 def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:

934 r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.

935

936 The captured output is made available via ``capsysbinary.readouterr()``

937 method calls, which return a ``(out, err)`` namedtuple.

938 ``out`` and ``err`` will be ``bytes`` objects.

939

940 Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.

941

942 Example:

943

944 .. code-block:: python

945

946 def test_output(capsysbinary):

947 print("hello")

948 captured = capsysbinary.readouterr()

949 assert captured.out == b"hello\n"

950 """

951 capman = request.config.pluginmanager.getplugin("capturemanager")

952 capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)

953 capman.set_fixture(capture_fixture)

954 capture_fixture._start()

955 yield capture_fixture

956 capture_fixture.close()

957 capman.unset_fixture()

958

959

960@fixture

961 def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:

962 r"""Enable text capturing of writes to file descriptors ``1`` and ``2``.

963

964 The captured output is made available via ``capfd.readouterr()`` method

965 calls, which return a ``(out, err)`` namedtuple.

966 ``out`` and ``err`` will be ``text`` objects.

967

968 Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.

969

970 Example:

971

972 .. code-block:: python

973

974 def test_system_echo(capfd):

975 os.system('echo "hello"')

976 captured = capfd.readouterr()

977 assert captured.out == "hello\n"

978 """

979 capman = request.config.pluginmanager.getplugin("capturemanager")

980 capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)

981 capman.set_fixture(capture_fixture)

982 capture_fixture._start()

983 yield capture_fixture

984 capture_fixture.close()

985 capman.unset_fixture()

986

987

988@fixture

989 def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:

990 r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.

991

992 The captured output is made available via ``capfd.readouterr()`` method

993 calls, which return a ``(out, err)`` namedtuple.

994 ``out`` and ``err`` will be ``byte`` objects.

995

996 Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.

997

998 Example:

999

1000 .. code-block:: python

1001

1002 def test_system_echo(capfdbinary):

1003 os.system('echo "hello"')

1004 captured = capfdbinary.readouterr()

1005 assert captured.out == b"hello\n"

1006

1007 """

1008 capman = request.config.pluginmanager.getplugin("capturemanager")

1009 capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)

1010 capman.set_fixture(capture_fixture)

1011 capture_fixture._start()

1012 yield capture_fixture

1013 capture_fixture.close()

1014 capman.unset_fixture()