from __future__ import annotations
import importlib.util
import os
import pathlib
import shutil
import subprocess
import tempfile
import unittest
SCRIPT = pathlib.Path(__file__).with_name("check-release.py")
SPEC = importlib.util.spec_from_file_location("check_release", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
check_release = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(check_release)
class VersionLadder(unittest.TestCase):
def test_accepts_exactly_one_semantic_version_step(self) -> None:
old = (1, 17, 2)
for new in ((1, 17, 3), (1, 18, 0), (2, 0, 0)):
with self.subTest(new=new):
self.assertTrue(check_release.next_step(old, new))
def test_rejects_skips_resets_and_unchanged_versions(self) -> None:
old = (1, 15, 1)
for new in ((1, 15, 1), (1, 17, 0), (1, 16, 1), (2, 0, 1), (1, 14, 9)):
with self.subTest(new=new):
self.assertFalse(check_release.next_step(old, new))
class ReleaseShape(unittest.TestCase):
def setUp(self) -> None:
repo = tempfile.mkdtemp(prefix="check-release-")
self.addCleanup(shutil.rmtree, repo)
self.addCleanup(os.chdir, os.getcwd())
os.chdir(repo)
self.git("init", "-q", "-b", "main")
self.git("config", "user.name", "test")
self.git("config", "user.email", "test@example.com")
self.git("config", "commit.gpgsign", "false")
self.base = self.commit("1.17.2", "Initial state.")
def git(self, *args: str) -> str:
return subprocess.check_output(("git",) + args, text=True).strip()
def commit(self, version: str, subject: str, extra: str | None = None, lock_version: str | None = None) -> str:
pathlib.Path("Cargo.toml").write_text(f'[package]\nname = "scsh"\nversion = "{version}"\n')
pathlib.Path("Cargo.lock").write_text(f'[[package]]\nname = "scsh"\nversion = "{lock_version or version}"\n')
if extra is not None:
pathlib.Path(extra).write_text("content\n")
self.git("add", "-A")
self.git("commit", "-q", "-m", subject)
return self.git("rev-parse", "HEAD")
def test_a_pr_without_a_version_bump_passes(self) -> None:
self.commit("1.17.2", "Add a feature.", extra="feature.txt")
self.assertIsNone(check_release.check_shape(self.base, "HEAD"))
def test_a_bump_only_pr_is_a_legal_release(self) -> None:
self.commit("1.17.3", "Bump version to 1.17.3.")
self.assertEqual(check_release.check_shape(self.base, "HEAD"), ((1, 17, 2), (1, 17, 3)))
def test_the_explicit_head_survives_a_synthetic_merge_checkout(self) -> None:
self.commit("1.17.2", "Add a feature.", extra="feature.txt")
head = self.commit("1.17.3", "Bump version to 1.17.3.")
self.git("checkout", "-q", "-b", "pr-merge", self.base)
self.git("merge", "-q", "--no-ff", head, "-m", f"Merge {head[:7]} into {self.base[:7]}")
self.assertEqual(check_release.check_shape(self.base, head), ((1, 17, 2), (1, 17, 3)))
with self.assertRaises(SystemExit):
check_release.check_shape(self.base, "HEAD")
def test_a_version_skip_is_rejected(self) -> None:
self.commit("1.19.0", "Bump version to 1.19.0.")
with self.assertRaises(SystemExit):
check_release.check_shape(self.base, "HEAD")
def test_a_final_commit_with_code_changes_is_rejected(self) -> None:
self.commit("1.17.3", "Bump version to 1.17.3.", extra="feature.txt")
with self.assertRaises(SystemExit):
check_release.check_shape(self.base, "HEAD")
def test_the_old_bump_subject_convention_is_rejected(self) -> None:
self.commit("1.17.3", "Bump to 1.17.3.")
with self.assertRaises(SystemExit):
check_release.check_shape(self.base, "HEAD")
def test_a_stale_cargo_lock_is_rejected_even_without_a_bump(self) -> None:
self.commit("1.17.3", "Bump version to 1.17.3.", lock_version="1.17.2")
with self.assertRaises(SystemExit):
check_release.check_shape(self.base, "HEAD")
if __name__ == "__main__":
unittest.main()