test_prune_releases.py
python
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b
Merge branch 'fix/wire-push-external-parent-manifest' into dev
Human
8 days ago
| 1 | """Tests for deploy/prune_releases.py -- PEP 440-aware release pruning. |
| 2 | |
| 3 | See MuseHub staging issue #128: `sort -V` doesn't understand PEP 440 channel |
| 4 | precedence (dev < a < b < rc < final), so it deleted the newest nightly from |
| 5 | S3 immediately after upload. `stale_releases()` replaces `sort -V` with |
| 6 | `packaging.version.Version`, which sorts by actual PEP 440 precedence. |
| 7 | |
| 8 | Test IDs (pinned in issue #128's plan): |
| 9 | PRUNE_01 exact incident reproduction (dev1 survives, rc14 pruned) |
| 10 | PRUNE_02 mixed channels, full precedence coverage |
| 11 | PRUNE_03 keep >= len(filenames) prunes nothing |
| 12 | PRUNE_04 unparseable filename raises, never silently mis-sorts |
| 13 | PRUNE_05 stable releases sort after all pre-release channels |
| 14 | """ |
| 15 | from __future__ import annotations |
| 16 | |
| 17 | import pytest |
| 18 | |
| 19 | from deploy.prune_releases import stale_releases |
| 20 | |
| 21 | |
| 22 | class TestStaleReleases: |
| 23 | def test_PRUNE_01_same_base_version_dev_is_lowest_precedence_not_newest(self) -> None: |
| 24 | """Correct PEP 440 precedence for the exact incident's four filenames. |
| 25 | |
| 26 | dev1 and rc14-16 all share the base version "0.2.0" -- PEP 440 defines |
| 27 | dev-releases as the *lowest*-precedence pre-release stage for a given |
| 28 | version (dev < a < b < rc < final), so 0.2.0.dev1 correctly sorts |
| 29 | BEFORE 0.2.0rc16 despite being built later in wall-clock time. This is |
| 30 | not the bug: sort -V's bug was comparing "dev1" against "rc14" as |
| 31 | plain text and getting 'd' < 'r' right by accident-adjacent means but |
| 32 | wrong reasoning for the general case. The actual root cause of the |
| 33 | incident is that the channel-pivot reused "0.2.0" for the new nightly |
| 34 | instead of bumping the base version (see docs/versioning.md) -- once |
| 35 | that discipline is followed, this exact ambiguity can't recur, because |
| 36 | a same-day pivot bumps to e.g. 0.2.1.dev1, which correctly outranks |
| 37 | every 0.2.0 build regardless of pre-release tag. |
| 38 | """ |
| 39 | filenames = [ |
| 40 | "muse-0.2.0rc14.tar.gz", |
| 41 | "muse-0.2.0rc15.tar.gz", |
| 42 | "muse-0.2.0rc16.tar.gz", |
| 43 | "muse-0.2.0.dev1.tar.gz", |
| 44 | ] |
| 45 | assert stale_releases(filenames, keep=3) == ["muse-0.2.0.dev1.tar.gz"] |
| 46 | |
| 47 | def test_PRUNE_01b_pivot_with_bumped_version_keeps_new_nightly(self) -> None: |
| 48 | """The correct fix for the incident: bump the base version on a pivot. |
| 49 | |
| 50 | Once the next nightly is labeled against a NEW base version (0.2.1, |
| 51 | not 0.2.0), PEP 440 precedence unambiguously ranks it above every |
| 52 | 0.2.0 build, pre-release or not -- this is the behavior |
| 53 | docs/versioning.md's new "channel pivot" rule exists to guarantee. |
| 54 | """ |
| 55 | filenames = [ |
| 56 | "muse-0.2.0rc14.tar.gz", |
| 57 | "muse-0.2.0rc15.tar.gz", |
| 58 | "muse-0.2.0rc16.tar.gz", |
| 59 | "muse-0.2.1.dev1.tar.gz", |
| 60 | ] |
| 61 | assert stale_releases(filenames, keep=3) == ["muse-0.2.0rc14.tar.gz"] |
| 62 | |
| 63 | def test_PRUNE_02_mixed_channels_full_precedence_coverage(self) -> None: |
| 64 | """dev < a < b < rc precedence across all four pre-release channels.""" |
| 65 | filenames = [ |
| 66 | "muse-0.2.0.dev1.tar.gz", |
| 67 | "muse-0.2.0a1.tar.gz", |
| 68 | "muse-0.2.0b1.tar.gz", |
| 69 | "muse-0.2.0rc1.tar.gz", |
| 70 | "muse-0.2.0rc2.tar.gz", |
| 71 | ] |
| 72 | stale = stale_releases(filenames, keep=2) |
| 73 | assert sorted(stale) == sorted([ |
| 74 | "muse-0.2.0.dev1.tar.gz", |
| 75 | "muse-0.2.0a1.tar.gz", |
| 76 | "muse-0.2.0b1.tar.gz", |
| 77 | ]) |
| 78 | |
| 79 | def test_PRUNE_03_keep_greater_than_len_prunes_nothing(self) -> None: |
| 80 | filenames = ["muse-0.2.0rc1.tar.gz", "muse-0.2.0rc2.tar.gz"] |
| 81 | assert stale_releases(filenames, keep=5) == [] |
| 82 | |
| 83 | def test_PRUNE_03b_keep_equal_to_len_prunes_nothing(self) -> None: |
| 84 | filenames = ["muse-0.2.0rc1.tar.gz", "muse-0.2.0rc2.tar.gz"] |
| 85 | assert stale_releases(filenames, keep=2) == [] |
| 86 | |
| 87 | def test_PRUNE_04_unparseable_filename_raises(self) -> None: |
| 88 | filenames = ["muse-0.2.0rc1.tar.gz", "muse-not-a-version.tar.gz"] |
| 89 | with pytest.raises(ValueError): |
| 90 | stale_releases(filenames, keep=1) |
| 91 | |
| 92 | def test_PRUNE_05_stable_sorts_after_all_prerelease_channels(self) -> None: |
| 93 | """A stable release must never be pruned ahead of any pre-release build.""" |
| 94 | filenames = [ |
| 95 | "muse-0.2.0.dev1.tar.gz", |
| 96 | "muse-0.2.0rc1.tar.gz", |
| 97 | "muse-0.2.0.tar.gz", |
| 98 | ] |
| 99 | stale = stale_releases(filenames, keep=1) |
| 100 | assert stale == ["muse-0.2.0.dev1.tar.gz", "muse-0.2.0rc1.tar.gz"] |
| 101 | |
| 102 | def test_PRUNE_08_empty_filename_list_returns_empty(self) -> None: |
| 103 | assert stale_releases([], keep=3) == [] |
| 104 | |
| 105 | def test_PRUNE_09_returns_oldest_first_stable_ordering(self) -> None: |
| 106 | """Stale list should be ordered oldest-first, for readable log output.""" |
| 107 | filenames = [ |
| 108 | "muse-0.2.0rc3.tar.gz", |
| 109 | "muse-0.2.0rc1.tar.gz", |
| 110 | "muse-0.2.0rc2.tar.gz", |
| 111 | ] |
| 112 | assert stale_releases(filenames, keep=1) == [ |
| 113 | "muse-0.2.0rc1.tar.gz", |
| 114 | "muse-0.2.0rc2.tar.gz", |
| 115 | ] |
File History
2 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b
Merge branch 'fix/wire-push-external-parent-manifest' into dev
Human
8 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226
Merge branch 'fix/two-column-scroll-layout' into dev
Human
8 days ago