@@ -205,6 +205,7 @@ def test_gate_state_initialized():
205205
206206def test_recompute_gate_grows_step_when_no_redraw ():
207207 bar = progressbar .ProgressBar (max_value = 10 ** 9 )
208+ bar ._gate_calibrated = True # back-off only applies once calibrated
208209 bar ._gate_step = 4
209210 bar ._gate_last_value = 0
210211 bar ._gate_last_timer = 100.0
@@ -214,6 +215,19 @@ def test_recompute_gate_grows_step_when_no_redraw():
214215 assert bar ._next_update == 50 + 8
215216
216217
218+ def test_recompute_gate_no_backoff_before_calibration ():
219+ # Before calibration the gate is inactive and update() runs every
220+ # iteration; doubling here would grow _gate_step exponentially with the
221+ # iteration count, so the back-off must NOT fire while uncalibrated.
222+ bar = progressbar .ProgressBar (max_value = 10 ** 9 )
223+ assert bar ._gate_calibrated is False
224+ bar ._gate_step = 4
225+ bar ._gate_last_timer = 100.0
226+ bar ._last_update_timer = 100.0 # no redraw happened
227+ bar ._recompute_gate (50 )
228+ assert bar ._gate_step == 4 # unchanged (no exponential growth)
229+
230+
217231def test_recompute_gate_targets_min_poll_interval ():
218232 bar = progressbar .ProgressBar (max_value = 10 ** 9 )
219233 bar .min_poll_interval = 0.05
@@ -257,34 +271,23 @@ def spy(value):
257271 assert bar .previous_value in drawn
258272
259273
260- def test_previous_value_not_set_on_skipped_update (monkeypatch ):
261- """previous_value must only advance when a redraw actually happens.
274+ def test_last_drawn_value_pinned_on_skipped_update (monkeypatch ):
275+ """The gate's pixel reference advances only when a redraw happens.
262276
263- Old code sequence for update(v):
264- self.previous_value = self.value # always
265- self.value = v
266- if _needs_update(): draw # maybe
277+ `_last_drawn_value` (the private pixel reference used by `_needs_update`)
278+ must stay pinned to the value at the last actual draw, even as later
279+ `update()` calls advance `self.value` without redrawing. The public
280+ `previous_value` keeps its original meaning: the value before the most
281+ recent `update()` call.
267282
268- New code sequence:
269- self.value = v
270- if _needs_update():
271- draw
272- self.previous_value = self.value # only on draw
273-
274- After a draw at value=3 (starting from 0) and two consecutive skips
275- at value=4 then value=5:
276- Old: previous_value == 4 (set at the start of update(5))
277- New: previous_value == 3 (pinned to the drawn value)
278-
279- The new semantics are required so that _needs_update()'s pixel-threshold
280- check measures "pixels since last *draw*" rather than "pixels since last
281- *call*".
283+ After a draw at value=3 (from 0) and two rate-limited skips at 4 then 5:
284+ _last_drawn_value == 3 (pinned to the drawn value, for pixel check)
285+ previous_value == 4 (value before the update(5) call)
282286 """
283287 from progressbar import bar as bar_module
284288
285- # Advance the timer a lot during start and first update so it draws,
286- # then freeze it so subsequent updates are skipped.
287- # Start is called once; _needs_update reads timer once per update call.
289+ # Freeze-then-advance clock: start at 0, jump to 1.0 so update(3) draws,
290+ # then keep it at 1.0 so subsequent updates are rate-limited (skipped).
288291 _time : list [float ] = [0.0 ]
289292
290293 def timer () -> float :
@@ -295,25 +298,24 @@ def timer() -> float:
295298 bar = progressbar .ProgressBar (max_value = 100 , fd = RecordingTTY ())
296299 bar .start () # _last_update_timer = 0.0
297300
298- # Advance time far past min_poll_interval (0.05 s) => update(3) will draw .
301+ # Advance time far past min_poll_interval (0.05 s) => update(3) draws .
299302 _time [0 ] = 1.0
300303 bar .update (3 )
301- # Redraw happened; _last_update_timer is now 1.0.
302- # Old code: previous_value was 0 (self.value before assignment) pre-draw.
303- # New code: previous_value set to 3 (self.value after draw).
304-
305- # Freeze time: delta = 0 => _needs_update() returns False for next calls.
306- # (No need to touch _last_update_timer; next timer() read returns 1.0.)
307-
308- bar .update (4 ) # skipped: old sets prev=3, val=4; new: val=4, prev stays
309- bar .update (5 ) # skipped: old sets prev=4, val=5; new: val=5, prev stays
310-
311- # New semantics: previous_value == 3 (the value at the last actual draw).
312- # Old semantics: previous_value == 4 (set at the start of update(5)).
313- assert bar .previous_value == 3 , (
314- f'previous_value should be pinned to last-drawn value (3), '
315- f'got { bar .previous_value !r} '
304+ assert bar ._last_drawn_value == 3 # a redraw happened at value 3
305+
306+ # Time frozen at 1.0: delta == 0 => _needs_update() returns False, so the
307+ # next updates advance self.value but do not redraw.
308+ bar .update (4 )
309+ bar .update (5 )
310+
311+ # Pixel reference stays at the last drawn value; public previous_value
312+ # tracks the value before the most recent update() call.
313+ assert bar ._last_drawn_value == 3 , (
314+ f'_last_drawn_value should stay at last-drawn (3), '
315+ f'got { bar ._last_drawn_value !r} '
316316 )
317+ assert bar .value == 5 # liveness preserved on the manual path
318+ assert bar .previous_value == 4 # value before update(5)
317319
318320
319321def test_gate_disabled_skips_recompute ():
@@ -572,13 +574,13 @@ def test_iterator_overhead_is_low():
572574def test_no_color_fast_path_and_ansi ():
573575 # Render-cost optimization adds a no-ESC fast path to no_color/len_color.
574576 # It must be identical to the regex path for both plain and ANSI input.
575- from progressbar import utils
577+ utils = progressbar . utils
576578
577- # Fast path (no ESC byte) — returned unchanged, str and bytes.
579+ # Fast path (no ESC byte): returned unchanged, str and bytes.
578580 assert utils .no_color ('plain text' ) == 'plain text'
579581 assert utils .no_color (b'plain bytes' ) == b'plain bytes'
580582 assert utils .len_color ('plain' ) == 5
581- # Regex path (ANSI present) — escape sequences stripped, str and bytes.
583+ # Regex path (ANSI present): escape sequences stripped, str and bytes.
582584 assert utils .no_color ('\x1b [31mred\x1b [0m' ) == 'red'
583585 assert utils .no_color (b'\x1b [31mred\x1b [0m' ) == b'red'
584586 assert utils .len_color ('\x1b [1mbold\x1b [0m' ) == 4
0 commit comments