diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py index 6875182c..fcd3341a 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -2830,6 +2830,23 @@ class Flomni( """ dev = builtins.__dict__.get("dev") + is_new_scan = ( + (self.tomo_type == 1 and subtomo_start == 1 and start_angle is None) + or (self.tomo_type == 2 and projection_number == None) + or (self.tomo_type == 3 and projection_number == None) + or (self.tomo_type == 4 and projection_number is None) + ) + if is_new_scan: + # Clear any stale progress from a previous scan immediately, before + # the angle-file check and the eye/optics-in / filter-out-of-beam + # gates below -- otherwise, if any of those decline or raise (e.g. + # the setup isn't in measurement condition, which disabling rt + # feedback can easily cause), tomo_scan() returns/raises having + # never touched `progress`, and the GUI/webpage keep showing + # projection/angle/start-time from whatever scan last completed -- + # arbitrarily old, and easy to mistake for a scan still running. + self.progress.reset() + # tomo_type 4: (re-)read the angle file before anything else, so a # missing/malformed file fails immediately -- before sample DB # registration, empty frames, or any motion (and, in the tomo queue, @@ -2882,14 +2899,7 @@ class Flomni( bec.builtin_actors.scan_interlock.enabled = True self._current_special_angles = self.special_angles.copy() - # a new tomo scan was started - if ( - (self.tomo_type == 1 and subtomo_start == 1 and start_angle is None) - or (self.tomo_type == 2 and projection_number == None) - or (self.tomo_type == 3 and projection_number == None) - or (self.tomo_type == 4 and projection_number is None) - ): - + if is_new_scan: # pylint: disable=undefined-variable # Always attempt registration (even for an empty/test account) # and let add_sample_database() -> TomoIDManager.register() @@ -2921,14 +2931,11 @@ class Flomni( + "\n".join(self._format_angle_entry(e) for e in file_angles.angles), "tomoscan", ) + # estimated_remaining_time/estimated_finish_time/accumulated_idle_time/ + # heartbeat/subtomo/projection/angle/... were already cleared by the + # self.progress.reset() above, before the gates -- only the actual + # start time is set here, once the scan is genuinely committed to. self.progress["tomo_start_time"] = datetime.datetime.now().isoformat() - # reset stale estimates from any previous scan, otherwise the GUI - # would keep showing a leftover ETA from before this scan has - # accumulated enough projections to compute a fresh one - self.progress["estimated_remaining_time"] = None - self.progress["estimated_finish_time"] = None - self.progress["accumulated_idle_time"] = 0.0 - self.progress["heartbeat"] = None self.collect_empty_frames() # Unconditional single 0-degree reference projection at the very # start of a new tomogram, independent of diff --git a/tests/tests_bec_ipython_client/test_flomni_tomo_angles.py b/tests/tests_bec_ipython_client/test_flomni_tomo_angles.py index 5074190d..d4a637b0 100644 --- a/tests/tests_bec_ipython_client/test_flomni_tomo_angles.py +++ b/tests/tests_bec_ipython_client/test_flomni_tomo_angles.py @@ -594,3 +594,69 @@ def test_hook_warning_fires_again_only_for_newly_appended_overridden_lines(monke # overridden line is picked up mid-scan. assert len(warnings) == 1 assert "hook is active" in warnings[0] + + +# ── progress is reset before any pre-flight gate can abort a new scan ────── + + +def test_new_scan_resets_progress_before_eye_optics_gate_declines(monkeypatch, tmp_path): + """A stale progress dict from a long-finished scan must not survive a + declined *new* scan attempt (e.g. the eye/optics-in prompt, which + disabling rt feedback can trigger) -- regression for a report where the + progress widget kept showing a days-old scan after a failed start.""" + flomni = make_flomni_for_angle_file(monkeypatch, tmp_path, "10\nEND\n") + flomni.progress.update( + projection=196, + total_projections=288, + angle=196.2, + subtomo=6, + tomo_type="Equally spaced sub-tomograms", + tomo_start_time="2026-09-25T14:57:28", + heartbeat="2026-09-25T14:57:30", + ) + flomni._check_eye_out_and_optics_in = lambda: False + flomni.OMNYTools.yesno = lambda *a, **k: False # decline "Shall I continue?" + flomni._tomo_scan_at_angle = lambda angle, subtomo, **overrides: pytest.fail( + "must not acquire -- the gate declined" + ) + + flomni.tomo_scan() # returns early, no exception + + assert flomni.progress["projection"] == 0 + assert flomni.progress["total_projections"] == 1 + assert flomni.progress["angle"] == 0 + assert flomni.progress["subtomo"] == 0 + assert flomni.progress["tomo_type"] == 0 + assert flomni.progress["tomo_start_time"] is None + assert flomni.progress["heartbeat"] is None + + +def test_new_scan_resets_progress_before_angle_file_error(monkeypatch, tmp_path): + """Same as above, but the abort comes from the tomo_type-4 upfront angle- + file validation, which runs even earlier than the eye/optics gate.""" + from csaxs_bec.bec_ipython_client.plugins.flomni.flomni import FlomniError + + flomni = make_flomni_for_angle_file(monkeypatch, tmp_path, "10\nEND\n") + flomni.tomo_angle_file = str(tmp_path / "gone.txt") + flomni.progress.update(projection=42, tomo_start_time="2026-09-25T14:57:28") + + with pytest.raises(FlomniError, match="Cannot read"): + flomni.tomo_scan() + + assert flomni.progress["projection"] == 0 + assert flomni.progress["tomo_start_time"] is None + + +def test_resume_does_not_reset_progress(monkeypatch, tmp_path): + """A *resume* (projection_number given) must not wipe progress -- only a + genuinely new scan start does.""" + flomni = make_flomni_for_angle_file(monkeypatch, tmp_path, "10\n20\n30\nEND\n") + flomni.progress.update(estimated_remaining_time=42.0) + reset_calls = [] + monkeypatch.setattr(flomni._progress_proxy, "reset", lambda: reset_calls.append(1)) + flomni._tomo_scan_at_angle = lambda angle, subtomo, **overrides: None + + flomni.tomo_scan(projection_number=1) + + assert reset_calls == [] + assert flomni.progress["estimated_remaining_time"] == 42.0