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