taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

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()