From 67769d5ff8196afb30eb1f6ff0b87a8dde7a49d3 Mon Sep 17 00:00:00 2001 From: x01dc Date: Mon, 17 Aug 2026 15:47:51 +0200 Subject: [PATCH] fix(flomni,lamni): use .get() in tomo_queue_show()'s job-line formatter A job's params dict only has whatever _TOMO_SCAN_PARAM_NAMES held when it was snapshotted by tomo_queue_add(); a job persisted before a key was added (e.g. fovx/fovy, tomo_circfov) raised KeyError on the hard p['key'] lookup, making the whole queue un-inspectable instead of just missing a field. Co-Authored-By: Claude Sonnet 5 --- .../bec_ipython_client/plugins/LamNI/lamni.py | 12 ++++++-- .../plugins/flomni/flomni.py | 16 ++++++++--- omny_e2e_tests/test_tomo_queue.py | 28 +++++++++++++++++++ .../test_lamni_tomo_queue.py | 21 ++++++++++++++ 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index 9248fb0e..7f8c6640 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -193,11 +193,17 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools def _describe_tomo_job_line(self, job: dict) -> str: """lamni's tomo_queue_show() one-line summary: circfov instead of - fov, no range= (lamni is 360-only).""" + fov, no range= (lamni is 360-only). + + Uses .get(): a job persisted before some key was added to + _TOMO_SCAN_PARAM_NAMES won't have it in its snapshotted params, and + tomo_queue_show() must stay usable rather than crashing on the + missing key. + """ p = job["params"] return ( - f"type={p['tomo_type']} circfov={p['tomo_circfov']}um " - f"step={p['tomo_shellstep']}um ctime={p['tomo_countingtime']}s" + f"type={p.get('tomo_type')} circfov={p.get('tomo_circfov')}um " + f"step={p.get('tomo_shellstep')}um ctime={p.get('tomo_countingtime')}s" ) def __init__(self, client): diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py index b6cd174e..062631f4 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -3761,12 +3761,20 @@ class Flomni( } def _describe_tomo_job_line(self, job: dict) -> str: - """flomni's tomo_queue_show() one-line summary: fov/type/step/ctime/range.""" + """flomni's tomo_queue_show() one-line summary: fov/type/step/ctime/range. + + Uses .get() throughout: a job persisted before some key was added to + _TOMO_SCAN_PARAM_NAMES won't have it in its snapshotted params, and + tomo_queue_show() must stay usable (e.g. to delete/inspect such a + job) rather than crashing on the missing key. + """ p = job["params"] + fovx, fovy = p.get("fovx"), p.get("fovy") + fov_str = f"{fovx}/{fovy}um" if fovx is not None and fovy is not None else "?" return ( - f"type={p['tomo_type']} fov={p['fovx']}/{p['fovy']}um " - f"step={p['tomo_shellstep']}um ctime={p['tomo_countingtime']}s " - f"range={p['tomo_angle_range']}deg" + f"type={p.get('tomo_type')} fov={fov_str} " + f"step={p.get('tomo_shellstep')}um ctime={p.get('tomo_countingtime')}s " + f"range={p.get('tomo_angle_range')}deg" ) def rt_off(self): diff --git a/omny_e2e_tests/test_tomo_queue.py b/omny_e2e_tests/test_tomo_queue.py index a1a3f968..561791f5 100644 --- a/omny_e2e_tests/test_tomo_queue.py +++ b/omny_e2e_tests/test_tomo_queue.py @@ -157,3 +157,31 @@ def test_legacy_queue_migration(flomni_sim): ids = [j.get("id") for j in jobs] assert all(ids), f"every job should have been healed with an id, got {ids}" assert len(set(ids)) == len(ids), f"healed ids should be unique, got {ids}" + + +def test_queue_show_survives_missing_param_key(flomni_sim): + """A job snapshotted before some key (e.g. fovx) existed in + _TOMO_SCAN_PARAM_NAMES won't carry it in its persisted ``params`` dict. + tomo_queue_show() must still print a summary line for that job instead + of raising KeyError -- otherwise a single stale queue entry makes the + whole queue un-inspectable (and un-deletable via the index tomo_queue_show() + would have printed). + """ + flomni = flomni_sim + bec = flomni.client + + params = _short_params(flomni) + del params["fovx"] + del params["fovy"] + legacy_job = { + "kind": "tomo", + "id": "legacy-missing-fov", + "label": "legacy_no_fov", + "params": params, + "status": "pending", + "added_at": datetime.datetime.now().isoformat(), + } + bec.set_global_var("tomo_queue", [legacy_job]) + + jobs = flomni.tomo_queue_show() + assert jobs[0]["label"] == "legacy_no_fov" diff --git a/tests/tests_bec_ipython_client/test_lamni_tomo_queue.py b/tests/tests_bec_ipython_client/test_lamni_tomo_queue.py index 67c96927..6ceca9ec 100644 --- a/tests/tests_bec_ipython_client/test_lamni_tomo_queue.py +++ b/tests/tests_bec_ipython_client/test_lamni_tomo_queue.py @@ -113,6 +113,27 @@ def test_tomo_queue_show_uses_lamni_specific_job_line(capsys): assert "range=" not in out # lamni is 360-only, no angle-range field +def test_tomo_queue_show_survives_missing_param_key(capsys): + """A job snapshotted before some key (e.g. tomo_circfov) existed in + _TOMO_SCAN_PARAM_NAMES won't carry it in its persisted params dict -- + tomo_queue_show() must still print a line for it instead of raising + KeyError, or a single stale queue entry makes the whole queue + un-inspectable. + """ + lamni = make_lamni() + lamni.tomo_circfov = 15.0 + lamni.tomo_queue_add(label="job1") + jobs = lamni._tomo_queue_proxy.as_list() + del jobs[0]["params"]["tomo_circfov"] + lamni.client.set_global_var("tomo_queue", jobs) + + result = lamni.tomo_queue_show() + + assert result[0]["label"] == "job1" + out = capsys.readouterr().out + assert "circfov=None" in out + + def test_tomo_queue_execute_runs_fresh_job_then_marks_done(): lamni = make_lamni() calls = [] -- 2.54.0