test_version.py (10693B)
1 #!/usr/bin/env python3 2 # This file is in the public domain. 3 """Run with python3 contrib/ci/test_version.py (Git; optional dpkg comparator).""" 4 5 import contextlib 6 import importlib.util 7 import io 8 import os 9 from pathlib import Path 10 import shutil 11 import subprocess 12 import tempfile 13 import unittest 14 import uuid 15 16 CI = Path(__file__).resolve().parent 17 spec = importlib.util.spec_from_file_location("ci_version", CI / "debian-version.py") 18 version = importlib.util.module_from_spec(spec) 19 spec.loader.exec_module(version) 20 21 22 def run(*args, cwd, check=True, env=None): 23 return subprocess.run(args, cwd=cwd, check=check, text=True, 24 capture_output=True, env=env) 25 26 27 def script(path, text): 28 path.parent.mkdir(parents=True, exist_ok=True) 29 path.write_text("#!/bin/sh\n" + text + "\n") 30 path.chmod(0o755) 31 32 33 def install_scripts(repo): 34 for source in [CI / "debian-version.py", *CI.rglob("version.sh")]: 35 dest = repo / "contrib/ci" / source.relative_to(CI) 36 dest.parent.mkdir(parents=True, exist_ok=True) 37 shutil.copy2(source, dest) 38 39 40 class PolicyTests(unittest.TestCase): 41 def test_tag_order_and_metadata_ties(self): 42 cases = [ 43 (["v1.9.9", "v1.10.0-dev.0"], "v1.10.0-dev.0"), 44 (["v1.2.3-dev.9", "v1.2.3-dev.10"], "v1.2.3-dev.10"), 45 (["v1.2.3-dev.999", "v1.2.3"], "v1.2.3"), 46 (["v1.2.3+z", "v1.2.3+a", "v1.2.3"], "v1.2.3"), 47 (["v1.2.3+z", "v1.2.3+a"], "v1.2.3+a"), 48 (["v1.2.3-dev.1+x", "v1.2.3-dev.1"], "v1.2.3-dev.1"), 49 ] 50 for tags, expected in cases: 51 for names in (tags, list(reversed(tags))): 52 self.assertEqual(version.select_tag(names), expected) 53 54 def test_unsupported_tags_are_reported(self): 55 tags = ["v01.2.3", "v1.02.3", "v1.2.03", "v1.2.3-dev.01", 56 "1.2.3", "v1.2.3-rc.1", "v1.2.3-dev.1.2", "v1.2.3-0.1", 57 "v1.2.3+", "v1.2.3+a..b", "v1.2.3+bad_tag", "v0.9.4a"] 58 with contextlib.redirect_stderr(io.StringIO()) as diagnostics: 59 self.assertEqual(version.select_tag(tags + ["v0.0.0"]), "v0.0.0") 60 with self.assertRaises(ValueError): 61 version.select_tag(tags) 62 for tag in tags: 63 self.assertIn(tag, diagnostics.getvalue()) 64 65 def test_mappings(self): 66 cases = [ 67 ("v1.2.3", True, 0, "1.2.3", "1.2.3"), 68 ("v1.2.3", False, 0, "1.2.4-0.0", "1.2.4~0.0"), 69 ("v1.2.3", False, 12, "1.2.4-0.12", "1.2.4~0.12"), 70 ("v1.2.4-dev.9", True, 0, "1.2.4-dev.9", "1.2.4~dev9"), 71 ("v1.2.4-dev.9", False, 0, "1.2.4-dev.9.0", "1.2.4~dev9.0"), 72 ("v1.2.4-dev.9", False, 12, "1.2.4-dev.9.12", "1.2.4~dev9.12"), 73 ] 74 for tag, exact, count, semver, debian in cases: 75 for metadata in ("", "+build.001", "+other"): 76 self.assertEqual(version.versions(tag + metadata, exact, count), 77 (semver, debian)) 78 79 @unittest.skipUnless(shutil.which("dpkg"), "dpkg is needed for Debian ordering") 80 def test_debian_and_semver_order_agree(self): 81 # This sequence is increasing under SemVer's numeric identifier and 82 # longer-prerelease rules, including patch rollover and dev 9 -> 10. 83 cases = [("v1.2.3", True, 0)] 84 cases += [("v1.2.3", False, n) for n in (0, 2, 10)] 85 for dev in (0, 1, 9, 10): 86 cases += [(f"v1.2.4-dev.{dev}", True, 0)] 87 cases += [(f"v1.2.4-dev.{dev}", False, n) for n in (0, 2, 10)] 88 cases += [("v1.2.4", True, 0), ("v1.2.4", False, 0), 89 ("v1.2.99", False, 10), ("v1.3.0-dev.0", True, 0)] 90 pairs = [version.versions(*case) for case in cases] 91 for index, (_, left) in enumerate(pairs): 92 for _, right in pairs[index + 1:]: 93 result = run("dpkg", "--compare-versions", left, "lt", right, 94 cwd=CI, check=False) 95 self.assertEqual(result.returncode, 0, (left, right)) 96 run("dpkg", "--compare-versions", left, "eq", left, cwd=CI) 97 98 99 class GitTests(unittest.TestCase): 100 def setUp(self): 101 self.temp = tempfile.TemporaryDirectory() 102 self.addCleanup(self.temp.cleanup) 103 self.base = Path(self.temp.name) 104 self.origin = self.base / "origin" 105 self.repo = self.base / "checkout" 106 run("git", "init", "-q", str(self.origin), cwd=self.base) 107 self.git("config", "user.name", "Version test") 108 self.git("config", "user.email", "version@example.invalid") 109 self.commit() 110 111 def git(self, *args): 112 return run("git", *args, cwd=self.origin).stdout.strip() 113 114 def commit(self): 115 self.git("-c", "commit.gpgsign=false", "commit", "--allow-empty", "-qm", str(uuid.uuid4())) 116 return self.git("rev-parse", "HEAD") 117 118 def clone(self, shallow=False): 119 args = ["git", "clone", "-q"] + (["--depth=1"] if shallow else []) 120 run(*args, self.origin.as_uri(), str(self.repo), cwd=self.base) 121 install_scripts(self.repo) 122 run("git", "checkout", "--detach", cwd=self.repo) 123 124 def check_version(self, expected, selected=None): 125 nested = self.repo / "packages/test" 126 nested.mkdir(parents=True, exist_ok=True) 127 for entry in (self.repo / "contrib/ci").rglob("version.sh"): 128 with self.subTest(entry=entry.relative_to(self.repo)): 129 result = run(str(entry), cwd=nested) 130 self.assertEqual(result.stdout, expected + "\n") 131 self.assertIn("HEAD:", result.stderr) 132 if selected: 133 self.assertIn(f"Version tag: {selected};", result.stderr) 134 135 def test_detached_annotated_and_lightweight_tags(self): 136 self.git("-c", "tag.gpgsign=false", "tag", "-am", "release", "v1.2.3") 137 self.git("tag", "v1.2.3+metadata") 138 self.clone() 139 self.check_version("1.2.3", "v1.2.3") 140 141 def test_fetches_new_tags_without_moving_head(self): 142 self.clone() 143 head = self.git("rev-parse", "HEAD") 144 self.git("tag", "v1.2.3-dev.10") 145 self.check_version("1.2.3~dev10") 146 self.assertEqual(run("git", "rev-parse", "HEAD", cwd=self.repo).stdout.strip(), head) 147 148 def test_snapshots_after_stable_and_dev_tags(self): 149 self.git("tag", "v1.2.3") 150 self.commit() 151 self.clone() 152 self.check_version("1.2.4~0.1") 153 self.git("tag", "v1.2.4-dev.9", "HEAD~1") 154 self.check_version("1.2.4~dev9.1") 155 156 def test_highest_tag_on_another_branch(self): 157 base = self.git("rev-parse", "HEAD") 158 self.git("checkout", "-qb", "release") 159 self.commit() 160 self.git("tag", "v2.0.0-dev.10") 161 self.git("checkout", "--detach", base) 162 self.commit() 163 self.git("tag", "v1.0.0") 164 self.clone() 165 self.check_version("2.0.0~dev10.1", "v2.0.0-dev.10") 166 167 def test_zero_distance_is_not_exact(self): 168 base = self.git("rev-parse", "HEAD") 169 self.commit() 170 self.git("tag", "v2.0.0") 171 self.git("checkout", "--detach", base) 172 self.clone() 173 self.check_version("2.0.1~0.0") 174 175 def test_errors_have_no_version_output(self): 176 self.clone() 177 entry = str(self.repo / "contrib/ci/version.sh") 178 result = run(entry, cwd=self.repo, check=False) 179 self.assertNotEqual(result.returncode, 0) 180 self.assertEqual(result.stdout, "") 181 self.assertIn("no supported version tags", result.stderr) 182 self.git("tag", "v1.0.0") 183 run("git", "remote", "set-url", "origin", str(self.base / "missing"), cwd=self.repo) 184 result = run(entry, cwd=self.repo, check=False) 185 self.assertNotEqual(result.returncode, 0) 186 self.assertEqual(result.stdout, "") 187 self.assertIn("Cannot determine CI version", result.stderr) 188 189 def test_shallow_checkout_is_rejected(self): 190 self.git("tag", "v1.0.0") 191 self.commit() 192 self.clone(shallow=True) 193 result = run(str(self.repo / "contrib/ci/version.sh"), cwd=self.repo, check=False) 194 self.assertNotEqual(result.returncode, 0) 195 self.assertEqual(result.stdout, "") 196 self.assertIn("full Git checkout is required", result.stderr) 197 198 199 class PackagingTests(unittest.TestCase): 200 def test_callers_stop_on_failed_or_empty_version_and_pass_success(self): 201 callers = [p for p in CI.rglob("*.sh") 202 if p.is_file() and "VERSION=" in p.read_text()] 203 self.assertTrue(callers) 204 for caller in callers: 205 for output, status in (("", 42), ("", 0), ("1.2.3~dev9.12", 0)): 206 with self.subTest(caller=caller.relative_to(CI), output=output, status=status): 207 with tempfile.TemporaryDirectory() as temp: 208 repo = Path(temp) 209 install_scripts(repo) 210 dest = repo / "contrib/ci" / caller.relative_to(CI) 211 dest.parent.mkdir(parents=True, exist_ok=True) 212 shutil.copy2(caller, dest) 213 # Stub all version entry points to isolate shell error handling. 214 for entry in (repo / "contrib/ci").rglob("version.sh"): 215 script(entry, f"printf '%s' '{output}'\nexit {status}") 216 script(repo / "bootstrap", "exit 0") 217 for pkg in ("taler-wallet-cli", "taler-harness", "test-webui"): 218 (repo / "packages" / pkg / "debian").mkdir(parents=True) 219 bindir = repo / "bin" 220 for name in ("apt-get", "mk-build-deps"): 221 script(bindir / name, "exit 0") 222 # Stop at the first changelog operation; never build/install. 223 marker = repo / "changelog-called" 224 stub = f'printf "%s" "$VERSION" > "{marker}"\nexit 73' 225 script(bindir / "gbp", stub) 226 script(repo / "contrib/ci/write-debian-changelog.sh", stub) 227 env = dict(os.environ, PATH=f"{bindir}:{os.environ['PATH']}") 228 result = run("bash", str(dest), "test-webui", cwd=repo, env=env, check=False) 229 if output: 230 self.assertEqual(result.returncode, 73, result.stderr) 231 self.assertEqual(marker.read_text(), output) 232 else: 233 self.assertNotEqual(result.returncode, 0, result.stderr) 234 self.assertFalse(marker.exists(), result.stderr) 235 236 237 if __name__ == "__main__": 238 unittest.main()