From 5b14f8d820fb38d5c6de591a0e489470bcc15ed6 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Apr 2026 14:14:24 +0100 Subject: [PATCH] Add method for checking OS version. --- .../things/system.py | 21 ++++++++++++ tests/unit_tests/assets/os-release | 10 ++++++ tests/unit_tests/assets/os-release-broken | 1 + tests/unit_tests/test_system_thing.py | 32 +++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 tests/unit_tests/assets/os-release create mode 100644 tests/unit_tests/assets/os-release-broken diff --git a/src/openflexure_microscope_server/things/system.py b/src/openflexure_microscope_server/things/system.py index ff9da423..4f47ec78 100644 --- a/src/openflexure_microscope_server/things/system.py +++ b/src/openflexure_microscope_server/things/system.py @@ -5,6 +5,7 @@ the microscope, server, and thing states to the web API. """ import os +import re import socket import subprocess import time @@ -22,6 +23,8 @@ from openflexure_microscope_server.utilities import VersionData, robust_version_ SHUTDOWN_CMD = ["sudo", "shutdown", "-h", "now"] REBOOT_CMD = ["sudo", "shutdown", "-r", "now"] +OS_VERSION_FILE = "/usr/lib/os-release" + class CommandOutput(BaseModel): """A pydantic model passing the STDOUT and STDERR from a subprocess over HTTP.""" @@ -81,6 +84,24 @@ class OpenFlexureSystem(lt.Thing): """Return True if running on a Raspberry Pi.""" return os.path.exists("/usr/bin/raspi-config") + @lt.property + def os_version(self) -> Optional[str]: + """Return the OS version of the Pi. Returns None if not on a Pi.""" + if not self.is_raspberrypi: + return None + + if not os.path.isfile(OS_VERSION_FILE): + return "unknown" + + with open(OS_VERSION_FILE, "r", encoding="utf-8") as os_file: + os_data = os_file.read() + + version_match = re.search(r"^VERSION_CODENAME=(.+)$", os_data, re.MULTILINE) + if not version_match: + return "unknown" + + return version_match.group(1) + @lt.action def shutdown(self) -> CommandOutput: """Attempt to shutdown the device.""" diff --git a/tests/unit_tests/assets/os-release b/tests/unit_tests/assets/os-release new file mode 100644 index 00000000..91ca955a --- /dev/null +++ b/tests/unit_tests/assets/os-release @@ -0,0 +1,10 @@ +PRETTY_NAME="Debian GNU/Linux 13 (trixie)" +NAME="Debian GNU/Linux" +VERSION_ID="13" +VERSION="13 (trixie)" +VERSION_CODENAME=trixie +DEBIAN_VERSION_FULL=13.4 +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" diff --git a/tests/unit_tests/assets/os-release-broken b/tests/unit_tests/assets/os-release-broken new file mode 100644 index 00000000..32271058 --- /dev/null +++ b/tests/unit_tests/assets/os-release-broken @@ -0,0 +1 @@ +This doesn't contain the data we are looking for. \ No newline at end of file diff --git a/tests/unit_tests/test_system_thing.py b/tests/unit_tests/test_system_thing.py index fa1729f7..ce73dc1c 100644 --- a/tests/unit_tests/test_system_thing.py +++ b/tests/unit_tests/test_system_thing.py @@ -11,6 +11,11 @@ from labthings_fastapi.testing import create_thing_without_server from openflexure_microscope_server.things import system from openflexure_microscope_server.utilities import VersionData, robust_version_strings +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +FAKE_OS_FILE = os.path.join(THIS_DIR, "assets", "os-release") +BROKEN_OS_FILE = os.path.join(THIS_DIR, "assets", "os-release-broken") +MISSING_OS_FILE = os.path.join(THIS_DIR, "assets", "this-does-not-exist") + @pytest.fixture def system_thing(): @@ -37,6 +42,33 @@ def test_is_raspberry(system_thing): assert system_thing.is_raspberrypi == _is_raspberrypi() +@pytest.mark.parametrize( + ("version_file", "expected_result"), + [ + (MISSING_OS_FILE, "unknown"), + (FAKE_OS_FILE, "trixie"), + (BROKEN_OS_FILE, "unknown"), + ], +) +def test_os_version_pi(version_file, expected_result, system_thing, mocker): + """Check reading the operating system version from a Pi (mocked!).""" + type(system_thing).is_raspberrypi = mocker.PropertyMock(return_value=True) + + mocker.patch( + "openflexure_microscope_server.things.system.OS_VERSION_FILE", version_file + ) + assert system_thing.os_version == expected_result + + +def test_os_version_not_pi(system_thing, mocker): + """Check reading the operating system when not on a Pi returns None.""" + mocker.patch( + "openflexure_microscope_server.things.system.OS_VERSION_FILE", FAKE_OS_FILE + ) + type(system_thing).is_raspberrypi = mocker.PropertyMock(return_value=False) + assert system_thing.os_version is None + + def test_version_data(system_thing): """Check VersionData is returned.