diff --git a/pyproject.toml b/pyproject.toml index 706c95fe..e7daf189 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "piexif", "pydantic ~= 2.10.6", "simplejpeg >= 1.8.2", + "semver ~= 3.0", ] [project.optional-dependencies] diff --git a/src/openflexure_microscope_server/things/stage/sangaboard.py b/src/openflexure_microscope_server/things/stage/sangaboard.py index 84d75e92..369d9547 100644 --- a/src/openflexure_microscope_server/things/stage/sangaboard.py +++ b/src/openflexure_microscope_server/things/stage/sangaboard.py @@ -10,6 +10,7 @@ from types import TracebackType from contextlib import contextmanager from collections.abc import Mapping +import semver import sangaboard import labthings_fastapi as lt @@ -17,7 +18,8 @@ from . import BaseStage LOGGER = logging.getLogger(__name__) -REQUIRED_VERSION = (1, 0, 4) +REQUIRED_VERSION = semver.Version.parse("1.0.0") +RECOMMENDED_VERSION = semver.Version.parse("1.0.4") class SangaboardThing(BaseStage): @@ -89,40 +91,31 @@ class SangaboardThing(BaseStage): ) def check_firmware(self) -> None: - """Check the Sangaboard firmware version and log warnings or info messages. + """Error/warn if firmware doesn't meet requirements/recommendations. - Logs a warning if the numeric firmware version is below ``REQUIRED_VERSION``. - Logs an info message if the version ends with ``'-dev'``. Malformed version - strings trigger a warning. + Raise a Runtime Error if the version is below REQUIRED_VERSION + + Log a warning if the version is below RECOMMENDED_VERSION """ with self.sangaboard() as sb: - version = sb.firmware_version + # This will raise a ValueError is if the firmware version cannot be parsed + # this error will stop the microscope booting as we cannot ensure valid + # firmware. + version = semver.Version.parse(sb.firmware_version) - # Remove '-dev' for numeric comparison - is_dev = version.endswith("-dev") - base_version = version[:-4] if is_dev else version - - # Parse the numeric part - parts = base_version.split(".") - try: - major, minor, patch = (int(p) for p in parts) - except ValueError: - LOGGER.warning( - f"Unrecognized Sangaboard firmware version format: {version}" - ) - return - - current = (major, minor, patch) - - # Warn if version is below minimum - if current < REQUIRED_VERSION: - LOGGER.warning( - f"Sangaboard firmware version {version} is below the required 1.0.4." + # Warn if version is below required + if version < REQUIRED_VERSION: + raise RuntimeError( + f"Sangaboard firmware version {version} is below the required " + f"{REQUIRED_VERSION}." ) - # Log dev info - if is_dev: - LOGGER.info(f"Firmware version {version} is a development build.") + # Warn if version is below recommended + if version < RECOMMENDED_VERSION: + LOGGER.warning( + f"Sangaboard firmware version {version} is below the recommended " + f"{RECOMMENDED_VERSION}." + ) def _hardware_move_relative( self, diff --git a/tests/test_sangaboard.py b/tests/test_sangaboard.py index bf1c8394..a75b19a7 100644 --- a/tests/test_sangaboard.py +++ b/tests/test_sangaboard.py @@ -1,8 +1,14 @@ """Tests for the Sangaboard thing.""" +import logging + import pytest -from openflexure_microscope_server.things.stage.sangaboard import SangaboardThing +from openflexure_microscope_server.things.stage.sangaboard import ( + SangaboardThing, + REQUIRED_VERSION, + RECOMMENDED_VERSION, +) @pytest.fixture @@ -15,6 +21,58 @@ def mock_sanga_thing(mocker): def test_check_old_firmware(mock_sanga_thing): """Check firmware prior to version 1 throws a Runtime Error.""" - mock_sanga_thing._sangaboard.firmware_version.return_value = "0.1.1" + mock_sanga_thing._sangaboard.firmware_version = "0.1.1" with pytest.raises(RuntimeError): mock_sanga_thing.check_firmware() + + +def test_firmware_before_recommended(mock_sanga_thing, caplog): + """Check required version warns if it is lower than the recommended version.""" + if REQUIRED_VERSION == RECOMMENDED_VERSION: + # If required is currently the same as recommended then this test isn't valid + return + mock_sanga_thing._sangaboard.firmware_version = str(REQUIRED_VERSION) + with caplog.at_level(logging.WARNING): + mock_sanga_thing.check_firmware() + assert len(caplog.records) == 1 + msg = ( + f"Sangaboard firmware version {REQUIRED_VERSION} is below the recommended " + f"{RECOMMENDED_VERSION}." + ) + assert caplog.records[0].message == msg + + +def test_dev_is_before_recommended(mock_sanga_thing, caplog): + """Check that a dev version of the recommended version warns.""" + if REQUIRED_VERSION == RECOMMENDED_VERSION: + # If required is currently the same as recommended then this test isn't valid + return + dev_version = RECOMMENDED_VERSION.replace(prerelease="dev1") + mock_sanga_thing._sangaboard.firmware_version = str(dev_version) + with caplog.at_level(logging.WARNING): + mock_sanga_thing.check_firmware() + assert len(caplog.records) == 1 + msg = ( + f"Sangaboard firmware version {dev_version} is below the recommended " + f"{RECOMMENDED_VERSION}." + ) + assert caplog.records[0].message == msg + + +def test_valid_firmware(mock_sanga_thing, caplog): + """Check valid firmware doesn't error.""" + versions = [ + RECOMMENDED_VERSION, # Recommended + RECOMMENDED_VERSION.bump_major(), # Higher versions + RECOMMENDED_VERSION.bump_minor(), + RECOMMENDED_VERSION.bump_patch(), + RECOMMENDED_VERSION.bump_patch().replace(prerelease="dev1"), + ] + # Note that dev1 will not warn in check version, but will warn in the underlying + # Sangaboard library as it is a pre-release. + + for version in versions: + mock_sanga_thing._sangaboard.firmware_version = str(version) + with caplog.at_level(logging.WARNING): + mock_sanga_thing.check_firmware() + assert len(caplog.records) == 0