diff --git a/src/openflexure_microscope_server/scan_directories.py b/src/openflexure_microscope_server/scan_directories.py index 3ef0c946..a8850afb 100644 --- a/src/openflexure_microscope_server/scan_directories.py +++ b/src/openflexure_microscope_server/scan_directories.py @@ -276,11 +276,15 @@ class ScanDirectoryManager: return [f.name for f in os.scandir(self._base_scan_dir) if f.is_dir()] @requires_lock - def all_scans_info(self) -> list[ScanInfo]: + def all_scans_info(self, ongoing: Optional[str] = None) -> list[ScanInfo]: """Return a lists of ScanInfo objects for each scan.""" all_info: list[ScanInfo] = [] for scan_name in self.all_scans: - all_info.append(ScanDirectory(scan_name, self.base_dir).scan_info()) + # If the scan is ongoing send flag to skip reading the json data + skip_json = scan_name == ongoing + scan_dir = ScanDirectory(scan_name, self.base_dir) + info = scan_dir.scan_info(skip_json=skip_json) + all_info.append(info) return all_info @requires_lock @@ -504,7 +508,7 @@ class ScanDirectory: LOGGER.warning(f"Could not validate scan data for {self.name}.") return None - def scan_info(self) -> ScanInfo: + def scan_info(self, skip_json: bool = False) -> ScanInfo: """Return the information to be used in the UI for the scan.""" scan_files = self.get_scan_files() scan_images = self._extract_scan_images(scan_files) @@ -514,7 +518,7 @@ class ScanDirectory: stitch_available = len(stitches) > 0 dzi = None if not dzi_files else str(dzi_files[0]) - scan_data = self.get_scan_data() + scan_data = None if skip_json else self.get_scan_data() duration = ( None if scan_data is None or scan_data.duration is None diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 71376362..c9454b11 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -49,8 +49,8 @@ CSMDep = lt.deps.direct_thing_client_dependency( AutofocusDep = lt.deps.direct_thing_client_dependency(AutofocusThing, "/autofocus/") -class ScanListData(BaseModel): - """The data to be sent to the Scan List tab.""" +class ScanListInfo(BaseModel): + """The information to be sent to the Scan List tab.""" scans: list[scan_directories.ScanInfo] """The list of scans as ScanInfo objects""" @@ -588,8 +588,18 @@ class SmartScanThing(lt.Thing): ) """Whether to run a final stitch at the end of a successful scan.""" + def _get_all_scan_info(self) -> list[scan_directories.ScanInfo]: + """Return all the information from the scan directories. + + It is preferable to use the method rather than calling + _scan_dir_manager.all_scans_info() directly as it will handle stopping the json + in any ongoing scans being read. + """ + ongoing_name = None if self._ongoing_scan is None else self._ongoing_scan.name + return self._scan_dir_manager.all_scans_info(ongoing=ongoing_name) + @lt.thing_property - def scans(self) -> ScanListData: + def scans(self) -> ScanListInfo: """All the available scans. Each scan has a name (which can be used to access it), along with @@ -598,8 +608,8 @@ class SmartScanThing(lt.Thing): uses a regular expression, and changes to the naming scheme will break it. """ - return ScanListData( - scans=self._scan_dir_manager.all_scans_info(), + return ScanListInfo( + scans=self._get_all_scan_info(), ongoing=None if self._ongoing_scan is None else self._ongoing_scan.name, ) @@ -667,7 +677,7 @@ class SmartScanThing(lt.Thing): def purge_empty_scans(self, logger: lt.deps.InvocationLogger) -> None: """Delete all scan folders containing no images at the top level.""" # JSON is ignored as it's created before any images are captured - for scan_info in self._scan_dir_manager.all_scans_info(): + for scan_info in self._get_all_scan_info(): if scan_info.number_of_images == 0: self._delete_scan(scan_info.name, logger) @@ -791,6 +801,6 @@ class SmartScanThing(lt.Thing): """ if self._scan_logger is not None: raise RuntimeError("Can't stitch previous scans while a scan is ongoing") - for scan in self._scan_dir_manager.all_scans_info(): + for scan in self._get_all_scan_info(): if scan.dzi is None: self.stitch_scan(logger=logger, cancel=cancel, scan_name=scan.name) diff --git a/tests/test_scan_directories.py b/tests/test_scan_directories.py index eae6a7d6..7ee8962d 100644 --- a/tests/test_scan_directories.py +++ b/tests/test_scan_directories.py @@ -170,15 +170,23 @@ def test_bad_scan_names(): assert scan_dir.name == "fake_scan_rm_-rf____0001" -def test_scan_sequence_and_listing(): +def test_scan_sequence_and_listing(caplog): """Check created scans are added in order and listed correctly.""" _clear_scan_dir() scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) + + # Create some scan data and mark it as successful to get an end date. + scan_data = _fake_scan_data() + scan_data.set_final_data(result="Success") # Make 4 scans - scan_dir_manager.new_scan_dir("fake_scan") - scan_dir_manager.new_scan_dir("fake_scan") - scan_dir_manager.new_scan_dir("fake_scan") - scan_dir_manager.new_scan_dir("fake_scan") + scan_dir = scan_dir_manager.new_scan_dir("fake_scan") + scan_dir.save_scan_data(scan_data) + scan_dir = scan_dir_manager.new_scan_dir("fake_scan") + scan_dir.save_scan_data(scan_data) + scan_dir = scan_dir_manager.new_scan_dir("fake_scan") + scan_dir.save_scan_data(scan_data) + scan_dir = scan_dir_manager.new_scan_dir("fake_scan") + scan_dir.save_scan_data(scan_data) # Check they exist and are numbered sequentially all_scans = scan_dir_manager.all_scans @@ -188,13 +196,55 @@ def test_scan_sequence_and_listing(): assert "fake_scan_0003" in all_scans assert "fake_scan_0004" in all_scans - # Check scan data can be read for all of them - # (more detailed scan_info tests below) - all_scan_info = scan_dir_manager.all_scans_info() - for scan_info in all_scan_info: - assert isinstance(scan_info, ScanInfo) - assert scan_info.name.startswith("fake_scan_000") - assert scan_info.number_of_images == 0 + # Start capturing warnings + with caplog.at_level(logging.WARNING): + # Check scan data can be read for all of them + # (more detailed scan_info tests below) + all_scan_info = scan_dir_manager.all_scans_info() + for scan_info in all_scan_info: + assert isinstance(scan_info, ScanInfo) + assert scan_info.name.startswith("fake_scan_000") + assert scan_info.number_of_images == 0 + assert scan_info.duration is not None + # There are no warnings or errors + assert len(caplog.records) == 0 + + # Mess up the JSON of the final scan + with open(scan_dir.scan_data_path, "w", encoding="utf-8") as f: + f.write("Mock JSON") + + # Re-read the data with bad data for fake_scan_0004 + all_scan_info = scan_dir_manager.all_scans_info() + for scan_info in all_scan_info: + assert isinstance(scan_info, ScanInfo) + assert scan_info.name.startswith("fake_scan_000") + assert scan_info.number_of_images == 0 + # Bad data for scan 0004. So duration is None. + if scan_info.name == "fake_scan_0004": + assert scan_info.duration is None + else: + assert scan_info.duration is not None + # This should have warned about the bad data + assert len(caplog.records) == 1 + assert ( + caplog.records[0].message == "Could not load scan data for fake_scan_0004." + ) + + # Clear the warning logs + caplog.clear() + # Re-read the data again claiming fake_scan_0004 is ongoing + all_scan_info = scan_dir_manager.all_scans_info(ongoing="fake_scan_0004") + for scan_info in all_scan_info: + assert isinstance(scan_info, ScanInfo) + assert scan_info.name.startswith("fake_scan_000") + assert scan_info.number_of_images == 0 + # scan 0004 is marked as ongoing, so duration is None. + if scan_info.name == "fake_scan_0004": + assert scan_info.duration is None + else: + assert scan_info.duration is not None + # This time there is no warning + assert len(caplog.records) == 0 def test_scan_name_non_sequential(): diff --git a/webapp/src/components/tabContentComponents/scanListComponents/scanCard.vue b/webapp/src/components/tabContentComponents/scanListComponents/scanCard.vue index a031b914..46316ffc 100644 --- a/webapp/src/components/tabContentComponents/scanListComponents/scanCard.vue +++ b/webapp/src/components/tabContentComponents/scanListComponents/scanCard.vue @@ -58,8 +58,9 @@