From 43b4d76d2e0f6fa077107541aaaeff08a6b50354 Mon Sep 17 00:00:00 2001 From: Kevin Weiss Date: Mon, 1 Jun 2026 18:10:13 +0200 Subject: [PATCH 1/2] feat(lib_types): Extend fw version with prerelease --- src/lob_hlpr/lib_types.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lob_hlpr/lib_types.py b/src/lob_hlpr/lib_types.py index bdb1516..ac7a211 100644 --- a/src/lob_hlpr/lib_types.py +++ b/src/lob_hlpr/lib_types.py @@ -29,6 +29,9 @@ class FirmwareVersion: commit: str | None = None """Commit hash associated with this version.""" + pre_release: str | None = None + """Pre-release identifier, e.g. 'rc.2' or 'alpha.1' (semver pre-release).""" + dirty: bool = False """Indicates if there are uncommitted changes in the source.""" @@ -37,6 +40,10 @@ class FirmwareVersion: _VERSION_REGEX = re.compile( r"(?P\d+)\.(?P\d+)\.(?P\d+)" + # Pre-release tag per semver (e.g. rc.2, alpha.1) + # excludes reserved -dirty/-unknown suffixes + r"(?:-(?P" + r"(?!dirty(?:-|$)|unknown(?:-|$))[a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z0-9]+)*))?" r"(?:-(?P\d+)-g(?P[0-9a-f]+))?" r"(?P-dirty)?(?P-unknown)?" ) @@ -54,6 +61,7 @@ def __post_init__(self): self.patch = int(m.group("patch")) self.commits = int(m.group("commits") or 0) self.commit = m.group("commit") + self.pre_release = m.group("pre_release") self.dirty = bool(m.group("dirty")) self.unknown = bool(m.group("unknown")) except (AttributeError, ValueError, TypeError, IndexError, KeyError) as exc: @@ -89,7 +97,7 @@ class FirmwareID: # Name group, non-greedy match up to the first space r"^(?P.+?)\s+" # Version group, matches a semantic versioning pattern - r"v(?P[0-9]+(?:\.[0-9]+){2}(?:-[\w]+)?(?:-\d+-g[0-9a-f]+)?)" + r"v(?P[0-9]+(?:\.[0-9]+){2}(?:-[\w]+(?:\.[\w]+)*)?(?:-\d+-g[0-9a-f]+)?)" # Optional variant group, matches anything after a '+' until a space or end r"(?:\+(?P[^\s]+))?" # Optional additional group, @@ -102,7 +110,7 @@ class FirmwareID: # Name group, non-greedy match up to the first + r"^(?P.+?)\+" # Version group, matches a semantic versioning pattern - r"(?P[0-9]+(?:\.[0-9]+){2}(?:-[\w]+)?(?:-\d+-g[0-9a-f]+)?)" + r"(?P[0-9]+(?:\.[0-9]+){2}(?:-[\w]+(?:\.[\w]+)*)?(?:-\d+-g[0-9a-f]+)?)" # Optional variant group, matches anything after a '+' until a space or end r"(?:\+(?P[^\s]+))?" # Optional additional group, From 78cca00ee6963a3b73445b13dccd9ecdbe2a596f Mon Sep 17 00:00:00 2001 From: Kevin Weiss Date: Mon, 1 Jun 2026 18:11:12 +0200 Subject: [PATCH 2/2] test(firmware_id): Extent tests for pre-release --- tests/test_firmware_id.py | 26 ++++++++++++++++++ tests/test_firmware_version.py | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/tests/test_firmware_id.py b/tests/test_firmware_id.py index 10b1df6..6557079 100644 --- a/tests/test_firmware_id.py +++ b/tests/test_firmware_id.py @@ -89,3 +89,29 @@ def test_firmware_id_with_no_name(): id_str = " v1.2.3 (Jan 01 2021 00:00:00)" with pytest.raises(ValueError): FirmwareID(id_str) + + +def test_valid_firmware_id_with_rc_version(): + """Tests origin-style firmware ID with an rc pre-release version.""" + id_str = "app-nrf91-origin+0.13.1-rc.2+hw4 TZ3 (May 28 2026 12:17:29)" + firmware_id = FirmwareID(id_str) + assert firmware_id.name == "app-nrf91-origin" + assert isinstance(firmware_id.version, FirmwareVersion) + assert firmware_id.version.version_string == "0.13.1-rc.2" + assert firmware_id.version.pre_release == "rc.2" + assert firmware_id.version.major == 0 + assert firmware_id.version.minor == 13 + assert firmware_id.version.patch == 1 + assert firmware_id.variants == ["hw4"] + assert firmware_id.built == "2026-05-28T12:17:29" + + +def test_valid_firmware_id_with_rc_version_and_variant(): + """Tests standard-style firmware ID with an rc pre-release version.""" + id_str = "app-nrf9160-wmbus v0.5.0-rc.1+hw3.alt_phy TZ2 (Jun 01 2026 08:00:00)" + firmware_id = FirmwareID(id_str) + assert firmware_id.name == "app-nrf9160-wmbus" + assert firmware_id.version.version_string == "0.5.0-rc.1" + assert firmware_id.version.pre_release == "rc.1" + assert firmware_id.variants == ["hw3", "alt_phy"] + assert firmware_id.built == "2026-06-01T08:00:00" diff --git a/tests/test_firmware_version.py b/tests/test_firmware_version.py index df06597..46651fb 100644 --- a/tests/test_firmware_version.py +++ b/tests/test_firmware_version.py @@ -56,3 +56,53 @@ def test_partial_version(): # Assuming partial versions are not allowed and should raise an error with pytest.raises(ValueError): FirmwareVersion("1.2") + + +def test_pre_release_rc(): + """Test with a semver pre-release tag like rc.2.""" + version = FirmwareVersion("0.13.1-rc.2") + assert version.major == 0 + assert version.minor == 13 + assert version.patch == 1 + assert version.pre_release == "rc.2" + assert version.commits == 0 + assert version.commit is None + assert version.dirty is False + assert version.unknown is False + + +def test_pre_release_alpha(): + """Test with a semver pre-release tag like alpha.1.""" + version = FirmwareVersion("1.0.0-alpha.1") + assert version.pre_release == "alpha.1" + assert version.major == 1 + assert version.minor == 0 + assert version.patch == 0 + + +def test_pre_release_with_commits(): + """Test a pre-release version that also has commit distance info.""" + version = FirmwareVersion("0.13.1-rc.2-3-gabcdef") + assert version.pre_release == "rc.2" + assert version.commits == 3 + assert version.commit == "abcdef" + + +def test_pre_release_none_for_plain_version(): + """Ensure pre_release is None for versions without a pre-release tag.""" + version = FirmwareVersion("1.2.3") + assert version.pre_release is None + + +def test_unknown_not_treated_as_pre_release(): + """Ensure -unknown is captured in the unknown flag, not as pre_release.""" + version = FirmwareVersion("1.2.3-unknown") + assert version.unknown is True + assert version.pre_release is None + + +def test_dirty_not_treated_as_pre_release(): + """Ensure -dirty is captured in the dirty flag, not as pre_release.""" + version = FirmwareVersion("1.2.3-dirty") + assert version.dirty is True + assert version.pre_release is None