fix(gui): reach the file-exists guard from every tab, test exact names

The tabs are handed parent=DataCollectionSettings but
QStackedWidget.addWidget() reparents them to the stack, so the guard's
parent() lookup never found file_path_panel and every Run (rotation,
screening, raster, X-ray centering, Simple) skipped the check. Lookup
now walks up the widget tree; regression test drives all five runs
through the real DataCollectionSettings.

The "taken" test is exact again: <run>_master.h5 plus the DAQ's
derived <run>_raster2d/_raster1d_master.h5. The directory test and the
_*_master.h5 glob are gone: neither is a file a run writes, and they
painted run numbers red that nothing would ever produce.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 14:27:19 +02:00
committed by duan_j
co-authored by Claude Fable 5.1
parent 19361974d3
commit bd2b25e156
4 changed files with 69 additions and 18 deletions
+23 -8
View File
@@ -1,4 +1,3 @@
import glob
import os
from datetime import datetime
from pathlib import Path
@@ -19,6 +18,22 @@ from aare.gui.widgets.title_label import TitleLabel
## 5. If sample is registered in the database as manual, it is by default placed in <date>/manual/<sample name>
def find_file_path_panel(widget) -> "FilePathPanel | None":
"""The FilePathPanel that owns ``widget``'s tab, or None outside the GUI.
Walks up the widget tree: the scan panels are handed parent=DataCollectionSettings
but QStackedWidget.addWidget() reparents them to the stack, so a plain
parent() lookup finds no file_path_panel and the Run guard is skipped.
"""
w = widget.parent()
while w is not None:
panel = getattr(w, "file_path_panel", None)
if panel is not None:
return panel
w = w.parent()
return None
class FilePathPanel(QWidget):
path_updated = Signal(str)
@@ -117,16 +132,16 @@ class FilePathPanel(QWidget):
pass
return str(root / p)
# Master files a run can produce: plain scans write "<run>_master.h5",
# X-ray Centering writes "<run>_raster2d_master.h5" / "<run>_raster1d_master.h5"
# (see daq/operations/raster/service.py). Exact names only: a glob or a
# directory test marks runs red that nothing will ever write.
_MASTER_SUFFIXES = ("_master.h5", "_raster2d_master.h5", "_raster1d_master.h5")
def _exists_for_run(self, expanded_base_with_run: str) -> bool:
# expanded_base_with_run is the base without scan-kind transforms yet
effective = self._effective_dataset_base(expanded_base_with_run)
# Taken = master file, directory, or any derived dataset of this run:
# X-ray centering writes "<run>_raster2d_master.h5", not "<run>_master.h5".
return (
os.path.exists(f"{effective}_master.h5")
or os.path.exists(effective)
or bool(glob.glob(f"{effective}_*_master.h5"))
)
return any(os.path.exists(effective + suffix) for suffix in self._MASTER_SUFFIXES)
def update_filename(self):
dir_name = self.directory_edit.text()
+4 -3
View File
@@ -42,6 +42,7 @@ from PySide6.QtWidgets import (
)
from aare.gui.constants import LOGGER_NAME
from aare.gui.panels.file_path_panel import find_file_path_panel
from aare.gui.widgets.message_box import precondition_check
from aare.gui.widgets.number_line_edit import NumberLineEdit
@@ -388,9 +389,9 @@ class ScanSettingsPanel(QWidget):
):
logger.warning("Beamline not ready; user chose not to continue scan")
return False
p = self.parent()
if hasattr(p, "file_path_panel"):
reply = p.file_path_panel.file_path_error_box(scan_kind=scan_kind)
file_path_panel = find_file_path_panel(self)
if file_path_panel is not None:
reply = file_path_panel.file_path_error_box(scan_kind=scan_kind)
logger.debug(f"reply from file path panel: {reply}")
if not reply:
logger.warning("Error with file path.")
+2 -1
View File
@@ -9,6 +9,7 @@ from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtWidgets import QGridLayout, QLabel, QPushButton, QSizePolicy, QSpacerItem, QWidget
from aare.gui.constants import LOGGER_NAME
from aare.gui.panels.file_path_panel import find_file_path_panel
from aare.gui.panels.rotation_data_collection import (
MAX_OMEGA_SPEED_DEG_S,
MIN_EXP_TIME_S,
@@ -517,7 +518,7 @@ class SimpleRotationSettingsPanel(QWidget):
def run_measurement(self):
# Same file-exists guard as the Rotation tab; this panel is not a
# ScanSettingsPanel, so it has no check_before_run() to inherit.
file_path_panel = getattr(self.parent(), "file_path_panel", None)
file_path_panel = find_file_path_panel(self)
if file_path_panel is not None and not file_path_panel.file_path_error_box(
scan_kind="rotation"
):
@@ -547,18 +547,52 @@ def test_a_derived_dataset_counts_as_taken(file_panel, monkeypatch):
# X-ray centering writes <run>_raster2d_master.h5, not <run>_master.h5.
from aare.gui.panels import file_path_panel
_taken(monkeypatch)
monkeypatch.setattr(
file_path_panel.glob,
"glob",
lambda pat: ["hit"] if pat.endswith("raster/d/x_001_*_master.h5") else [],
)
_taken(monkeypatch, "raster/d/x_001_raster2d_master.h5")
monkeypatch.setattr(file_path_panel.QMessageBox, "warning", lambda *a, **k: None)
assert file_panel.file_path_error_box(scan_kind="raster") is False
assert file_panel.run_number_edit.value() == 2
def test_a_directory_or_a_sibling_run_is_not_taken(file_panel, monkeypatch):
# Only the exact master files count; anything else would paint runs red
# that nothing will write.
_taken(monkeypatch, "raster/d/x_001", "raster/d/x_0011_master.h5", "raster/d/x_001_data.h5")
file_panel.set_scan_kind("raster")
assert file_panel.file_name_label.styleSheet() == ""
assert file_panel.file_path_error_box(scan_kind="raster") is True
def test_every_tab_reaches_the_file_guard(settings_panel, monkeypatch):
# The tabs live in a QStackedWidget, which reparents them: a parent()
# lookup of file_path_panel found nothing and every Run skipped the guard.
from aare.gui.panels import scan_settings_panel
asked = []
monkeypatch.setattr(
settings_panel.file_path_panel,
"file_path_error_box",
lambda scan_kind: asked.append(scan_kind) or False,
)
monkeypatch.setattr(scan_settings_panel, "precondition_check", lambda *a, **k: True)
scans = []
for panel in (settings_panel.screening, settings_panel.raster):
panel._beamline_state = BeamlineStateEnum.SampleAlignment
settings_panel.screening.rotation_scan.connect(scans.append)
settings_panel.simple.rotation_scan.connect(scans.append)
settings_panel.raster.evaluate_grid.connect(lambda: scans.append("grid"))
settings_panel.raster.evaluate_grid_auto.connect(lambda: scans.append("auto"))
settings_panel.screening.run_screening()
settings_panel.screening.run_measurement()
settings_panel.simple.run_measurement()
settings_panel.raster._on_evaluate_clicked()
settings_panel.raster._on_evaluate_auto_clicked()
assert asked == ["screening", "rotation", "rotation", "raster", "raster"]
assert scans == []
def test_simple_tab_run_is_blocked_by_the_file_guard(qapp):
from PySide6.QtWidgets import QWidget