Add channel-aware release tooling and updater recovery

This commit is contained in:
2026-09-10 00:39:37 +02:00
parent cc48bf45d2
commit 0a89323533
42 changed files with 7840 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
from __future__ import annotations
import unittest
from ptlinux.update_resolver import (
BLOCKED_STATUS,
PRERELEASE_CHANNEL,
STABLE_CHANNEL,
WITHDRAWN_STATUS,
ReleaseModelError,
ReleaseRecord,
SemanticVersion,
UpdateResolver,
previous_release_tag,
validate_channel_version,
validate_release_progression,
)
class SemanticVersionTests(unittest.TestCase):
def test_orders_beta_rc_final_and_multidigit_versions(self) -> None:
versions = [
SemanticVersion.parse(value)
for value in (
"1.9.0",
"1.9.0-rc.1",
"1.10.0",
"1.9.0-beta.2",
"1.9.0-beta.1",
"1.9.9",
)
]
self.assertEqual(
[str(value) for value in sorted(versions)],
[
"1.9.0-beta.1",
"1.9.0-beta.2",
"1.9.0-rc.1",
"1.9.0",
"1.9.9",
"1.10.0",
],
)
def test_rejects_unsupported_or_noncanonical_versions(self) -> None:
for value in (
"v1.2.3",
"01.2.3",
"1.02.3",
"1.2.03",
"1.2.3-alpha.1",
"1.2.3-beta",
"1.2.3+build",
):
with self.subTest(value=value):
with self.assertRaises(ReleaseModelError):
SemanticVersion.parse(value)
def test_rejects_channel_version_mismatches(self) -> None:
with self.assertRaisesRegex(ReleaseModelError, "belongs to prerelease"):
validate_channel_version(STABLE_CHANNEL, "1.9.0-beta.1")
with self.assertRaisesRegex(ReleaseModelError, "belongs to stable"):
validate_channel_version(PRERELEASE_CHANNEL, "1.9.0")
def test_selects_channel_specific_changelog_bases(self) -> None:
tags = ["not-a-release", "v1.7.4", "v1.9.0-beta.1", "v1.9.0-beta.2"]
self.assertEqual(
previous_release_tag(STABLE_CHANNEL, "1.8.0", tags),
"v1.7.4",
)
self.assertEqual(
previous_release_tag(PRERELEASE_CHANNEL, "1.9.0-rc.1", tags),
"v1.9.0-beta.2",
)
self.assertEqual(
previous_release_tag(PRERELEASE_CHANNEL, "2.0.0-beta.1", tags),
"v1.7.4",
)
def test_release_progression_cannot_move_a_channel_backwards(self) -> None:
validate_release_progression(
STABLE_CHANNEL,
"1.9.0",
["v1.8.0", "v2.0.0-beta.1"],
)
validate_release_progression(
PRERELEASE_CHANNEL,
"2.0.0-beta.2",
["v1.9.0", "v2.0.0-beta.1"],
)
with self.assertRaisesRegex(ReleaseModelError, "newer"):
validate_release_progression(
STABLE_CHANNEL,
"1.8.0",
["v1.9.0"],
)
with self.assertRaisesRegex(ReleaseModelError, "newer"):
validate_release_progression(
PRERELEASE_CHANNEL,
"1.9.0-rc.1",
["v2.0.0-beta.1"],
)
class UpdateResolverTests(unittest.TestCase):
def setUp(self) -> None:
self.resolver = UpdateResolver(
[
ReleaseRecord("1.7.4", STABLE_CHANNEL, tag="v1.7.4"),
ReleaseRecord("1.8.0", STABLE_CHANNEL, tag="v1.8.0"),
ReleaseRecord("1.9.0-beta.1", PRERELEASE_CHANNEL),
ReleaseRecord("1.9.0-beta.2", PRERELEASE_CHANNEL),
ReleaseRecord("1.9.0-rc.1", PRERELEASE_CHANNEL),
ReleaseRecord("2.0.0-beta.1", PRERELEASE_CHANNEL, WITHDRAWN_STATUS),
ReleaseRecord("2.0.0-beta.2", PRERELEASE_CHANNEL, BLOCKED_STATUS),
]
)
def test_stable_clients_never_receive_prereleases(self) -> None:
candidate = self.resolver.update_candidate("1.7.4", STABLE_CHANNEL)
self.assertIsNotNone(candidate)
assert candidate is not None
self.assertEqual(candidate.version, "1.8.0")
def test_prerelease_clients_receive_highest_active_semantic_version(self) -> None:
candidate = self.resolver.update_candidate(
"1.9.0-beta.1",
PRERELEASE_CHANNEL,
)
self.assertIsNotNone(candidate)
assert candidate is not None
self.assertEqual(candidate.version, "1.9.0-rc.1")
def test_prerelease_client_transitions_to_the_final_stable_release(self) -> None:
resolver = UpdateResolver(
[
ReleaseRecord("1.9.0-rc.1", PRERELEASE_CHANNEL),
ReleaseRecord("1.9.0", STABLE_CHANNEL),
]
)
candidate = resolver.update_candidate("1.9.0-rc.1", PRERELEASE_CHANNEL)
self.assertIsNotNone(candidate)
assert candidate is not None
self.assertEqual(candidate.version, "1.9.0")
def test_switching_to_stable_does_not_downgrade(self) -> None:
self.assertIsNone(
self.resolver.update_candidate("1.9.0-beta.2", STABLE_CHANNEL)
)
def test_channel_aware_rollback_candidates_are_real_releases(self) -> None:
stable = self.resolver.rollback_candidates("1.9.0", STABLE_CHANNEL)
prerelease = self.resolver.rollback_candidates(
"1.9.0-rc.1",
PRERELEASE_CHANNEL,
)
self.assertEqual([item.version for item in stable], ["1.8.0", "1.7.4"])
self.assertEqual(
[item.version for item in prerelease],
["1.9.0-beta.2", "1.9.0-beta.1", "1.8.0", "1.7.4"],
)
if __name__ == "__main__":
unittest.main()