capture='fd' captures logging output from a handler holding a stale stderr reference (issue #2827). stdlib logging.StreamHandler grabs sys.stderr at configuration time. Under normal CliRunner (sys-level capture), the handler still writes to the original stream object and output is l
(tmp_path)
| 598 | |
| 599 | @needs_fd_capture |
| 600 | def test_capture_fd_logging_handler(tmp_path): |
| 601 | """capture='fd' captures logging output from a handler holding a stale |
| 602 | stderr reference (issue #2827). |
| 603 | |
| 604 | stdlib logging.StreamHandler grabs sys.stderr at configuration time. |
| 605 | Under normal CliRunner (sys-level capture), the handler still writes |
| 606 | to the original stream object and output is lost. fd-level capture |
| 607 | redirects the underlying file descriptor, so the writes are captured. |
| 608 | """ |
| 609 | import logging |
| 610 | |
| 611 | # Create a writer backed by the real fd 2, simulating a handler |
| 612 | # configured at import time before pytest or CliRunner replaced |
| 613 | # sys.stderr. open(2, closefd=False) mirrors the real scenario: |
| 614 | # the original sys.stderr is a TextIOWrapper -> BufferedWriter -> |
| 615 | # FileIO(fd=2). |
| 616 | stale_stderr = open(2, "w", closefd=False) # noqa: SIM115 |
| 617 | handler = logging.StreamHandler(stale_stderr) |
| 618 | handler.setFormatter(logging.Formatter("%(message)s")) |
| 619 | |
| 620 | logger = logging.getLogger(f"click_test_{tmp_path.name}") |
| 621 | logger.addHandler(handler) |
| 622 | logger.setLevel(logging.INFO) |
| 623 | logger.propagate = False |
| 624 | |
| 625 | @click.command() |
| 626 | def cli(): |
| 627 | logger.info("log from stale handler") |
| 628 | click.echo("normal echo") |
| 629 | |
| 630 | # sys-level capture misses the log line (it bypasses sys.stderr). |
| 631 | runner_sys = CliRunner(capture="sys") |
| 632 | result_sys = runner_sys.invoke(cli) |
| 633 | assert "normal echo" in result_sys.output |
| 634 | assert "log from stale handler" not in result_sys.output |
| 635 | |
| 636 | # fd-level capture catches it by redirecting fd 2. |
| 637 | runner_fd = CliRunner(capture="fd") |
| 638 | result_fd = runner_fd.invoke(cli) |
| 639 | assert "normal echo" in result_fd.output |
| 640 | assert "log from stale handler" in result_fd.output |
| 641 | |
| 642 | logger.removeHandler(handler) |
| 643 | |
| 644 | |
| 645 | @needs_fd_capture |