libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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