Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/lob_hlpr/lib_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand All @@ -37,6 +40,10 @@ class FirmwareVersion:

_VERSION_REGEX = re.compile(
r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)"
# Pre-release tag per semver (e.g. rc.2, alpha.1)
# excludes reserved -dirty/-unknown suffixes
r"(?:-(?P<pre_release>"
r"(?!dirty(?:-|$)|unknown(?:-|$))[a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z0-9]+)*))?"
r"(?:-(?P<commits>\d+)-g(?P<commit>[0-9a-f]+))?"
r"(?P<dirty>-dirty)?(?P<unknown>-unknown)?"
)
Comment on lines 41 to 49
Expand All @@ -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:
Expand Down Expand Up @@ -89,7 +97,7 @@ class FirmwareID:
# Name group, non-greedy match up to the first space
r"^(?P<name>.+?)\s+"
# Version group, matches a semantic versioning pattern
r"v(?P<version>[0-9]+(?:\.[0-9]+){2}(?:-[\w]+)?(?:-\d+-g[0-9a-f]+)?)"
r"v(?P<version>[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<variant>[^\s]+))?"
# Optional additional group,
Expand All @@ -102,7 +110,7 @@ class FirmwareID:
# Name group, non-greedy match up to the first +
r"^(?P<name>.+?)\+"
# Version group, matches a semantic versioning pattern
r"(?P<version>[0-9]+(?:\.[0-9]+){2}(?:-[\w]+)?(?:-\d+-g[0-9a-f]+)?)"
r"(?P<version>[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<variant>[^\s]+))?"
# Optional additional group,
Expand Down
26 changes: 26 additions & 0 deletions tests/test_firmware_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
50 changes: 50 additions & 0 deletions tests/test_firmware_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Comment on lines +61 to +62
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
Loading