-
-
Notifications
You must be signed in to change notification settings - Fork 106
Expand file tree
/
Copy pathtest_native_accelerator.py
More file actions
286 lines (216 loc) · 8.38 KB
/
Copy pathtest_native_accelerator.py
File metadata and controls
286 lines (216 loc) · 8.38 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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# tests/test_native_accelerator.py
"""Tests for the optional native (Cython) iterator accelerator.
Two groups:
* Integration-coverage tests that exercise ``ProgressBar.__iter__`` dispatch
and the ``_fast_*`` protocol hooks **without** needing the compiled
``speedups`` package (using a fake iterator / direct calls), so they run —
and keep ``bar.py`` at 100% coverage — in CI where ``speedups`` is absent.
* End-to-end equivalence tests marked ``@requires_speedups`` that drive the
real ``speedups.progressbar.FastBarIterator``; they run wherever it is
installed (dev/bench env) and are skipped otherwise.
The conftest ``disable_native_accelerator`` autouse fixture forces the
pure-Python path for the rest of the suite; here we restore the real iterator
explicitly where needed.
"""
from __future__ import annotations
import gc
import io
import re
import sys
import pytest
import progressbar
# Alias (not a `from` import) so CodeQL doesn't flag `progressbar` as imported
# with both `import` and `import from`.
bar_module = progressbar.bar
# Captured at import, before the autouse fixture nulls it for each test.
_REAL_FAST = bar_module._FastBarIterator
HAS_SPEEDUPS = _REAL_FAST is not None
requires_speedups = pytest.mark.skipif(
not HAS_SPEEDUPS,
reason='native accelerator (speedups package) not installed',
)
_PERCENT = re.compile(r'(\d+)%')
_ANSI = re.compile(r'\x1b\[[0-9;]*m')
class TTY(io.StringIO):
def isatty(self) -> bool:
return True
class RecordingTTY(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 _percentages(frames: list[str]) -> list[int]:
out: list[int] = []
for frame in frames:
match = _PERCENT.search(_ANSI.sub('', frame))
if match:
out.append(int(match.group(1)))
return out
class _FakeFast:
"""Stand-in for FastBarIterator: records construction, yields nothing.
Lets the native dispatch branch be covered without the compiled package.
"""
def __init__(self, bar, iterable):
self.bar = bar
self.iterable = iterable
def __iter__(self):
return self
def __next__(self):
raise StopIteration
# --- dispatch coverage (no compiled speedups required) --------------------
def test_iter_uses_native_when_available(monkeypatch):
monkeypatch.setattr(bar_module, '_FastBarIterator', _FakeFast)
bar = progressbar.ProgressBar(max_value=10, fd=TTY())
iterable = range(10)
it = iter(bar(iterable))
assert isinstance(it, _FakeFast)
assert it.bar is bar
assert it.iterable is bar._iterable
def test_iter_falls_back_when_native_absent(monkeypatch):
monkeypatch.setattr(bar_module, '_FastBarIterator', None)
bar = progressbar.ProgressBar(max_value=10, fd=TTY())
it = iter(bar(range(10)))
assert not isinstance(it, _FakeFast)
assert list(it) == list(range(10))
def test_iter_falls_back_without_iterable(monkeypatch):
# Native needs an iterable; iterating a bar without one must not use it.
monkeypatch.setattr(bar_module, '_FastBarIterator', _FakeFast)
bar = progressbar.ProgressBar(max_value=10, fd=TTY())
it = iter(bar)
assert not isinstance(it, _FakeFast)
def test_iter_falls_back_when_env_disabled(monkeypatch):
monkeypatch.setattr(bar_module, '_FastBarIterator', _FakeFast)
monkeypatch.setenv('PROGRESSBAR_DISABLE_FASTPATH', '1')
bar = progressbar.ProgressBar(max_value=10, fd=TTY())
it = iter(bar(range(10)))
assert not isinstance(it, _FakeFast)
assert list(it) == list(range(10))
# --- protocol hook unit coverage (no compiled speedups required) ----------
def test_fast_begin_starts_once():
bar = progressbar.ProgressBar(max_value=10, fd=TTY())
assert bar.start_time is None
bar._fast_begin()
assert bar.start_time is not None
started = bar.start_time
bar._fast_begin() # already started: no-op
assert bar.start_time is started
def test_fast_tick_updates_value():
bar = progressbar.ProgressBar(max_value=100, fd=TTY())
bar._fast_begin()
bar._fast_tick(50)
assert bar.value == 50
def test_fast_end_finishes_at_100():
bar = progressbar.ProgressBar(max_value=10, fd=TTY())
bar._fast_begin()
bar._fast_end()
assert bar._finished
assert bar.value == bar.max_value
def test_fast_end_dirty_keeps_partial_value():
bar = progressbar.ProgressBar(max_value=10, fd=TTY())
bar._fast_begin()
bar._fast_tick(3)
bar._fast_end_dirty()
assert bar._finished
assert bar.value == 3 # not snapped to max_value
# --- end-to-end with the real compiled accelerator ------------------------
@pytest.fixture
def native(monkeypatch):
"""Restore the real FastBarIterator for a single test."""
monkeypatch.setattr(bar_module, '_FastBarIterator', _REAL_FAST)
return _REAL_FAST
@requires_speedups
def test_native_iterator_type(native):
bar = progressbar.ProgressBar(max_value=10, fd=TTY())
it = iter(bar(range(10)))
assert type(it) is _REAL_FAST
@requires_speedups
def test_native_yields_all_items_and_final_value(native):
bar = progressbar.ProgressBar(max_value=100, fd=RecordingTTY())
out = list(bar(range(100)))
assert out == list(range(100))
assert bar.value == 100
assert bar.percentage == 100.0
assert bar._finished
@requires_speedups
def test_native_renders_and_finishes_at_100(native):
fd = RecordingTTY()
list(progressbar.progressbar(range(500), fd=fd))
frames = fd.repaints()
assert frames, 'native path drew nothing'
pcts = _percentages(frames)
assert pcts == sorted(pcts), f'percentages not monotonic: {pcts}'
assert pcts[-1] == 100
@requires_speedups
def test_native_matches_fallback_items(native, monkeypatch):
# Native run.
native_items = list(progressbar.progressbar(range(250), fd=RecordingTTY()))
# Fallback run (force pure-Python).
monkeypatch.setattr(bar_module, '_FastBarIterator', None)
fallback_items = list(
progressbar.progressbar(range(250), fd=RecordingTTY())
)
assert native_items == fallback_items == list(range(250))
@requires_speedups
def test_native_generator_input(native):
def gen():
yield from range(30)
bar = progressbar.ProgressBar(max_value=30, fd=RecordingTTY())
assert list(bar(gen())) == list(range(30))
assert bar.value == 30
@requires_speedups
def test_native_unknown_length(native):
bar = progressbar.ProgressBar(
max_value=progressbar.UnknownLength, fd=RecordingTTY()
)
out = list(bar(iter(range(40))))
assert out == list(range(40))
assert bar.value == 39
assert bar._finished
@requires_speedups
def test_native_empty_iterable(native):
bar = progressbar.ProgressBar(max_value=0, fd=RecordingTTY())
assert list(bar([])) == []
assert bar._finished
@requires_speedups
def test_native_with_statement(native):
fd = RecordingTTY()
with progressbar.ProgressBar(max_value=10, fd=fd) as bar:
out = list(bar(range(10)))
assert out == list(range(10))
assert bar._finished
@requires_speedups
def test_native_overshoot_clamps(native):
# max_error=False: iterating past max_value clamps instead of raising.
bar = progressbar.ProgressBar(
max_value=5, fd=RecordingTTY(), max_error=False
)
out = list(bar(range(20)))
assert out == list(range(20)) # every item still yielded
assert bar.value == 5 # clamped to max at finish
@requires_speedups
def test_native_break_restores_streams(native):
# Issue #212: breaking out of the loop must restore redirected streams,
# which the cdef iterator does via __dealloc__ (no GeneratorExit hook).
real_out, real_err = sys.stdout, sys.stderr
fd = RecordingTTY()
bar = progressbar.ProgressBar(max_value=1000, fd=fd, redirect_stdout=True)
for i in bar(range(1000)):
assert sys.stdout is not real_out # redirected while iterating
if i == 5:
break
del bar
gc.collect()
assert sys.stdout is real_out
assert sys.stderr is real_err
@requires_speedups
def test_native_exception_restores_streams(native):
real_out = sys.stdout
fd = RecordingTTY()
bar = progressbar.ProgressBar(max_value=1000, fd=fd, redirect_stdout=True)
with pytest.raises(ValueError):
for i in bar(range(1000)):
if i == 5:
raise ValueError('boom')
del bar
gc.collect()
assert sys.stdout is real_out