From 10ef93557bc1995f5e5c3ff346ff5423c13c97e4 Mon Sep 17 00:00:00 2001 From: x01dc Date: Tue, 21 Jul 2026 16:57:06 +0200 Subject: [PATCH] fix/stop lamni tests from leaking a fake bec global across the suite test_lamni_account_change_reset.py and test_lamni_tomo_angles.py set builtins.__dict__["bec"] directly with no cleanup, so a leaked fake account ("e22222") from an earlier test file was still in place when test_x_ray_eye_align.py later constructed a real LamNI(), tripping _maybe_reset_params_on_account_change()'s interactive yesno() prompt and failing under pytest's captured stdin. Use monkeypatch.setitem so pytest reverts the global after each test, matching the pattern already used in test_lamni_tomo_alignment_scan.py. Co-Authored-By: Claude Sonnet 5 --- .../test_lamni_account_change_reset.py | 16 +++--- .../test_lamni_tomo_angles.py | 52 +++++++++++-------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/tests/tests_bec_ipython_client/test_lamni_account_change_reset.py b/tests/tests_bec_ipython_client/test_lamni_account_change_reset.py index d879831..fe2ce65 100644 --- a/tests/tests_bec_ipython_client/test_lamni_account_change_reset.py +++ b/tests/tests_bec_ipython_client/test_lamni_account_change_reset.py @@ -60,9 +60,9 @@ def make_lamni(answer=True): return obj -def test_skips_silently_when_no_active_account(): +def test_skips_silently_when_no_active_account(monkeypatch): lamni = make_lamni() - builtins.__dict__["bec"] = type("Bec", (), {"active_account": ""})() + monkeypatch.setitem(builtins.__dict__, "bec", type("Bec", (), {"active_account": ""})()) lamni._maybe_reset_params_on_account_change() @@ -70,10 +70,10 @@ def test_skips_silently_when_no_active_account(): assert lamni.OMNYTools.prompts == [] -def test_skips_silently_on_same_account(): +def test_skips_silently_on_same_account(monkeypatch): lamni = make_lamni() lamni.client.set_global_var("defaults_applied_for_account", "e12345") - builtins.__dict__["bec"] = type("Bec", (), {"active_account": "e12345"})() + monkeypatch.setitem(builtins.__dict__, "bec", type("Bec", (), {"active_account": "e12345"})()) lamni.tomo_shellstep = 99.0 lamni._maybe_reset_params_on_account_change() @@ -82,12 +82,12 @@ def test_skips_silently_on_same_account(): assert lamni.tomo_shellstep == 99.0 -def test_prompts_and_resets_on_new_account_when_confirmed(): +def test_prompts_and_resets_on_new_account_when_confirmed(monkeypatch): lamni = make_lamni(answer=True) lamni.client.set_global_var("defaults_applied_for_account", "e11111") lamni.tomo_shellstep = 99.0 lamni.tomo_circfov = 123.0 - builtins.__dict__["bec"] = type("Bec", (), {"active_account": "e22222"})() + monkeypatch.setitem(builtins.__dict__, "bec", type("Bec", (), {"active_account": "e22222"})()) lamni._maybe_reset_params_on_account_change() @@ -99,11 +99,11 @@ def test_prompts_and_resets_on_new_account_when_confirmed(): assert lamni.client.get_global_var("defaults_applied_for_account") == "e22222" -def test_records_account_without_resetting_when_declined(): +def test_records_account_without_resetting_when_declined(monkeypatch): lamni = make_lamni(answer=False) lamni.client.set_global_var("defaults_applied_for_account", "e11111") lamni.tomo_shellstep = 99.0 - builtins.__dict__["bec"] = type("Bec", (), {"active_account": "e22222"})() + monkeypatch.setitem(builtins.__dict__, "bec", type("Bec", (), {"active_account": "e22222"})()) lamni._maybe_reset_params_on_account_change() diff --git a/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py b/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py index 127531d..0246079 100644 --- a/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py +++ b/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py @@ -152,7 +152,9 @@ class _FakeScans: dataset_id_on_hold = _FakeContextManager() -def make_lamni_for_tomo_scan(tomo_angle_stepsize: float, active_account: str) -> LamNI: +def make_lamni_for_tomo_scan( + monkeypatch, tomo_angle_stepsize: float, active_account: str +) -> LamNI: """Bare LamNI instance with tomo_scan()'s "new scan" branch reachable, everything downstream of it stubbed out (sample database, PDF report, per-sub-tomogram scanning) so only the account-handling logic under @@ -167,22 +169,26 @@ def make_lamni_for_tomo_scan(tomo_angle_stepsize: float, active_account: str) -> obj.lamnigui_show_progress = lambda: None obj.at_each_angle_hook = None obj.OMNYTools = types.SimpleNamespace(printgreenbold=lambda msg: None) - builtins.__dict__["bec"] = types.SimpleNamespace( - active_account=active_account, - queue=types.SimpleNamespace(next_scan_number=1), - builtin_actors=types.SimpleNamespace( - scan_interlock=types.SimpleNamespace(trigger_setting=None, enabled=False) + monkeypatch.setitem( + builtins.__dict__, + "bec", + types.SimpleNamespace( + active_account=active_account, + queue=types.SimpleNamespace(next_scan_number=1), + builtin_actors=types.SimpleNamespace( + scan_interlock=types.SimpleNamespace(trigger_setting=None, enabled=False) + ), ), ) - builtins.__dict__["scans"] = _FakeScans() + monkeypatch.setitem(builtins.__dict__, "scans", _FakeScans()) return obj -def test_tomo_scan_skips_sample_database_when_no_active_account(): +def test_tomo_scan_skips_sample_database_when_no_active_account(monkeypatch): """Empty active_account (e.g. a dev/sim session) must not crash and must not try to register a sample -- tomo_id falls back to 0, mirroring Flomni.tomo_scan()'s identical guard.""" - lamni = make_lamni_for_tomo_scan(45.0, active_account="") + lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="") lamni.add_sample_database = lambda *a, **k: (_ for _ in ()).throw( AssertionError("add_sample_database must not be called with no active account") ) @@ -192,10 +198,10 @@ def test_tomo_scan_skips_sample_database_when_no_active_account(): assert lamni.tomo_id == 0 -def test_tomo_scan_registers_sample_with_plain_string_account(): +def test_tomo_scan_registers_sample_with_plain_string_account(monkeypatch): """A real active_account must be passed through as-is -- calling .decode() on it (a plain str, not bytes) raises AttributeError.""" - lamni = make_lamni_for_tomo_scan(45.0, active_account="e12345") + lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="e12345") recorded = {} def _fake_add_sample_database(samplename, date, eaccount, scan_number, setup, info, user): @@ -210,11 +216,11 @@ def test_tomo_scan_registers_sample_with_plain_string_account(): assert lamni.tomo_id == 42 -def test_tomo_scan_shows_progress_gui(): +def test_tomo_scan_shows_progress_gui(monkeypatch): """tomo_scan() must open/show the progress GUI unconditionally at the start of every scan, mirroring Flomni.tomo_scan()'s self.flomnigui_show_progress() call -- lamni.py never had this at all.""" - lamni = make_lamni_for_tomo_scan(45.0, active_account="") + lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="") lamni.add_sample_database = lambda *a, **k: 0 calls = [] lamni.lamnigui_show_progress = lambda: calls.append("shown") @@ -224,7 +230,7 @@ def test_tomo_scan_shows_progress_gui(): assert calls == ["shown"] -def test_tomo_scan_clears_heartbeat_on_normal_completion(): +def test_tomo_scan_clears_heartbeat_on_normal_completion(monkeypatch): """The busy-detector heartbeat must be cleared as soon as the scan finishes, not left for the next poll to time out (120s) before the GUI stops showing "beamline busy" for an already-finished scan -- mirrors @@ -236,7 +242,7 @@ def test_tomo_scan_clears_heartbeat_on_normal_completion(): since tomo_scan()'s own "new scan" branch already resets the heartbeat to None *before* the scan body runs. """ - lamni = make_lamni_for_tomo_scan(45.0, active_account="") + lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="") lamni.add_sample_database = lambda *a, **k: 0 def _fake_sub_tomo_scan(subtomo_number, start_angle=None): @@ -249,12 +255,12 @@ def test_tomo_scan_clears_heartbeat_on_normal_completion(): assert lamni.progress["heartbeat"] is None -def test_tomo_scan_clears_heartbeat_even_if_scan_raises(): +def test_tomo_scan_clears_heartbeat_even_if_scan_raises(monkeypatch): """The heartbeat must be cleared on every exit path, including a mid-scan exception -- otherwise a crashed scan looks permanently "busy" to the GUI until the 120s staleness timeout, or forever if polled more often than that.""" - lamni = make_lamni_for_tomo_scan(45.0, active_account="") + lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="") lamni.add_sample_database = lambda *a, **k: 0 def _boom(subtomo_number, start_angle=None): @@ -282,10 +288,10 @@ def test_subtomo_starts_near_zero(subtomo_number, expected): assert lamni._subtomo_starts_near_zero(subtomo_number) is expected -def test_zero_deg_reference_disabled_by_default(): +def test_zero_deg_reference_disabled_by_default(monkeypatch): """zero_deg_reference_at_each_subtomo defaults to False -- a fresh tomo_scan() must not fire any extra 0-deg shots.""" - lamni = make_lamni_for_tomo_scan(45.0, active_account="") + lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="") lamni.add_sample_database = lambda *a, **k: 0 recorded = [] lamni._tomo_scan_at_angle = lambda angle, subtomo_number: recorded.append( @@ -298,11 +304,11 @@ def test_zero_deg_reference_disabled_by_default(): assert recorded == [] -def test_zero_deg_reference_fires_for_odd_subtomos_and_final_shot(): +def test_zero_deg_reference_fires_for_odd_subtomos_and_final_shot(monkeypatch): """With the flag on, a fresh tomo_scan() must fire an extra angle-0 shot before each odd sub-tomogram (1, 3, 5, 7) plus one final shot after sub-tomogram 8 completes -- mirrors Flomni.tomo_scan()'s equivalent.""" - lamni = make_lamni_for_tomo_scan(45.0, active_account="") + lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="") lamni.add_sample_database = lambda *a, **k: 0 lamni.zero_deg_reference_at_each_subtomo = True recorded = [] @@ -316,12 +322,12 @@ def test_zero_deg_reference_fires_for_odd_subtomos_and_final_shot(): assert recorded == [(0, 1), (0, 3), (0, 5), (0, 7), (0, 8)] -def test_zero_deg_reference_skipped_when_resuming_mid_subtomo(): +def test_zero_deg_reference_skipped_when_resuming_mid_subtomo(monkeypatch): """A resume (start_angle given explicitly for the first sub-tomogram of this call) must skip the extra shot for that first sub-tomogram -- we're not actually passing through 0 deg at that moment -- but must still fire normally for later sub-tomograms in the same call.""" - lamni = make_lamni_for_tomo_scan(45.0, active_account="") + lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="") lamni.zero_deg_reference_at_each_subtomo = True recorded = [] lamni._tomo_scan_at_angle = lambda angle, subtomo_number: recorded.append(