gabriel / muse public
test_terminal_supercharge.py python
288 lines 13.5 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Seven-tier tests for ``muse/core/terminal.py`` — ``use_color``.
2
3 Tiers
4 -----
5 Unit — each branch: NO_COLOR set, TERM=dumb, isatty True/False.
6 Integration — env var combinations, interaction with stdout redirection.
7 End-to-end — CLI commands respect use_color (no ANSI in piped output).
8 Stress — 100 000 calls; concurrent calls with env mutation.
9 Data integrity — return value is strictly bool; NO_COLOR value doesn't matter.
10 Security — hostile env var values do not crash or inject.
11 Performance — 100 000 calls under 1 s.
12 """
13
14 from __future__ import annotations
15
16 import os
17 import subprocess
18 import sys
19 import threading
20 import time
21 from unittest import mock
22
23 import pytest
24
25
26 # ──────────────────────────────────────────────────────────────────────────────
27 # Unit — branch-by-branch
28 # ──────────────────────────────────────────────────────────────────────────────
29
30
31 class TestUnit:
32 def test_no_color_set_returns_false(self) -> None:
33 from muse.core.terminal import use_color
34 with mock.patch.dict(os.environ, {"NO_COLOR": "1"}, clear=False):
35 assert use_color() is False
36
37 def test_no_color_empty_string_returns_false(self) -> None:
38 """NO_COLOR spec says any value (including empty) disables colour."""
39 from muse.core.terminal import use_color
40 with mock.patch.dict(os.environ, {"NO_COLOR": ""}, clear=False):
41 assert use_color() is False
42
43 def test_term_dumb_returns_false(self) -> None:
44 from muse.core.terminal import use_color
45 env = {k: v for k, v in os.environ.items() if k != "NO_COLOR"}
46 env["TERM"] = "dumb"
47 with mock.patch.dict(os.environ, env, clear=True):
48 assert use_color() is False
49
50 def test_tty_true_no_inhibitors_returns_true(self) -> None:
51 from muse.core.terminal import use_color
52 env = {k: v for k, v in os.environ.items()
53 if k not in ("NO_COLOR", "TERM")}
54 with mock.patch.dict(os.environ, env, clear=True), \
55 mock.patch("sys.stdout") as mock_stdout:
56 mock_stdout.isatty.return_value = True
57 assert use_color() is True
58
59 def test_tty_false_returns_false(self) -> None:
60 from muse.core.terminal import use_color
61 env = {k: v for k, v in os.environ.items()
62 if k not in ("NO_COLOR", "TERM")}
63 with mock.patch.dict(os.environ, env, clear=True), \
64 mock.patch("sys.stdout") as mock_stdout:
65 mock_stdout.isatty.return_value = False
66 assert use_color() is False
67
68 def test_returns_bool_type(self) -> None:
69 from muse.core.terminal import use_color
70 with mock.patch.dict(os.environ, {"NO_COLOR": "1"}):
71 result = use_color()
72 assert type(result) is bool
73
74
75 # ──────────────────────────────────────────────────────────────────────────────
76 # Integration — env var combinations
77 # ──────────────────────────────────────────────────────────────────────────────
78
79
80 class TestIntegration:
81 def test_no_color_beats_tty(self) -> None:
82 """NO_COLOR must win even when stdout is a TTY."""
83 from muse.core.terminal import use_color
84 with mock.patch.dict(os.environ, {"NO_COLOR": "1"}), \
85 mock.patch("sys.stdout") as mock_stdout:
86 mock_stdout.isatty.return_value = True
87 assert use_color() is False
88
89 def test_term_dumb_beats_tty(self) -> None:
90 from muse.core.terminal import use_color
91 env = {k: v for k, v in os.environ.items() if k != "NO_COLOR"}
92 env["TERM"] = "dumb"
93 with mock.patch.dict(os.environ, env, clear=True), \
94 mock.patch("sys.stdout") as mock_stdout:
95 mock_stdout.isatty.return_value = True
96 assert use_color() is False
97
98 def test_no_color_and_term_dumb_both_set_returns_false(self) -> None:
99 from muse.core.terminal import use_color
100 with mock.patch.dict(os.environ, {"NO_COLOR": "1", "TERM": "dumb"}):
101 assert use_color() is False
102
103 def test_term_xterm_not_dumb_does_not_inhibit(self) -> None:
104 from muse.core.terminal import use_color
105 env = {k: v for k, v in os.environ.items() if k != "NO_COLOR"}
106 env["TERM"] = "xterm-256color"
107 with mock.patch.dict(os.environ, env, clear=True), \
108 mock.patch("sys.stdout") as mock_stdout:
109 mock_stdout.isatty.return_value = True
110 assert use_color() is True
111
112 def test_no_color_unset_term_non_dumb_tty_false_returns_false(self) -> None:
113 from muse.core.terminal import use_color
114 env = {k: v for k, v in os.environ.items()
115 if k not in ("NO_COLOR",)}
116 env["TERM"] = "xterm"
117 with mock.patch.dict(os.environ, env, clear=True), \
118 mock.patch("sys.stdout") as mock_stdout:
119 mock_stdout.isatty.return_value = False
120 assert use_color() is False
121
122
123 # ──────────────────────────────────────────────────────────────────────────────
124 # End-to-end — subprocess with NO_COLOR; CLI output has no ANSI
125 # ──────────────────────────────────────────────────────────────────────────────
126
127
128 class TestEndToEnd:
129 def test_no_color_env_prevents_ansi_in_subprocess(self, tmp_path) -> None:
130 """When NO_COLOR is set, use_color() returns False in a fresh process."""
131 script = "from muse.core.terminal import use_color; print(use_color())"
132 result = subprocess.run(
133 [sys.executable, "-c", script],
134 env={**os.environ, "NO_COLOR": "1"},
135 capture_output=True, text=True,
136 )
137 assert result.stdout.strip() == "False"
138
139 def test_term_dumb_prevents_ansi_in_subprocess(self, tmp_path) -> None:
140 script = "from muse.core.terminal import use_color; print(use_color())"
141 env = {k: v for k, v in os.environ.items() if k != "NO_COLOR"}
142 env["TERM"] = "dumb"
143 result = subprocess.run(
144 [sys.executable, "-c", script],
145 env=env, capture_output=True, text=True,
146 )
147 assert result.stdout.strip() == "False"
148
149 def test_piped_output_is_not_tty(self) -> None:
150 """When stdout is redirected to a pipe, isatty() returns False."""
151 script = "import sys; print(sys.stdout.isatty())"
152 result = subprocess.run(
153 [sys.executable, "-c", script],
154 capture_output=True, text=True,
155 )
156 assert result.stdout.strip() == "False"
157
158
159 # ──────────────────────────────────────────────────────────────────────────────
160 # Stress
161 # ──────────────────────────────────────────────────────────────────────────────
162
163
164 class TestStress:
165 def test_100000_calls_no_crash(self) -> None:
166 from muse.core.terminal import use_color
167 with mock.patch.dict(os.environ, {"NO_COLOR": "1"}):
168 for _ in range(100_000):
169 use_color()
170
171 def test_concurrent_calls_consistent(self) -> None:
172 """Concurrent calls with NO_COLOR=1 must all return False."""
173 from muse.core.terminal import use_color
174 results: list[bool] = []
175 lock = threading.Lock()
176
177 def _call() -> None:
178 with mock.patch.dict(os.environ, {"NO_COLOR": "1"}):
179 v = use_color()
180 with lock:
181 results.append(v)
182
183 threads = [threading.Thread(target=_call) for _ in range(100)]
184 for t in threads:
185 t.start()
186 for t in threads:
187 t.join()
188 assert all(v is False for v in results)
189 assert len(results) == 100
190
191 def test_alternating_no_color_calls(self) -> None:
192 from muse.core.terminal import use_color
193 for i in range(10_000):
194 with mock.patch.dict(os.environ, {"NO_COLOR": "1"} if i % 2 == 0 else {}):
195 result = use_color()
196 if i % 2 == 0:
197 assert result is False
198
199
200 # ──────────────────────────────────────────────────────────────────────────────
201 # Data integrity
202 # ──────────────────────────────────────────────────────────────────────────────
203
204
205 class TestDataIntegrity:
206 def test_return_is_always_bool(self) -> None:
207 from muse.core.terminal import use_color
208 for env_patch, isatty in [
209 ({"NO_COLOR": "1"}, True),
210 ({}, False),
211 ]:
212 with mock.patch.dict(os.environ, env_patch), \
213 mock.patch("sys.stdout") as m:
214 m.isatty.return_value = isatty
215 result = use_color()
216 assert type(result) is bool
217
218 def test_no_color_any_value_returns_false(self) -> None:
219 """NO_COLOR spec: any non-empty value disables colour."""
220 from muse.core.terminal import use_color
221 for val in ("1", "true", "yes", "0", "false", "no", "anything"):
222 with mock.patch.dict(os.environ, {"NO_COLOR": val}):
223 assert use_color() is False
224
225 def test_result_is_deterministic_for_same_env(self) -> None:
226 from muse.core.terminal import use_color
227 with mock.patch.dict(os.environ, {"NO_COLOR": "1"}):
228 results = {use_color() for _ in range(1000)}
229 assert results == {False}
230
231
232 # ──────────────────────────────────────────────────────────────────────────────
233 # Security
234 # ──────────────────────────────────────────────────────────────────────────────
235
236
237 class TestSecurity:
238 def test_ansi_in_no_color_value_does_not_crash(self) -> None:
239 from muse.core.terminal import use_color
240 with mock.patch.dict(os.environ, {"NO_COLOR": "\x1b[31mred\x1b[0m"}):
241 result = use_color()
242 assert result is False # any value for NO_COLOR disables colour
243
244 def test_null_byte_in_term_rejected_by_os(self) -> None:
245 """os.environ rejects null bytes at the OS level — verify the ValueError."""
246 env = {k: v for k, v in os.environ.items() if k != "NO_COLOR"}
247 env["TERM"] = "xterm\x00evil"
248 with pytest.raises((ValueError, TypeError)):
249 mock.patch.dict(os.environ, env, clear=True).__enter__()
250
251 def test_very_long_no_color_value_does_not_crash(self) -> None:
252 from muse.core.terminal import use_color
253 with mock.patch.dict(os.environ, {"NO_COLOR": "x" * 100_000}):
254 assert use_color() is False
255
256 def test_very_long_term_value_does_not_crash(self) -> None:
257 from muse.core.terminal import use_color
258 env = {k: v for k, v in os.environ.items() if k != "NO_COLOR"}
259 env["TERM"] = "x" * 100_000
260 with mock.patch.dict(os.environ, env, clear=True), \
261 mock.patch("sys.stdout") as m:
262 m.isatty.return_value = False
263 result = use_color()
264 assert type(result) is bool
265
266
267 # ──────────────────────────────────────────────────────────────────────────────
268 # Performance
269 # ──────────────────────────────────────────────────────────────────────────────
270
271
272 class TestPerformance:
273 def test_100000_calls_under_1s(self) -> None:
274 from muse.core.terminal import use_color
275 with mock.patch.dict(os.environ, {"NO_COLOR": "1"}):
276 start = time.perf_counter()
277 for _ in range(100_000):
278 use_color()
279 elapsed = time.perf_counter() - start
280 assert elapsed < 1.0, f"100 000 calls took {elapsed:.2f}s — expected < 1s"
281
282 def test_single_call_under_1ms(self) -> None:
283 from muse.core.terminal import use_color
284 with mock.patch.dict(os.environ, {"NO_COLOR": "1"}):
285 start = time.perf_counter()
286 use_color()
287 elapsed = time.perf_counter() - start
288 assert elapsed < 0.001, f"Single call took {elapsed*1000:.2f}ms — expected < 1ms"
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago