diff --git a/src/aare/gui/panels/file_path_panel.py b/src/aare/gui/panels/file_path_panel.py index a8ed8a28..f897c9b7 100644 --- a/src/aare/gui/panels/file_path_panel.py +++ b/src/aare/gui/panels/file_path_panel.py @@ -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 /manual/ +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 "_master.h5", + # X-ray Centering writes "_raster2d_master.h5" / "_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 "_raster2d_master.h5", not "_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() diff --git a/src/aare/gui/panels/scan_settings_panel.py b/src/aare/gui/panels/scan_settings_panel.py index e732f3dd..59faed7e 100644 --- a/src/aare/gui/panels/scan_settings_panel.py +++ b/src/aare/gui/panels/scan_settings_panel.py @@ -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.") diff --git a/src/aare/gui/panels/smart_rotation_panel.py b/src/aare/gui/panels/smart_rotation_panel.py index e86a8031..fb6e0b5c 100644 --- a/src/aare/gui/panels/smart_rotation_panel.py +++ b/src/aare/gui/panels/smart_rotation_panel.py @@ -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" ): diff --git a/tests/unit/gui/test_data_collection_settings.py b/tests/unit/gui/test_data_collection_settings.py index a6c54c92..1308b9a9 100644 --- a/tests/unit/gui/test_data_collection_settings.py +++ b/tests/unit/gui/test_data_collection_settings.py @@ -547,18 +547,52 @@ def test_a_derived_dataset_counts_as_taken(file_panel, monkeypatch): # X-ray centering writes _raster2d_master.h5, not _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