-
-
Notifications
You must be signed in to change notification settings - Fork 106
Expand file tree
/
Copy pathtest_fast_default.py
More file actions
250 lines (198 loc) · 7.8 KB
/
Copy pathtest_fast_default.py
File metadata and controls
250 lines (198 loc) · 7.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
from __future__ import annotations
import gc
import io
import sys
import progressbar
# Alias (not a `from` import) so CodeQL doesn't flag `progressbar` as imported
# with both `import` and `import from`.
fast_module = progressbar.fast
class TTY(io.StringIO):
def isatty(self) -> bool:
return True
def repaints(self) -> list[str]:
return [p for p in self.getvalue().split('\r') if p]
def test_fast_known_length_renders_and_completes():
fd = TTY()
bar = fast_module.FastProgressBar(max_value=1000, fd=fd)
out = list(bar(range(1000)))
assert out == list(range(1000))
assert bar.value == 1000
assert bar.percentage == 100.0
assert bar._finished
frames = fd.repaints()
assert frames, 'fast bar drew nothing'
# Close-to-default look: percentage, (n of max), a bar, Elapsed/ETA.
last = frames[-1]
assert '100%' in last
assert '(1000 of 1000)' in last
assert '|' in last # bar delimiters
assert 'Elapsed Time:' in last
def test_fast_elapsed_with_no_start_time():
"""Test _fast_elapsed returns 0 when start_time is None."""
fd = TTY()
bar = fast_module.FastProgressBar(max_value=100, fd=fd)
assert bar._fast_elapsed() == 0.0
def test_fast_format_line_with_eta_calculation():
"""Test ETA calculation path with done > 0 and elapsed > 0."""
from datetime import datetime, timedelta
fd = TTY()
bar = fast_module.FastProgressBar(max_value=100, fd=fd)
# Manually set start_time to the past to ensure elapsed > 0
bar.start_time = datetime.now() - timedelta(seconds=2)
bar.value = 50 # Set done=50 to enable ETA calculation
line = bar._format_line()
# With done > 0 and elapsed > 0, ETA is computed instead of '--:--:--'.
assert 'ETA:' in line
assert 'ETA: --:--:--' not in line
def test_fast_spinner_frames_cycle():
"""The spinner is exactly four frames and cycles through all of them.
Regression: the raw literal ``r'|/-\\'`` is 5 chars long (the escape is not
collapsed), so the intended four-frame cycle was fragile and length-coupled
to a hardcoded ``% 4``.
"""
from datetime import datetime, timedelta
assert len(fast_module._SPINNER_FRAMES) == 4
assert set(fast_module._SPINNER_FRAMES) == set('|/-\\')
fd = TTY()
bar = fast_module.FastProgressBar(
max_value=progressbar.UnknownLength, fd=fd
)
bar.start_time = datetime(2020, 1, 1)
seen = []
for quarter in range(4):
# Freeze elapsed at exact quarter-seconds so int(elapsed * 4) walks
# 0, 1, 2, 3 and must surface each distinct frame.
bar.end_time = bar.start_time + timedelta(seconds=quarter / 4)
seen.append(bar._format_line().lstrip()[0])
assert seen == ['|', '/', '-', '\\']
def test_fast_format_line_uses_native_hook(monkeypatch):
"""The native `_format_fast_line` hook takes precedence when set."""
def stub(bar) -> str:
return 'NATIVE_HOOK_OUTPUT'
monkeypatch.setattr(fast_module, '_format_fast_line', stub)
fd = TTY()
bar = fast_module.FastProgressBar(max_value=100, fd=fd)
assert bar._format_line() == 'NATIVE_HOOK_OUTPUT'
def test_fast_unknown_length_renders_count_and_elapsed():
fd = TTY()
bar = fast_module.FastProgressBar(
max_value=progressbar.UnknownLength, fd=fd
)
out = list(bar(iter(range(40))))
assert out == list(range(40))
assert bar.value == 39
last = fd.repaints()[-1]
assert 'Elapsed Time:' in last
assert '40' in last # the count is shown
assert ' of ' not in last # no "(n of max)" when length unknown
def test_fast_prefix_suffix_in_line_not_widgets():
fd = TTY()
bar = fast_module.FastProgressBar(
max_value=10, fd=fd, prefix='load ', suffix=' done'
)
list(bar(range(10)))
assert bar.widgets == [] # prefix/suffix not injected as widgets
last = fd.repaints()[-1]
assert last.lstrip().startswith('load')
assert 'done' in last
def test_fast_empty_iterable():
fd = TTY()
bar = fast_module.FastProgressBar(max_value=0, fd=fd)
assert list(bar([])) == []
assert bar._finished
def test_fast_break_restores_streams():
real_out = sys.stdout
fd = TTY()
bar = fast_module.FastProgressBar(
max_value=1000, fd=fd, redirect_stdout=True
)
for i in bar(range(1000)):
if i == 5:
break
del bar
gc.collect()
assert sys.stdout is real_out
def test_fast_with_statement():
fd = TTY()
with fast_module.FastProgressBar(max_value=10, fd=fd) as bar:
out = list(bar(range(10)))
assert out == list(range(10))
assert bar._finished
def test_shortcut_dispatch(monkeypatch):
# Record which class the shortcut constructs for each input combination.
from progressbar import shortcuts
calls = {'fast': 0, 'full': 0}
class FastSpy(fast_module.FastProgressBar):
def __init__(self, *a, **k):
calls['fast'] += 1
super().__init__(*a, **k)
class FullSpy(progressbar.ProgressBar):
def __init__(self, *a, **k):
calls['full'] += 1
super().__init__(*a, **k)
monkeypatch.setattr(shortcuts.fast_module, 'FastProgressBar', FastSpy)
monkeypatch.setattr(shortcuts.bar, 'ProgressBar', FullSpy)
# Default (no widgets, no fast flag) -> fast.
assert list(shortcuts.progressbar(range(3), fd=TTY())) == [0, 1, 2]
assert calls == {'fast': 1, 'full': 0}
# Custom widgets -> full.
list(
shortcuts.progressbar(
range(3), fd=TTY(), widgets=[progressbar.Percentage()]
)
)
assert calls == {'fast': 1, 'full': 1}
# fast=False -> full even with no widgets.
list(shortcuts.progressbar(range(3), fd=TTY(), fast=False))
assert calls == {'fast': 1, 'full': 2}
# Env override forces full.
monkeypatch.setenv('PROGRESSBAR_DISABLE_FASTPATH', '1')
list(shortcuts.progressbar(range(3), fd=TTY()))
assert calls == {'fast': 1, 'full': 3}
# Dynamic variables force full (the fast formatter can't render them).
monkeypatch.delenv('PROGRESSBAR_DISABLE_FASTPATH', raising=False)
list(shortcuts.progressbar(range(3), fd=TTY(), variables={'x': 1}))
assert calls == {'fast': 1, 'full': 4}
def test_full_bar_injects_prefix_suffix_widgets():
# The full ProgressBar (unlike the fast bar) injects prefix/suffix as
# FormatLabel widgets in start(); exercise that path directly.
fd = TTY()
bar_ = progressbar.ProgressBar(
max_value=10, fd=fd, prefix='pre ', suffix=' suf'
)
list(bar_(range(10)))
assert bar_.widgets # widgets were built (not the fast empty list)
last = fd.repaints()[-1]
assert 'pre' in last
assert 'suf' in last
def test_import_progressbar_is_lazy():
# A fresh interpreter: `import progressbar` must not eagerly pull heavy
# submodules; FastProgressBar still resolves lazily.
import subprocess
check = (
'import sys, progressbar\n'
'assert "progressbar.multi" not in sys.modules\n'
'assert progressbar.FastProgressBar is not None\n'
'print("ok")\n'
)
out = subprocess.run(
[sys.executable, '-c', check], capture_output=True, text=True
)
assert out.returncode == 0, out.stderr
assert 'ok' in out.stdout
def test_fast_path_does_not_import_widgets_or_colors():
# Running the fast default end-to-end must not pull in the widgets module
# or the terminal colour tables (the heaviest imports).
import subprocess
check = (
'import sys, progressbar\n'
'list(progressbar.progressbar(range(10)))\n'
'assert "progressbar.widgets" not in sys.modules\n'
'assert "progressbar.terminal.colors" not in sys.modules\n'
'print("ok")\n'
)
out = subprocess.run(
[sys.executable, '-c', check], capture_output=True, text=True
)
assert out.returncode == 0, out.stderr
assert 'ok' in out.stdout