fix/stop lamni tests from leaking a fake bec global across the suite
CI for csaxs_bec / test (pull_request) Successful in 3m9s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 3m13s

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 <noreply@anthropic.com>
This commit was merged in pull request #266.
This commit is contained in:
x01dc
2026-07-21 16:57:06 +02:00
co-authored by Claude Sonnet 5
parent 9198638f9b
commit 10ef93557b
2 changed files with 37 additions and 31 deletions
@@ -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()
@@ -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(