Coverage for tests / test_progress_bar.py: 100%
244 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
1"""
2Tests for the Progress bar functionality.
3Created after vendoring Click to ensure test coverage is back up to 100%.
4"""
6import io 1cdabefg
7import shutil 1cdabefg
9import pytest 1cdabefg
10import typer 1cdabefg
11from typer import progressbar 1cdabefg
12from typer._click import _termui_impl 1cdabefg
13from typer.testing import CliRunner 1cdabefg
15runner = CliRunner() 1cdabefg
18def _fake_clock(monkeypatch: pytest.MonkeyPatch) -> list[float]: 1cdabefg
19 clock = [0.0] 1cdabefg
20 monkeypatch.setattr(_termui_impl.time, "time", lambda: clock[0]) 1cdabefg
21 return clock 1cdabefg
24def _pbar(**kw): 1cdabefg
25 return progressbar(file=kw.pop("file", io.StringIO()), **kw) 1cdabefg
28@pytest.mark.parametrize( 1cdabefg
29 ("iterable", "length", "hidden", "label", "expected_count"),
30 [
31 (["a", "b"], None, False, "Processing", 2),
32 (None, 3, False, "Counting", 3),
33 (["x", "y"], None, True, "Hidden", 2),
34 ],
35)
36def test_progressbar(iterable, length, hidden, label, expected_count): 1cdabefg
37 app = typer.Typer() 1cdabefg
39 @app.command() 1cdabefg
40 def main(): 1cdabefg
41 bar_out = io.StringIO() 1cdabefg
42 count = 0 1cdabefg
43 with progressbar( 1cdabefg
44 iterable, length=length, hidden=hidden, label=label, file=bar_out
45 ) as bar:
46 for _ in bar: 1cdabefg
47 count += 1 1cdabefg
48 typer.echo(f"count={count}") 1cdabefg
49 typer.echo(f"bar={bar_out.getvalue()!r}") 1cdabefg
51 result = runner.invoke(app, []) 1cdabefg
52 assert result.exit_code == 0, result.output 1cdabefg
53 assert f"count={expected_count}" in result.stdout 1cdabefg
54 assert (label in result.stdout) == (not hidden) 1cdabefg
57@pytest.mark.parametrize( 1cdabefg
58 ("label", "pbar_kw", "must_contain", "must_not_contain"),
59 [
60 pytest.param(
61 "TTY",
62 {
63 "show_pos": True,
64 "show_percent": True,
65 "item_show_func": lambda item: f"item={item}",
66 },
67 ("TTY", "1/1", "100%", "item=x"),
68 (),
69 ),
70 pytest.param(
71 "HeurPct",
72 {},
73 ("HeurPct", "100%"),
74 ("1/1",),
75 ),
76 pytest.param(
77 "HeurPos",
78 {"show_pos": True},
79 ("HeurPos", "1/1"),
80 ("100%",),
81 ),
82 ],
83)
84def test_progressbar_tty( 1cdabefg
85 monkeypatch, label: str, pbar_kw: dict, must_contain, must_not_contain
86):
87 monkeypatch.setattr(_termui_impl, "isatty", lambda f: True) 1cdabefg
88 _fake_clock(monkeypatch) 1cdabefg
90 app = typer.Typer() 1cdabefg
92 @app.command() 1cdabefg
93 def main(): 1cdabefg
94 bar_out = io.StringIO() 1cdabefg
95 with progressbar( 1cdabefg
96 ["x"],
97 label=label,
98 file=bar_out,
99 bar_template="%(label)s %(info)s",
100 width=1,
101 **pbar_kw,
102 ) as bar:
103 for _ in bar: 1cdabefg
104 pass 1cdabefg
105 typer.echo(bar_out.getvalue()) 1cdabefg
107 result = runner.invoke(app, []) 1cdabefg
108 assert result.exit_code == 0, result.output 1cdabefg
109 for part in must_contain: 1cdabefg
110 assert part in result.stdout 1cdabefg
111 for part in must_not_contain: 1cdabefg
112 assert part not in result.stdout 1cdabefg
115def test_progressbar_tty_show_eta(monkeypatch): 1cdabefg
116 monkeypatch.setattr(_termui_impl, "isatty", lambda f: True) 1cdabefg
117 clock = _fake_clock(monkeypatch) 1cdabefg
118 clock[0] = 1_000.0 1cdabefg
120 app = typer.Typer() 1cdabefg
122 @app.command() 1cdabefg
123 def main(): 1cdabefg
124 bar_out = io.StringIO() 1cdabefg
125 with progressbar( 1cdabefg
126 ["a", "b"],
127 label="ETA",
128 file=bar_out,
129 show_pos=True,
130 show_percent=False,
131 show_eta=True,
132 bar_template="%(label)s %(info)s",
133 width=1,
134 ) as bar:
135 for i, _ in enumerate(bar): 1cdabefg
136 if i == 0: 1cdabefg
137 clock[0] = 1_001.0 1cdabefg
138 typer.echo(bar_out.getvalue()) 1cdabefg
140 result = runner.invoke(app, []) 1cdabefg
141 assert result.exit_code == 0, result.output 1cdabefg
142 for part in ("ETA", "1/2", "00:00:01"): 1cdabefg
143 assert part in result.stdout 1cdabefg
146def test_progressbar_autowidth(monkeypatch): 1cdabefg
147 monkeypatch.setattr(_termui_impl, "isatty", lambda f: True) 1cdabefg
148 call = [0] 1cdabefg
149 real_get_terminal_size = shutil.get_terminal_size 1cdabefg
151 def fake_get_terminal_size(*args, **kwargs): 1cdabefg
152 # Pytest (and others) call get_terminal_size(fallback=...); only stub no-arg calls
153 if args or kwargs: 1cdabefg
154 return real_get_terminal_size(*args, **kwargs) 1ab
155 col = 120 if call[0] == 0 else 40 1cdabefg
156 call[0] += 1 1cdabefg
157 return type("TS", (), {"columns": col, "lines": 24})() 1cdabefg
159 monkeypatch.setattr(shutil, "get_terminal_size", fake_get_terminal_size) 1cdabefg
161 state: dict[str, object] = {} 1cdabefg
163 app = typer.Typer() 1cdabefg
165 @app.command() 1cdabefg
166 def main(): 1cdabefg
167 out = io.StringIO() 1cdabefg
168 with progressbar(["a", "b"], width=0, label="AW", file=out) as bar: 1cdabefg
169 state["autowidth"] = bar.autowidth 1cdabefg
170 for _ in bar: 1cdabefg
171 pass 1cdabefg
172 state["call_count"] = call[0] 1cdabefg
173 state["out"] = out.getvalue() 1cdabefg
174 typer.echo("done") 1cdabefg
176 result = runner.invoke(app, []) 1cdabefg
177 assert result.exit_code == 0, result.output 1cdabefg
178 assert state["autowidth"] is True 1cdabefg
179 assert state["call_count"] >= 2 1cdabefg
180 out = str(state["out"]) 1cdabefg
181 assert "\r" in out and "AW" in out 1cdabefg
182 assert "0%" in out and "50%" in out and "100%" in out 1cdabefg
185def test_progress_bar_iter(): 1cdabefg
186 not_entered = _pbar(iterable=[1, 2], length=2) 1cdabefg
187 with pytest.raises(RuntimeError, match="with block"): 1cdabefg
188 iter(not_entered) 1cdabefg
190 entered = _pbar(iterable=[10, 20], length=2) 1cdabefg
191 with entered: 1cdabefg
192 iterator = iter(entered) 1cdabefg
193 assert next(iterator) == 10 1cdabefg
194 assert next(entered) == 20 1cdabefg
195 with pytest.raises(StopIteration): 1cdabefg
196 next(iterator) 1cdabefg
199def test_progress_bar_time(monkeypatch): 1cdabefg
200 clock = _fake_clock(monkeypatch) 1cdabefg
201 state: dict[str, object] = {} 1cdabefg
202 clock[0] = 1_000.0 1cdabefg
204 app = typer.Typer() 1cdabefg
206 @app.command() 1cdabefg
207 def main(): 1cdabefg
208 bar = _pbar(iterable=None, length=10) 1cdabefg
209 state["tpi0"] = bar.time_per_iteration 1cdabefg
210 clock[0] = 1_000.5 1cdabefg
211 bar.make_step(1) 1cdabefg
212 state["avg_after_one"] = list(bar.avg) 1cdabefg
213 state["tpi_after_one"] = bar.time_per_iteration 1cdabefg
214 clock[0] = 1_001.0 1cdabefg
215 bar.make_step(1) 1cdabefg
216 state["pos2"] = bar.pos 1cdabefg
217 state["avg2"] = list(bar.avg) 1cdabefg
218 state["tpi2"] = bar.time_per_iteration 1cdabefg
219 clock[0] = 1_002.0 1cdabefg
220 bar.make_step(1) 1cdabefg
221 state["avg3"] = list(bar.avg) 1cdabefg
222 state["tpi3"] = bar.time_per_iteration 1cdabefg
223 typer.echo("ok") 1cdabefg
225 result = runner.invoke(app, []) 1cdabefg
226 assert result.exit_code == 0, result.output 1cdabefg
227 assert state["tpi0"] == 0.0 1cdabefg
228 assert state["avg_after_one"] == [] and state["tpi_after_one"] == 0.0 1cdabefg
229 assert state["pos2"] == 2 1cdabefg
230 assert state["avg2"] == [(1_001.0 - 1_000.0) / 2.0] 1cdabefg
231 assert state["tpi2"] == pytest.approx(0.5) 1cdabefg
232 assert state["avg3"] == [0.5, (1_002.0 - 1_000.0) / 3.0] 1cdabefg
233 assert state["tpi3"] == pytest.approx(sum(state["avg3"]) / 2.0) # type: ignore[arg-type] 1cdabefg
236def test_progress_bar_time_zero_steps(monkeypatch): 1cdabefg
237 clock = _fake_clock(monkeypatch) 1cdabefg
238 state: dict[str, object] = {} 1cdabefg
239 clock[0] = 2_000.0 1cdabefg
241 app = typer.Typer() 1cdabefg
243 @app.command() 1cdabefg
244 def main(): 1cdabefg
245 bar = _pbar(iterable=None, length=3) 1cdabefg
246 clock[0] = 2_001.0 1cdabefg
247 bar.make_step(0) 1cdabefg
248 state["pos"] = bar.pos 1cdabefg
249 state["avg"] = list(bar.avg) 1cdabefg
250 state["tpi"] = bar.time_per_iteration 1cdabefg
251 typer.echo("ok") 1cdabefg
253 result = runner.invoke(app, []) 1cdabefg
254 assert result.exit_code == 0, result.output 1cdabefg
255 assert state["pos"] == 0 1cdabefg
256 assert state["avg"] == [1.0] 1cdabefg
257 assert state["tpi"] == pytest.approx(1.0) 1cdabefg
260def test_progress_bar_eta(monkeypatch): 1cdabefg
261 state: dict[str, object] = {} 1cdabefg
263 app = typer.Typer() 1cdabefg
265 @app.command() 1cdabefg
266 def main(): 1cdabefg
267 state["eta0"] = _pbar(iterable=[1, 2], length=None).eta 1cdabefg
269 done = _pbar(iterable=None, length=5) 1cdabefg
270 done.pos, done.finished = 2, True 1cdabefg
271 state["eta_done"] = done.eta 1cdabefg
273 fresh = _pbar(iterable=None, length=5) 1cdabefg
274 state["eta_known_fresh"] = fresh.eta_known 1cdabefg
275 state["fmt_eta_fresh"] = fresh.format_eta() 1cdabefg
277 clock = _fake_clock(monkeypatch) 1cdabefg
278 clock[0] = 5_000.0 1cdabefg
279 bar = _pbar(iterable=None, length=10) 1cdabefg
280 clock[0] = 5_001.0 1cdabefg
281 bar.make_step(3) 1cdabefg
282 state["pos3"] = bar.pos 1cdabefg
283 state["eta_after"] = bar.eta 1cdabefg
284 state["tpi"] = bar.time_per_iteration 1cdabefg
286 cases_out = [] 1cdabefg
287 for t0, t1, length, n_steps, _expected_fmt, expected_eta_int in ( 1cdabefg
288 (9_000.0, 9_001.0, 10, 1, "00:00:09", None),
289 (1_000.0, 100_000.0, 2, 1, "1d 03:30:00", 99_000),
290 ):
291 clock2 = _fake_clock(monkeypatch) 1cdabefg
292 clock2[0] = t0 1cdabefg
293 b = _pbar(iterable=None, length=length) 1cdabefg
294 clock2[0] = t1 1cdabefg
295 b.make_step(n_steps) 1cdabefg
296 cases_out.append( 1cdabefg
297 (
298 b.eta_known,
299 b.format_eta(),
300 int(b.eta) if expected_eta_int is not None else None,
301 )
302 )
304 state["cases"] = cases_out 1cdabefg
306 clock3 = _fake_clock(monkeypatch) 1cdabefg
307 clock3[0] = 3_000.0 1cdabefg
308 b2 = _pbar(iterable=None, length=2) 1cdabefg
309 clock3[0] = 3_001.0 1cdabefg
310 b2.make_step(1) 1cdabefg
311 state["fmt_before_finish"] = b2.format_eta() 1cdabefg
312 b2.finish() 1cdabefg
313 state["fmt_after_finish"] = b2.format_eta() 1cdabefg
314 typer.echo("ok") 1cdabefg
316 result = runner.invoke(app, []) 1cdabefg
317 assert result.exit_code == 0, result.output 1cdabefg
318 assert state["eta0"] == 0.0 1cdabefg
319 assert state["eta_done"] == 0.0 1cdabefg
320 assert not state["eta_known_fresh"] and state["fmt_eta_fresh"] == "" 1cdabefg
321 assert state["pos3"] == 3 1cdabefg
322 assert state["eta_after"] == pytest.approx(state["tpi"] * (10 - 3)) # type: ignore[operator] 1cdabefg
324 (ek1, fmt1, ei1), (ek2, fmt2, ei2) = state["cases"] # type: ignore[misc] 1cdabefg
325 assert ek1 and fmt1 == "00:00:09" and ei1 is None 1cdabefg
326 assert ek2 and fmt2 == "1d 03:30:00" and ei2 == 99_000 1cdabefg
328 assert state["fmt_before_finish"] != "" 1cdabefg
329 assert state["fmt_after_finish"] == "" 1cdabefg
332@pytest.mark.parametrize( 1cdabefg
333 ("width", "fill_char", "empty_char", "expected_bar", "finished", "sample_timing"),
334 [
335 pytest.param(4, "X", "-", "XXXX", True, False, id="finished"),
336 pytest.param(4, "#", "-", "----", False, False, id="no_timing_yet"),
337 pytest.param(5, "*", ".", None, False, True, id="indeterminate"),
338 ],
339)
340def test_progress_bar_unknown_length( 1cdabefg
341 monkeypatch,
342 width: int,
343 fill_char: str,
344 empty_char: str,
345 expected_bar: str | None,
346 finished: bool,
347 sample_timing: bool,
348):
349 clock: list[float] | None = _fake_clock(monkeypatch) if sample_timing else None 1cdabefg
350 if clock is not None: 1cdabefg
351 clock[0] = 100.0 1cdabefg
353 state: dict[str, object] = {} 1cdabefg
355 class _IterableWithoutLength: 1cdabefg
356 def __iter__(self): 1cdabefg
357 return iter((1, 2, 3)) 1cdabefg
359 app = typer.Typer() 1cdabefg
361 @app.command() 1cdabefg
362 def main(): 1cdabefg
363 bar = _pbar( 1cdabefg
364 iterable=_IterableWithoutLength(),
365 length=None,
366 width=width,
367 fill_char=fill_char,
368 empty_char=empty_char,
369 )
370 assert bar.length is None 1cdabefg
372 if sample_timing: 1cdabefg
373 assert clock is not None 1cdabefg
374 clock[0] = 101.0 1cdabefg
375 bar.make_step(1) 1cdabefg
376 assert bar.time_per_iteration > 0 1cdabefg
377 rendered = bar.format_bar() 1cdabefg
378 assert len(rendered) == width 1cdabefg
379 assert rendered.count(fill_char) == 1 1cdabefg
380 assert rendered.count(empty_char) == width - 1 1cdabefg
381 state["branch"] = "sample_timing" 1cdabefg
382 elif finished: 1cdabefg
383 bar.finished = True 1cdabefg
384 state["bar"] = bar.format_bar() 1cdabefg
385 state["branch"] = "finished" 1cdabefg
386 else:
387 assert bar.time_per_iteration == 0.0 1cdabefg
388 state["bar"] = bar.format_bar() 1cdabefg
389 state["branch"] = "no_timing" 1cdabefg
390 typer.echo("ok") 1cdabefg
392 result = runner.invoke(app, []) 1cdabefg
393 assert result.exit_code == 0, result.output 1cdabefg
394 if sample_timing: 1cdabefg
395 assert state["branch"] == "sample_timing" 1cdabefg
396 else:
397 assert state["bar"] == expected_bar 1cdabefg