test_version.py (8851B)
1 #!/usr/bin/env python3 2 3 # This file is in the public domain. 4 5 """Test Git/archive versioning and Gradle task invalidation without compiling Kotlin.""" 6 7 import os 8 import re 9 import shutil 10 import subprocess 11 import tarfile 12 import tempfile 13 import unittest 14 from pathlib import Path 15 16 ROOT = Path(__file__).resolve().parents[1] 17 18 19 class VersionTests(unittest.TestCase): 20 def setUp(self): 21 self.temporary = tempfile.TemporaryDirectory(prefix="program-version-") 22 self.addCleanup(self.temporary.cleanup) 23 self.base = Path(self.temporary.name) 24 self.repo = self.base / "source" 25 self.env = os.environ.copy() 26 self.env.update( 27 { 28 "GIT_CONFIG_GLOBAL": os.devnull, 29 "GIT_CONFIG_NOSYSTEM": "1", 30 "GIT_AUTHOR_NAME": "Version Test", 31 "GIT_AUTHOR_EMAIL": "version@example.invalid", 32 "GIT_COMMITTER_NAME": "Version Test", 33 "GIT_COMMITTER_EMAIL": "version@example.invalid", 34 "JAVA_TOOL_OPTIONS": "-Djava.io.tmpdir=" + tempfile.gettempdir(), 35 } 36 ) 37 for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"): 38 self.env.pop(name, None) 39 configs = subprocess.check_output( 40 ["git", "ls-files", "*.gradle", "gradle.properties"], cwd=ROOT, text=True 41 ).splitlines() 42 configs.append("build-system/version.gradle") 43 for name in configs: 44 target = self.repo / name 45 target.parent.mkdir(parents=True, exist_ok=True) 46 shutil.copy(ROOT / name, target) 47 (self.repo / ".gitignore").write_text("build\n.gradle\n.version\n") 48 49 def command(self, *args, cwd=None, check=True): 50 result = subprocess.run( 51 args, 52 cwd=cwd or self.repo, 53 env=self.env, 54 text=True, 55 capture_output=True, 56 check=False, 57 ) 58 if check and result.returncode: 59 self.fail(f"{args}:\n{result.stdout}\n{result.stderr}") 60 return result 61 62 def git(self, *args, repo=None): 63 return self.command("git", *args, cwd=repo).stdout.strip() 64 65 def initialize(self): 66 self.git("init", "-b", "main") 67 self.git("add", ".") 68 self.git("commit", "-m", "Initial source") 69 70 def build(self, repo=None, check=True): 71 return self.command( 72 str(ROOT / "gradlew"), 73 "--offline", 74 "--console=plain", 75 "--project-dir", 76 str(repo or self.repo), 77 "libeufinVersion", 78 ":libeufin-common:versionConstant", 79 cwd=repo, 80 check=check, 81 ) 82 83 def assert_version(self, expected, repo=None): 84 result = self.build(repo) 85 generated = ( 86 repo or self.repo 87 ) / "libeufin-common/build/generated/constants/CompileConstants.kt" 88 actual = re.search( 89 r'val VERSION: String = "(.*)"', generated.read_text() 90 ).group(1) 91 self.assertEqual(expected, actual) 92 self.assertIn(expected, result.stdout.splitlines()) 93 return result 94 95 def description(self, repo=None): 96 return self.git("describe", "--tags", "--always", "--abbrev=8", repo=repo) 97 98 def test_release_development_tags_and_untagged_commits(self): 99 self.initialize() 100 self.assert_version(self.git("rev-parse", "--short=8", "HEAD")) 101 self.git("tag", "-a", "v1.2.3", "-m", "Release") 102 self.assert_version("v1.2.3") 103 self.git("commit", "--allow-empty", "-m", "Next commit") 104 self.assert_version(self.description()) 105 self.git("tag", "v1.2.4-dev.1") 106 self.assert_version("v1.2.4-dev.1") 107 # Dirty files do not change the commit version. 108 (self.repo / ".gitignore").write_text("target\nCargo.lock\n.version\nextra\n") 109 self.assert_version("v1.2.4-dev.1") 110 111 def test_git_takes_precedence_over_stamp(self): 112 self.initialize() 113 (self.repo / ".version").write_text("v0.0.1\n") 114 self.assert_version(self.description()) 115 116 def test_incremental_branch_and_packed_tag_changes(self): 117 self.initialize() 118 self.git("tag", "v1.0.0") 119 self.assert_version("v1.0.0") 120 unchanged = self.assert_version("v1.0.0") 121 self.assertIn(":libeufin-common:versionConstant UP-TO-DATE", unchanged.stdout) 122 self.git("switch", "-c", "next") 123 self.git("commit", "--allow-empty", "-m", "Next commit") 124 self.assert_version(self.description()) 125 self.git("tag", "v1.1.0") 126 self.assert_version("v1.1.0") 127 self.git("pack-refs", "--all", "--prune") 128 self.assert_version("v1.1.0") 129 self.git("tag", "-d", "v1.1.0") 130 self.assert_version(self.description()) 131 self.git("switch", "main") 132 self.assert_version("v1.0.0") 133 self.git("switch", "--detach", "next") 134 self.assert_version(self.description()) 135 136 def test_linked_worktree_uses_shared_refs(self): 137 self.initialize() 138 self.git("tag", "v1.0.0") 139 worktree = self.base / "worktree" 140 self.git("worktree", "add", "-b", "linked", str(worktree)) 141 self.assert_version("v1.0.0", worktree) 142 self.git("commit", "--allow-empty", "-m", "Worktree commit", repo=worktree) 143 self.assert_version(self.description(worktree), worktree) 144 self.git("tag", "v1.1.0", repo=worktree) 145 self.assert_version("v1.1.0", worktree) 146 self.git("pack-refs", "--all", "--prune") 147 self.assert_version("v1.1.0", worktree) 148 self.git("tag", "-d", "v1.1.0") 149 self.assert_version(self.description(worktree), worktree) 150 151 def test_shallow_tagged_and_untagged_clones(self): 152 self.initialize() 153 self.git("tag", "v1.0.0") 154 clone = self.base / "clone" 155 self.git( 156 "clone", "--depth=1", "--branch=v1.0.0", self.repo.as_uri(), str(clone) 157 ) 158 self.assert_version("v1.0.0", clone) 159 self.git("commit", "--allow-empty", "-m", "Unreachable tag") 160 shallow = self.base / "shallow" 161 self.git("clone", "--depth=1", "--no-tags", self.repo.as_uri(), str(shallow)) 162 self.assert_version( 163 self.git("rev-parse", "--short=8", "HEAD", repo=shallow), shallow 164 ) 165 166 def test_archive_fallback_and_incremental_stamp_changes(self): 167 (self.repo / ".version").write_text(" v2.0.0\n") 168 self.assert_version("v2.0.0") 169 (self.repo / ".version").write_text("v2.0.1\n") 170 self.assert_version("v2.0.1") 171 self.initialize() 172 self.assert_version(self.description()) 173 (self.repo / ".git").rename(self.base / "saved-git") 174 self.assert_version("v2.0.1") 175 176 def test_archive_does_not_use_enclosing_repository(self): 177 self.git("init", "-b", "main", repo=self.base) 178 self.git("commit", "--allow-empty", "-m", "Unrelated project", repo=self.base) 179 self.git("tag", "v9.9.9", repo=self.base) 180 (self.repo / ".version").write_text("v2.0.0\n") 181 self.assert_version("v2.0.0") 182 (self.repo / ".version").unlink() 183 result = self.build(check=False) 184 self.assertNotEqual(0, result.returncode) 185 self.assertIn("Cannot determine program version", result.stderr) 186 187 def test_missing_or_invalid_archive_stamp_fails(self): 188 result = self.build(check=False) 189 self.assertNotEqual(0, result.returncode) 190 self.assertIn("Source archives must contain", result.stderr) 191 for value in ("\n ", "v1.0.0\nv2.0.0", "v1.0.0\rjunk", "v1.0.0\0junk"): 192 with self.subTest(value=value): 193 (self.repo / ".version").write_text(value) 194 result = self.build(check=False) 195 self.assertNotEqual(0, result.returncode) 196 self.assertIn( 197 "Program version must be a nonempty single line", result.stderr 198 ) 199 200 def test_source_archive_stamps_git_version(self): 201 self.initialize() 202 (self.repo / ".version").write_text("v0.0.1\n") 203 self.git("add", "-f", ".version") 204 self.git("commit", "-m", "Stale version stamp") 205 self.git("tag", "v2.0.0") 206 archive = self.base / "source.tar.gz" 207 self.command("python3", str(ROOT / "build-system/archive.py"), str(archive)) 208 with tarfile.open(archive) as contents: 209 self.assertEqual(1, contents.getnames().count("source/.version")) 210 self.assertEqual( 211 b"v2.0.0\n", contents.extractfile("source/.version").read() 212 ) 213 self.assertFalse(any("/.git/" in name for name in contents.getnames())) 214 unpacked = self.base / "unpacked" 215 contents.extractall(unpacked, filter="data") 216 self.assert_version("v2.0.0", unpacked / "source") 217 218 219 if __name__ == "__main__": 220 unittest.main()