test_version.py (8393B)
1 #!/usr/bin/env python3 2 3 # This file is part of TALER 4 # Copyright (C) 2026 Taler Systems SA 5 # 6 # TALER is free software; you can redistribute it and/or modify it under the 7 # terms of the GNU Affero General Public License as published by the Free Software 8 # Foundation; either version 3, or (at your option) any later version. 9 # 10 # TALER is distributed in the hope that it will be useful, but WITHOUT ANY 11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 13 # 14 # You should have received a copy of the GNU Affero General Public License along with 15 # TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 16 17 """Exercise the real build script with dependency-free Cargo/Git fixtures.""" 18 19 import os 20 import shutil 21 import subprocess 22 import tempfile 23 import unittest 24 from pathlib import Path 25 26 ROOT = Path(__file__).resolve().parents[1] 27 CRATE = Path("common/taler-build") 28 29 30 class VersionTests(unittest.TestCase): 31 def setUp(self): 32 self.temporary = tempfile.TemporaryDirectory(prefix="program-version-") 33 self.addCleanup(self.temporary.cleanup) 34 self.base = Path(self.temporary.name) 35 self.repo = self.base / "source" 36 self.env = os.environ.copy() 37 self.env.update( 38 { 39 "GIT_CONFIG_GLOBAL": os.devnull, 40 "GIT_CONFIG_NOSYSTEM": "1", 41 "GIT_AUTHOR_NAME": "Version Test", 42 "GIT_AUTHOR_EMAIL": "version@example.invalid", 43 "GIT_COMMITTER_NAME": "Version Test", 44 "GIT_COMMITTER_EMAIL": "version@example.invalid", 45 "CARGO_TARGET_DIR": str(self.base / "target"), 46 } 47 ) 48 for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"): 49 self.env.pop(name, None) 50 crate = self.repo / CRATE 51 (crate / "src").mkdir(parents=True) 52 shutil.copy(ROOT / CRATE / "build.rs", crate / "build.rs") 53 (crate / "Cargo.toml").write_text( 54 '[package]\nname = "version-probe"\nversion = "0.0.0"\nedition = "2024"\n' 55 ) 56 (crate / "src/main.rs").write_text( 57 'fn main() { println!("{}", env!("BUILD_VERSION")); }\n' 58 ) 59 (self.repo / ".gitignore").write_text("target\nCargo.lock\n.version\n") 60 61 def command(self, *args, cwd=None, check=True): 62 result = subprocess.run( 63 args, 64 cwd=cwd or self.repo, 65 env=self.env, 66 text=True, 67 capture_output=True, 68 check=False, 69 ) 70 if check and result.returncode: 71 self.fail(f"{args}:\n{result.stdout}\n{result.stderr}") 72 return result 73 74 def git(self, *args, repo=None): 75 return self.command("git", *args, cwd=repo).stdout.strip() 76 77 def initialize(self): 78 self.git("init", "-b", "main") 79 self.git("add", ".") 80 self.git("commit", "-m", "Initial source") 81 82 def build(self, repo=None, check=True): 83 return self.command( 84 "cargo", 85 "build", 86 "--offline", 87 "--verbose", 88 "--manifest-path", 89 str((repo or self.repo) / CRATE / "Cargo.toml"), 90 cwd=repo, 91 check=check, 92 ) 93 94 def assert_version(self, expected, repo=None): 95 result = self.build(repo) 96 actual = self.command( 97 str(self.base / "target/debug/version-probe") 98 ).stdout.strip() 99 self.assertEqual(expected, actual) 100 return result 101 102 def description(self, repo=None): 103 return self.git("describe", "--tags", "--always", "--abbrev=8", repo=repo) 104 105 def test_release_development_tags_and_untagged_commits(self): 106 self.initialize() 107 self.assert_version(self.git("rev-parse", "--short=8", "HEAD")) 108 self.git("tag", "-a", "v1.2.3", "-m", "Release") 109 self.assert_version("v1.2.3") 110 self.git("commit", "--allow-empty", "-m", "Next commit") 111 self.assert_version(self.description()) 112 self.git("tag", "v1.2.4-dev.1") 113 self.assert_version("v1.2.4-dev.1") 114 # Dirty files do not change the commit version. 115 (self.repo / ".gitignore").write_text("target\nCargo.lock\n.version\nextra\n") 116 self.assert_version("v1.2.4-dev.1") 117 118 def test_git_takes_precedence_over_stamp(self): 119 self.initialize() 120 (self.repo / ".version").write_text("v0.0.1\n") 121 self.assert_version(self.description()) 122 123 def test_incremental_branch_and_packed_tag_changes(self): 124 self.initialize() 125 self.git("tag", "v1.0.0") 126 self.assert_version("v1.0.0") 127 unchanged = self.assert_version("v1.0.0") 128 self.assertIn("Fresh version-probe", unchanged.stderr) 129 self.git("switch", "-c", "next") 130 self.git("commit", "--allow-empty", "-m", "Next commit") 131 self.assert_version(self.description()) 132 self.git("tag", "v1.1.0") 133 self.assert_version("v1.1.0") 134 self.git("pack-refs", "--all", "--prune") 135 self.assert_version("v1.1.0") 136 self.git("tag", "-d", "v1.1.0") 137 self.assert_version(self.description()) 138 self.git("switch", "main") 139 self.assert_version("v1.0.0") 140 self.git("switch", "--detach", "next") 141 self.assert_version(self.description()) 142 143 def test_linked_worktree_uses_shared_refs(self): 144 self.initialize() 145 self.git("tag", "v1.0.0") 146 worktree = self.base / "worktree" 147 self.git("worktree", "add", "-b", "linked", str(worktree)) 148 self.assert_version("v1.0.0", worktree) 149 self.git("commit", "--allow-empty", "-m", "Worktree commit", repo=worktree) 150 self.assert_version(self.description(worktree), worktree) 151 self.git("tag", "v1.1.0", repo=worktree) 152 self.assert_version("v1.1.0", worktree) 153 self.git("pack-refs", "--all", "--prune") 154 self.assert_version("v1.1.0", worktree) 155 self.git("tag", "-d", "v1.1.0") 156 self.assert_version(self.description(worktree), worktree) 157 158 def test_shallow_tagged_and_untagged_clones(self): 159 self.initialize() 160 self.git("tag", "v1.0.0") 161 clone = self.base / "clone" 162 self.git( 163 "clone", "--depth=1", "--branch=v1.0.0", self.repo.as_uri(), str(clone) 164 ) 165 self.assert_version("v1.0.0", clone) 166 self.git("commit", "--allow-empty", "-m", "Unreachable tag") 167 shallow = self.base / "shallow" 168 self.git("clone", "--depth=1", "--no-tags", self.repo.as_uri(), str(shallow)) 169 self.assert_version( 170 self.git("rev-parse", "--short=8", "HEAD", repo=shallow), shallow 171 ) 172 173 def test_archive_fallback_and_incremental_stamp_changes(self): 174 (self.repo / ".version").write_text(" v2.0.0\n") 175 self.assert_version("v2.0.0") 176 (self.repo / ".version").write_text("v2.0.1\n") 177 self.assert_version("v2.0.1") 178 self.initialize() 179 self.assert_version(self.description()) 180 (self.repo / ".git").rename(self.base / "saved-git") 181 self.assert_version("v2.0.1") 182 183 def test_archive_does_not_use_enclosing_repository(self): 184 self.git("init", "-b", "main", repo=self.base) 185 self.git("commit", "--allow-empty", "-m", "Unrelated project", repo=self.base) 186 self.git("tag", "v9.9.9", repo=self.base) 187 (self.repo / ".version").write_text("v2.0.0\n") 188 self.assert_version("v2.0.0") 189 (self.repo / ".version").unlink() 190 result = self.build(check=False) 191 self.assertNotEqual(0, result.returncode) 192 self.assertIn("cannot determine program version", result.stderr) 193 194 def test_missing_or_invalid_archive_stamp_fails(self): 195 result = self.build(check=False) 196 self.assertNotEqual(0, result.returncode) 197 self.assertIn("Source archives must contain a .version file", result.stderr) 198 for value in ("\n ", "v1.0.0\nv2.0.0", "v1.0.0\rjunk", "v1.0.0\0junk"): 199 with self.subTest(value=value): 200 (self.repo / ".version").write_text(value) 201 result = self.build(check=False) 202 self.assertNotEqual(0, result.returncode) 203 self.assertIn( 204 "program version must be a nonempty single line", result.stderr 205 ) 206 207 208 if __name__ == "__main__": 209 unittest.main()