provide-testkit 0.5.1__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. {provide_testkit-0.5.1/src/provide_testkit.egg-info → provide_testkit-0.5.2}/PKG-INFO +1 -1
  2. provide_testkit-0.5.2/VERSION +1 -0
  3. provide_testkit-0.5.2/src/provide/testkit/_capture.py +81 -0
  4. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/pytest_plugin.py +7 -0
  5. {provide_testkit-0.5.1 → provide_testkit-0.5.2/src/provide_testkit.egg-info}/PKG-INFO +1 -1
  6. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide_testkit.egg-info/SOURCES.txt +2 -0
  7. provide_testkit-0.5.2/tests/test_capture_stream_swap.py +142 -0
  8. provide_testkit-0.5.1/VERSION +0 -1
  9. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/LICENSE +0 -0
  10. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/README.md +0 -0
  11. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/pyproject.toml +0 -0
  12. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/setup.cfg +0 -0
  13. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/__init__.py +0 -0
  14. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/__init__.py +0 -0
  15. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/__main__.py +0 -0
  16. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/_blocker.py +0 -0
  17. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/_early_init.py +0 -0
  18. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/_install_blocker.py +0 -0
  19. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/_install_pth.py +0 -0
  20. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/_streams.py +0 -0
  21. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/base/__init__.py +0 -0
  22. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/base/foundation.py +0 -0
  23. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/base/harness.py +0 -0
  24. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/base/minimal.py +0 -0
  25. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/chaos/__init__.py +0 -0
  26. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/chaos/concurrency_strategies.py +0 -0
  27. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/chaos/fixtures.py +0 -0
  28. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/chaos/io_strategies.py +0 -0
  29. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/chaos/strategies.py +0 -0
  30. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/chaos/time_strategies.py +0 -0
  31. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/cli/__init__.py +0 -0
  32. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/cli/runner.py +0 -0
  33. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/cli/testing.py +0 -0
  34. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/common/__init__.py +0 -0
  35. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/common/fixtures.py +0 -0
  36. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/conftest.py +0 -0
  37. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/crypto/__init__.py +0 -0
  38. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/crypto/fixtures.py +0 -0
  39. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/file/__init__.py +0 -0
  40. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/file/content_fixtures.py +0 -0
  41. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/file/directory_fixtures.py +0 -0
  42. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/file/fixtures.py +0 -0
  43. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/file/special_fixtures.py +0 -0
  44. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/file/temp.py +0 -0
  45. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/hub/__init__.py +0 -0
  46. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/hub/fixtures.py +0 -0
  47. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/logger/__init__.py +0 -0
  48. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/logger/hooks.py +0 -0
  49. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/logger/mocks.py +0 -0
  50. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/logger/reset.py +0 -0
  51. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/main.py +0 -0
  52. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/mocking/__init__.py +0 -0
  53. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/mocking/fixtures.py +0 -0
  54. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/mocking/time.py +0 -0
  55. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/process/__init__.py +0 -0
  56. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/process/async_fixtures.py +0 -0
  57. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/process/fixtures.py +0 -0
  58. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/process/script_assertions.py +0 -0
  59. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/process/script_fixtures.py +0 -0
  60. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/process/subprocess_fixtures.py +0 -0
  61. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/process/system_fixtures.py +0 -0
  62. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/provide_testkit_init.pth +0 -0
  63. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/py.typed +0 -0
  64. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/__init__.py +0 -0
  65. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/artifacts.py +0 -0
  66. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/base.py +0 -0
  67. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/cli.py +0 -0
  68. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/complexity/__init__.py +0 -0
  69. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/complexity/analyzer.py +0 -0
  70. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/complexity/fixture.py +0 -0
  71. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/coverage/__init__.py +0 -0
  72. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/coverage/fixture.py +0 -0
  73. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/coverage/reporter.py +0 -0
  74. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/coverage/tracker.py +0 -0
  75. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/decorators.py +0 -0
  76. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/documentation/__init__.py +0 -0
  77. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/documentation/checker.py +0 -0
  78. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/documentation/fixture.py +0 -0
  79. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/profiling/__init__.py +0 -0
  80. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/profiling/fixture.py +0 -0
  81. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/profiling/profiler.py +0 -0
  82. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/report.py +0 -0
  83. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/runner.py +0 -0
  84. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/security/__init__.py +0 -0
  85. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/security/fixture.py +0 -0
  86. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/security/gitleaks_scanner.py +0 -0
  87. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/security/pip_audit_scanner.py +0 -0
  88. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/security/safety_scanner.py +0 -0
  89. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/security/scanner.py +0 -0
  90. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/security/semgrep_scanner.py +0 -0
  91. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/quality/security/trufflehog_scanner.py +0 -0
  92. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/streams/__init__.py +0 -0
  93. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/streams/testing.py +0 -0
  94. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/threading/__init__.py +0 -0
  95. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/threading/basic_fixtures.py +0 -0
  96. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/threading/data_fixtures.py +0 -0
  97. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/threading/execution_fixtures.py +0 -0
  98. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/threading/fixtures.py +0 -0
  99. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/threading/sync_fixtures.py +0 -0
  100. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/time/__init__.py +0 -0
  101. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/time/classes.py +0 -0
  102. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/time/controlled.py +0 -0
  103. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/time/fixtures.py +0 -0
  104. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/time/freezing.py +0 -0
  105. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/time/measurement.py +0 -0
  106. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/time/mocking.py +0 -0
  107. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/time/rate_limiting.py +0 -0
  108. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/transport/__init__.py +0 -0
  109. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/transport/fixtures.py +0 -0
  110. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/utils/__init__.py +0 -0
  111. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/utils/environment.py +0 -0
  112. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide/testkit/utils/temp_env.py +0 -0
  113. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide_testkit.egg-info/dependency_links.txt +0 -0
  114. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide_testkit.egg-info/entry_points.txt +0 -0
  115. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide_testkit.egg-info/requires.txt +0 -0
  116. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/src/provide_testkit.egg-info/top_level.txt +0 -0
  117. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/tests/test_async_fixtures.py +0 -0
  118. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/tests/test_early_init.py +0 -0
  119. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/tests/test_placeholder.py +0 -0
  120. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/tests/test_pth_installation.py +0 -0
  121. {provide_testkit-0.5.1 → provide_testkit-0.5.2}/tests/test_stream_encoding.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: provide-testkit
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Testing utilities and fixtures for the provide ecosystem.
5
5
  Author-email: Tim Perkins <code@tim.life>
6
6
  Maintainer-email: "provide.io" <code@provide.io>
@@ -0,0 +1 @@
1
+ 0.5.2
@@ -0,0 +1,81 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ """Capture that gives back the stream it took, not the one it remembers.
7
+
8
+ ``SysCaptureBase.suspend`` hands ``sys.stdout`` back to the stream pytest saved
9
+ before capture started, and ``resume`` reinstates ``tmpfile`` -- the stream
10
+ capture installed at ``start``. Neither looks at what is actually there. A test
11
+ that swaps the stream after capture started owns something pytest has no record
12
+ of, so the swap is discarded on the first suspend and every write after it lands
13
+ in pytest's buffer instead of the caller's.
14
+
15
+ ``log_cli = true`` makes that ordinary: pytest's live-log handler sits on the
16
+ root logger and suspends global capture around each record. One log line emitted
17
+ from inside ``CliRunner.invoke`` -- from any library, at any depth -- leaves
18
+ ``result.output`` empty while the text itself is real and lands in pytest's own
19
+ capture. ``contextlib.redirect_stdout`` and hand-rolled stream fixtures lose
20
+ their swap the same way.
21
+
22
+ The pair below remembers the stream that was in place at suspend and returns it
23
+ at resume, which is what the two methods already claim to do. Teardown is
24
+ untouched: ``done`` still restores the stream capture replaced, so a swap left
25
+ behind by a test cannot outlive it.
26
+
27
+ This belongs in pytest, and the tests in ``tests/test_capture_stream_swap.py``
28
+ drive the real ``SysCapture``, so they fail loudly once pytest changes the
29
+ methods underneath -- whether it fixes this or reshapes the class. Drop this
30
+ module when the pytest floor carries the fix.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import sys
36
+ from typing import Any
37
+
38
+ from _pytest.capture import SysCaptureBase
39
+
40
+ __all__ = ["install_capture_swap_fix"]
41
+
42
+ _INSTALLED_MARKER = "_provide_testkit_swap_safe"
43
+ # The stream is parked on pytest's object, so the name says whose it is.
44
+ _SWAPPED_IN = "_provide_testkit_swapped_in"
45
+
46
+
47
+ def _suspend(self: SysCaptureBase[Any]) -> None:
48
+ self._assert_state("suspend", ("started", "suspended"))
49
+ # Suspending while suspended is legal, and the stream sitting there then is
50
+ # the one suspend itself installed -- not a swap worth remembering.
51
+ if self._state == "started":
52
+ setattr(self, _SWAPPED_IN, getattr(sys, self.name))
53
+ setattr(sys, self.name, self._old)
54
+ self._state = "suspended"
55
+
56
+
57
+ def _resume(self: SysCaptureBase[Any]) -> None:
58
+ self._assert_state("resume", ("started", "suspended"))
59
+ if self._state == "started":
60
+ return
61
+ # A capture resumed without ever suspending has nothing recorded, and the
62
+ # stream capture installed is the right answer for it.
63
+ setattr(sys, self.name, getattr(self, _SWAPPED_IN, self.tmpfile))
64
+ self._state = "started"
65
+
66
+
67
+ def install_capture_swap_fix() -> bool:
68
+ """Make pytest's stream capture preserve a swap made after it started.
69
+
70
+ Returns whether this call is the one that installed it. Declining leaves
71
+ pytest exactly as found: the fix is worth having, and silently layering it
72
+ over a patch someone else installed is not.
73
+ """
74
+ if getattr(SysCaptureBase, _INSTALLED_MARKER, False):
75
+ return False
76
+ if any(getattr(SysCaptureBase, name).__module__ != "_pytest.capture" for name in ("suspend", "resume")):
77
+ return False
78
+ SysCaptureBase.suspend = _suspend # type: ignore[method-assign]
79
+ SysCaptureBase.resume = _resume # type: ignore[method-assign]
80
+ setattr(SysCaptureBase, _INSTALLED_MARKER, True)
81
+ return True
@@ -29,6 +29,7 @@ import sys
29
29
 
30
30
  import structlog
31
31
 
32
+ from provide.testkit._capture import install_capture_swap_fix
32
33
  from provide.testkit._streams import unicode_safe
33
34
 
34
35
  # Configure structlog with test-safe defaults BEFORE Foundation imports.
@@ -84,6 +85,12 @@ from provide.testkit._install_blocker import install_setproctitle_blocker # noq
84
85
 
85
86
  install_setproctitle_blocker(force=True)
86
87
 
88
+ # Pytest hands a suspended stream back to whatever it saved at start, so a test
89
+ # that swaps sys.stdout after that -- CliRunner.isolation, redirect_stdout --
90
+ # loses the swap the first time a live log record suspends capture, and its
91
+ # output lands in pytest's buffer instead. See provide.testkit._capture.
92
+ install_capture_swap_fix()
93
+
87
94
  __all__ = ["SetproctitleImportBlocker", "pytest_load_initial_conftests"]
88
95
 
89
96
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: provide-testkit
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Testing utilities and fixtures for the provide ecosystem.
5
5
  Author-email: Tim Perkins <code@tim.life>
6
6
  Maintainer-email: "provide.io" <code@provide.io>
@@ -6,6 +6,7 @@ src/provide/__init__.py
6
6
  src/provide/testkit/__init__.py
7
7
  src/provide/testkit/__main__.py
8
8
  src/provide/testkit/_blocker.py
9
+ src/provide/testkit/_capture.py
9
10
  src/provide/testkit/_early_init.py
10
11
  src/provide/testkit/_install_blocker.py
11
12
  src/provide/testkit/_install_pth.py
@@ -110,6 +111,7 @@ src/provide_testkit.egg-info/entry_points.txt
110
111
  src/provide_testkit.egg-info/requires.txt
111
112
  src/provide_testkit.egg-info/top_level.txt
112
113
  tests/test_async_fixtures.py
114
+ tests/test_capture_stream_swap.py
113
115
  tests/test_early_init.py
114
116
  tests/test_placeholder.py
115
117
  tests/test_pth_installation.py
@@ -0,0 +1,142 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ """Pytest's capture must give back the stream it took, not the one it remembers.
7
+
8
+ ``SysCaptureBase.resume`` reinstates the stream capture installed at ``start``.
9
+ Anything that swapped ``sys.stdout`` after that point -- ``CliRunner.isolation``,
10
+ ``redirect_stdout``, a fixture of one's own -- is dropped the first time capture
11
+ suspends, and every write after that lands in pytest's buffer. Live logging
12
+ (``log_cli = true``) suspends capture per record, so one log line emitted from
13
+ inside a Click command empties ``result.output`` while the text itself is real.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Iterator
19
+ import io
20
+ import sys
21
+
22
+ from _pytest.capture import SysCapture
23
+ import pytest
24
+
25
+ from provide.testkit._capture import install_capture_swap_fix
26
+
27
+ STDOUT_FD = 1
28
+
29
+
30
+ @pytest.fixture
31
+ def stdout_restored() -> Iterator[None]:
32
+ """Put ``sys.stdout`` back, however the test under it leaves things."""
33
+ saved = sys.stdout
34
+ try:
35
+ yield
36
+ finally:
37
+ sys.stdout = saved
38
+
39
+
40
+ @pytest.mark.usefixtures("stdout_restored")
41
+ def test_resume_returns_a_swap_made_after_capture_started() -> None:
42
+ """The case behind an empty ``CliRunner.result.output``."""
43
+ capture = SysCapture(STDOUT_FD)
44
+ capture.start()
45
+ swapped_in = io.StringIO()
46
+ sys.stdout = swapped_in
47
+
48
+ capture.suspend()
49
+ capture.resume()
50
+
51
+ assert sys.stdout is swapped_in
52
+ capture.done()
53
+
54
+
55
+ @pytest.mark.usefixtures("stdout_restored")
56
+ def test_resume_returns_the_capture_stream_when_nothing_swapped_it() -> None:
57
+ """The ordinary path stays exactly as pytest wrote it."""
58
+ capture = SysCapture(STDOUT_FD)
59
+ capture.start()
60
+
61
+ capture.suspend()
62
+ capture.resume()
63
+
64
+ assert sys.stdout is capture.tmpfile
65
+ capture.done()
66
+
67
+
68
+ @pytest.mark.usefixtures("stdout_restored")
69
+ def test_a_second_suspend_does_not_overwrite_the_remembered_swap() -> None:
70
+ """Suspending while suspended is legal, and must not record ``_old`` as the swap."""
71
+ capture = SysCapture(STDOUT_FD)
72
+ capture.start()
73
+ swapped_in = io.StringIO()
74
+ sys.stdout = swapped_in
75
+
76
+ capture.suspend()
77
+ capture.suspend()
78
+ capture.resume()
79
+
80
+ assert sys.stdout is swapped_in
81
+ capture.done()
82
+
83
+
84
+ @pytest.mark.usefixtures("stdout_restored")
85
+ def test_done_restores_the_stream_capture_replaced() -> None:
86
+ """Teardown belongs to pytest: a foreign swap does not outlive the capture."""
87
+ original = sys.stdout
88
+ capture = SysCapture(STDOUT_FD)
89
+ capture.start()
90
+ sys.stdout = io.StringIO()
91
+
92
+ capture.done()
93
+
94
+ assert sys.stdout is original
95
+
96
+
97
+ def test_installing_the_fix_again_reports_that_it_was_already_there() -> None:
98
+ """The plugin installs it at import; a second call must not stack a layer."""
99
+ assert install_capture_swap_fix() is False
100
+
101
+
102
+ CLI_UNDER_LIVE_LOGGING = """
103
+ import logging
104
+
105
+ import click
106
+ from click.testing import CliRunner
107
+
108
+
109
+ @click.group(invoke_without_command=True)
110
+ @click.pass_context
111
+ def cli(ctx: click.Context) -> None:
112
+ # Any library logging from inside the command does this; the record only has
113
+ # to reach the root logger, where live logging is listening.
114
+ logging.getLogger("some.library").warning("a record reaches the root logger")
115
+
116
+
117
+ @cli.command()
118
+ def sub() -> None:
119
+ click.echo("SUBCOMMAND OUTPUT")
120
+
121
+
122
+ def test_the_runner_sees_what_the_command_printed() -> None:
123
+ assert CliRunner().invoke(cli, ["sub"]).output != ""
124
+ """
125
+
126
+
127
+ def test_a_cli_runner_keeps_its_output_across_a_live_log_record(pytester: pytest.Pytester) -> None:
128
+ """The reported symptom, through the real Click and the real live-log handler."""
129
+ pytester.makepyfile(CLI_UNDER_LIVE_LOGGING)
130
+
131
+ result = pytester.runpytest_subprocess("-o", "log_cli=true")
132
+
133
+ result.assert_outcomes(passed=1)
134
+
135
+
136
+ def test_the_same_run_loses_its_output_without_this_plugin(pytester: pytest.Pytester) -> None:
137
+ """Without this, the test above could pass for the wrong reason."""
138
+ pytester.makepyfile(CLI_UNDER_LIVE_LOGGING)
139
+
140
+ result = pytester.runpytest_subprocess("-o", "log_cli=true", "-p", "no:provide_testkit")
141
+
142
+ result.assert_outcomes(failed=1)
@@ -1 +0,0 @@
1
- 0.5.1
File without changes