1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15+ from time import sleep
16+ from types import SimpleNamespace
1517from unittest .mock import patch
1618
1719import pytest
1820
1921import pymc as pm
2022
21- from pymc .progress_bar import MCMCProgressBarManager
23+ from pymc .progress_bar import MCMCProgressBarManager , NutpieProgressBarManager
2224from pymc .progress_bar .marimo_progress import MarimoProgressBackend
2325
2426
@@ -75,6 +77,7 @@ def test_render_html_structure(self, step_method):
7577 {"completed" : 75 , "total" : 150 , "failing" : True , "stats" : {"divergences" : 1 }},
7678 ]
7779 backend ._start_times = [0 , 0 ]
80+ backend ._end_times = [None , None ]
7881
7982 html = backend ._render_html ()
8083
@@ -103,6 +106,7 @@ def test_render_html_with_stats(self, step_method):
103106 },
104107 ]
105108 backend ._start_times = [0 ]
109+ backend ._end_times = [None ]
106110
107111 html = backend ._render_html ()
108112
@@ -121,6 +125,25 @@ def test_is_last_sets_completed_to_total(self):
121125 assert backend ._task_state [0 ]["completed" ] == 150
122126 assert backend ._task_state [1 ]["completed" ] == 0
123127
128+ def test_elapsed_freezes_after_completion (self ):
129+ """Completed chains must not show drifting speed/elapsed on re-renders."""
130+ backend = MarimoProgressBackend (
131+ step_name = "Draw" , n_bars = 2 , total = 10 , combined = False , full_stats = False
132+ )
133+ backend ._initialize_tasks ()
134+
135+ # Complete chain 0
136+ for i in range (10 ):
137+ backend .update (task_id = 0 , advance = 1 , failing = False , stats = {}, is_last = i == 9 )
138+
139+ html_at_finish = backend ._render_task_row (0 , backend ._task_state [0 ], [])
140+
141+ # Let wall-clock advance so unfrozen elapsed would visibly drift
142+ sleep (0.3 )
143+
144+ html_after = backend ._render_task_row (0 , backend ._task_state [0 ], [])
145+ assert html_at_finish == html_after
146+
124147 def test_marimo_smc_progress (self ):
125148 backend = MarimoProgressBackend (
126149 step_name = "Stage" , n_bars = 1 , total = 1.0 , combined = False , full_stats = False
@@ -139,3 +162,39 @@ def test_marimo_smc_progress(self):
139162 old = beta
140163
141164 assert backend ._task_state [0 ]["completed" ] == 1.0
165+
166+ def test_nutpie_elapsed_freezes_after_completion (self ):
167+ """Completed nutpie chains must not show drifting elapsed in marimo."""
168+ with patch ("pymc.progress_bar.progress.in_marimo_notebook" , return_value = True ):
169+ manager = NutpieProgressBarManager (chains = 2 , draws = 100 , progressbar = True )
170+ assert isinstance (manager ._backend , MarimoProgressBackend )
171+ total = 1100
172+
173+ backend = manager ._backend
174+ backend ._initialize_tasks ()
175+
176+ def cp (finished , runtime_ms , started = True ):
177+ return SimpleNamespace (
178+ finished_draws = finished ,
179+ total_draws = total ,
180+ runtime_ms = runtime_ms ,
181+ started = started ,
182+ divergent_draws = [],
183+ step_size = 0.5 ,
184+ latest_num_steps = 7 ,
185+ )
186+
187+ # Chain 0 finishes, chain 1 halfway
188+ manager .update ([cp (total , runtime_ms = 5000 ), cp (500 , runtime_ms = 2500 )])
189+
190+ html_at_finish = backend ._render_task_row (0 , backend ._task_state [0 ], [])
191+
192+ # Let wall-clock advance so unfrozen elapsed would visibly drift
193+ sleep (0.3 )
194+
195+ # More callbacks arrive while chain 1 is still running
196+ manager .update ([cp (total , runtime_ms = 5000 ), cp (800 , runtime_ms = 4000 )])
197+
198+ # Chain 0's row must be identical — elapsed and speed frozen
199+ html_after = backend ._render_task_row (0 , backend ._task_state [0 ], [])
200+ assert html_at_finish == html_after
0 commit comments