donau

Donation authority for GNU Taler (experimental)
Log | Files | Refs | Submodules | README | LICENSE

test_version.py (10693B)


      1 #!/usr/bin/env python3
      2 # This file is in the public domain.
      3 """Run with python3 contrib/ci/test_version.py (Git; optional dpkg comparator)."""
      4 
      5 import contextlib
      6 import importlib.util
      7 import io
      8 import os
      9 from pathlib import Path
     10 import shutil
     11 import subprocess
     12 import tempfile
     13 import unittest
     14 import uuid
     15 
     16 CI = Path(__file__).resolve().parent
     17 spec = importlib.util.spec_from_file_location("ci_version", CI / "debian-version.py")
     18 version = importlib.util.module_from_spec(spec)
     19 spec.loader.exec_module(version)
     20 
     21 
     22 def run(*args, cwd, check=True, env=None):
     23     return subprocess.run(args, cwd=cwd, check=check, text=True,
     24                           capture_output=True, env=env)
     25 
     26 
     27 def script(path, text):
     28     path.parent.mkdir(parents=True, exist_ok=True)
     29     path.write_text("#!/bin/sh\n" + text + "\n")
     30     path.chmod(0o755)
     31 
     32 
     33 def install_scripts(repo):
     34     for source in [CI / "debian-version.py", *CI.rglob("version.sh")]:
     35         dest = repo / "contrib/ci" / source.relative_to(CI)
     36         dest.parent.mkdir(parents=True, exist_ok=True)
     37         shutil.copy2(source, dest)
     38 
     39 
     40 class PolicyTests(unittest.TestCase):
     41     def test_tag_order_and_metadata_ties(self):
     42         cases = [
     43             (["v1.9.9", "v1.10.0-dev.0"], "v1.10.0-dev.0"),
     44             (["v1.2.3-dev.9", "v1.2.3-dev.10"], "v1.2.3-dev.10"),
     45             (["v1.2.3-dev.999", "v1.2.3"], "v1.2.3"),
     46             (["v1.2.3+z", "v1.2.3+a", "v1.2.3"], "v1.2.3"),
     47             (["v1.2.3+z", "v1.2.3+a"], "v1.2.3+a"),
     48             (["v1.2.3-dev.1+x", "v1.2.3-dev.1"], "v1.2.3-dev.1"),
     49         ]
     50         for tags, expected in cases:
     51             for names in (tags, list(reversed(tags))):
     52                 self.assertEqual(version.select_tag(names), expected)
     53 
     54     def test_unsupported_tags_are_reported(self):
     55         tags = ["v01.2.3", "v1.02.3", "v1.2.03", "v1.2.3-dev.01",
     56                 "1.2.3", "v1.2.3-rc.1", "v1.2.3-dev.1.2", "v1.2.3-0.1",
     57                 "v1.2.3+", "v1.2.3+a..b", "v1.2.3+bad_tag", "v0.9.4a"]
     58         with contextlib.redirect_stderr(io.StringIO()) as diagnostics:
     59             self.assertEqual(version.select_tag(tags + ["v0.0.0"]), "v0.0.0")
     60             with self.assertRaises(ValueError):
     61                 version.select_tag(tags)
     62         for tag in tags:
     63             self.assertIn(tag, diagnostics.getvalue())
     64 
     65     def test_mappings(self):
     66         cases = [
     67             ("v1.2.3", True, 0, "1.2.3", "1.2.3"),
     68             ("v1.2.3", False, 0, "1.2.4-0.0", "1.2.4~0.0"),
     69             ("v1.2.3", False, 12, "1.2.4-0.12", "1.2.4~0.12"),
     70             ("v1.2.4-dev.9", True, 0, "1.2.4-dev.9", "1.2.4~dev9"),
     71             ("v1.2.4-dev.9", False, 0, "1.2.4-dev.9.0", "1.2.4~dev9.0"),
     72             ("v1.2.4-dev.9", False, 12, "1.2.4-dev.9.12", "1.2.4~dev9.12"),
     73         ]
     74         for tag, exact, count, semver, debian in cases:
     75             for metadata in ("", "+build.001", "+other"):
     76                 self.assertEqual(version.versions(tag + metadata, exact, count),
     77                                  (semver, debian))
     78 
     79     @unittest.skipUnless(shutil.which("dpkg"), "dpkg is needed for Debian ordering")
     80     def test_debian_and_semver_order_agree(self):
     81         # This sequence is increasing under SemVer's numeric identifier and
     82         # longer-prerelease rules, including patch rollover and dev 9 -> 10.
     83         cases = [("v1.2.3", True, 0)]
     84         cases += [("v1.2.3", False, n) for n in (0, 2, 10)]
     85         for dev in (0, 1, 9, 10):
     86             cases += [(f"v1.2.4-dev.{dev}", True, 0)]
     87             cases += [(f"v1.2.4-dev.{dev}", False, n) for n in (0, 2, 10)]
     88         cases += [("v1.2.4", True, 0), ("v1.2.4", False, 0),
     89                   ("v1.2.99", False, 10), ("v1.3.0-dev.0", True, 0)]
     90         pairs = [version.versions(*case) for case in cases]
     91         for index, (_, left) in enumerate(pairs):
     92             for _, right in pairs[index + 1:]:
     93                 result = run("dpkg", "--compare-versions", left, "lt", right,
     94                              cwd=CI, check=False)
     95                 self.assertEqual(result.returncode, 0, (left, right))
     96             run("dpkg", "--compare-versions", left, "eq", left, cwd=CI)
     97 
     98 
     99 class GitTests(unittest.TestCase):
    100     def setUp(self):
    101         self.temp = tempfile.TemporaryDirectory()
    102         self.addCleanup(self.temp.cleanup)
    103         self.base = Path(self.temp.name)
    104         self.origin = self.base / "origin"
    105         self.repo = self.base / "checkout"
    106         run("git", "init", "-q", str(self.origin), cwd=self.base)
    107         self.git("config", "user.name", "Version test")
    108         self.git("config", "user.email", "version@example.invalid")
    109         self.commit()
    110 
    111     def git(self, *args):
    112         return run("git", *args, cwd=self.origin).stdout.strip()
    113 
    114     def commit(self):
    115         self.git("-c", "commit.gpgsign=false", "commit", "--allow-empty", "-qm", str(uuid.uuid4()))
    116         return self.git("rev-parse", "HEAD")
    117 
    118     def clone(self, shallow=False):
    119         args = ["git", "clone", "-q"] + (["--depth=1"] if shallow else [])
    120         run(*args, self.origin.as_uri(), str(self.repo), cwd=self.base)
    121         install_scripts(self.repo)
    122         run("git", "checkout", "--detach", cwd=self.repo)
    123 
    124     def check_version(self, expected, selected=None):
    125         nested = self.repo / "packages/test"
    126         nested.mkdir(parents=True, exist_ok=True)
    127         for entry in (self.repo / "contrib/ci").rglob("version.sh"):
    128             with self.subTest(entry=entry.relative_to(self.repo)):
    129                 result = run(str(entry), cwd=nested)
    130                 self.assertEqual(result.stdout, expected + "\n")
    131                 self.assertIn("HEAD:", result.stderr)
    132                 if selected:
    133                     self.assertIn(f"Version tag: {selected};", result.stderr)
    134 
    135     def test_detached_annotated_and_lightweight_tags(self):
    136         self.git("-c", "tag.gpgsign=false", "tag", "-am", "release", "v1.2.3")
    137         self.git("tag", "v1.2.3+metadata")
    138         self.clone()
    139         self.check_version("1.2.3", "v1.2.3")
    140 
    141     def test_fetches_new_tags_without_moving_head(self):
    142         self.clone()
    143         head = self.git("rev-parse", "HEAD")
    144         self.git("tag", "v1.2.3-dev.10")
    145         self.check_version("1.2.3~dev10")
    146         self.assertEqual(run("git", "rev-parse", "HEAD", cwd=self.repo).stdout.strip(), head)
    147 
    148     def test_snapshots_after_stable_and_dev_tags(self):
    149         self.git("tag", "v1.2.3")
    150         self.commit()
    151         self.clone()
    152         self.check_version("1.2.4~0.1")
    153         self.git("tag", "v1.2.4-dev.9", "HEAD~1")
    154         self.check_version("1.2.4~dev9.1")
    155 
    156     def test_highest_tag_on_another_branch(self):
    157         base = self.git("rev-parse", "HEAD")
    158         self.git("checkout", "-qb", "release")
    159         self.commit()
    160         self.git("tag", "v2.0.0-dev.10")
    161         self.git("checkout", "--detach", base)
    162         self.commit()
    163         self.git("tag", "v1.0.0")
    164         self.clone()
    165         self.check_version("2.0.0~dev10.1", "v2.0.0-dev.10")
    166 
    167     def test_zero_distance_is_not_exact(self):
    168         base = self.git("rev-parse", "HEAD")
    169         self.commit()
    170         self.git("tag", "v2.0.0")
    171         self.git("checkout", "--detach", base)
    172         self.clone()
    173         self.check_version("2.0.1~0.0")
    174 
    175     def test_errors_have_no_version_output(self):
    176         self.clone()
    177         entry = str(self.repo / "contrib/ci/version.sh")
    178         result = run(entry, cwd=self.repo, check=False)
    179         self.assertNotEqual(result.returncode, 0)
    180         self.assertEqual(result.stdout, "")
    181         self.assertIn("no supported version tags", result.stderr)
    182         self.git("tag", "v1.0.0")
    183         run("git", "remote", "set-url", "origin", str(self.base / "missing"), cwd=self.repo)
    184         result = run(entry, cwd=self.repo, check=False)
    185         self.assertNotEqual(result.returncode, 0)
    186         self.assertEqual(result.stdout, "")
    187         self.assertIn("Cannot determine CI version", result.stderr)
    188 
    189     def test_shallow_checkout_is_rejected(self):
    190         self.git("tag", "v1.0.0")
    191         self.commit()
    192         self.clone(shallow=True)
    193         result = run(str(self.repo / "contrib/ci/version.sh"), cwd=self.repo, check=False)
    194         self.assertNotEqual(result.returncode, 0)
    195         self.assertEqual(result.stdout, "")
    196         self.assertIn("full Git checkout is required", result.stderr)
    197 
    198 
    199 class PackagingTests(unittest.TestCase):
    200     def test_callers_stop_on_failed_or_empty_version_and_pass_success(self):
    201         callers = [p for p in CI.rglob("*.sh")
    202                    if p.is_file() and "VERSION=" in p.read_text()]
    203         self.assertTrue(callers)
    204         for caller in callers:
    205             for output, status in (("", 42), ("", 0), ("1.2.3~dev9.12", 0)):
    206                 with self.subTest(caller=caller.relative_to(CI), output=output, status=status):
    207                     with tempfile.TemporaryDirectory() as temp:
    208                         repo = Path(temp)
    209                         install_scripts(repo)
    210                         dest = repo / "contrib/ci" / caller.relative_to(CI)
    211                         dest.parent.mkdir(parents=True, exist_ok=True)
    212                         shutil.copy2(caller, dest)
    213                         # Stub all version entry points to isolate shell error handling.
    214                         for entry in (repo / "contrib/ci").rglob("version.sh"):
    215                             script(entry, f"printf '%s' '{output}'\nexit {status}")
    216                         script(repo / "bootstrap", "exit 0")
    217                         for pkg in ("taler-wallet-cli", "taler-harness", "test-webui"):
    218                             (repo / "packages" / pkg / "debian").mkdir(parents=True)
    219                         bindir = repo / "bin"
    220                         for name in ("apt-get", "mk-build-deps"):
    221                             script(bindir / name, "exit 0")
    222                         # Stop at the first changelog operation; never build/install.
    223                         marker = repo / "changelog-called"
    224                         stub = f'printf "%s" "$VERSION" > "{marker}"\nexit 73'
    225                         script(bindir / "gbp", stub)
    226                         script(repo / "contrib/ci/write-debian-changelog.sh", stub)
    227                         env = dict(os.environ, PATH=f"{bindir}:{os.environ['PATH']}")
    228                         result = run("bash", str(dest), "test-webui", cwd=repo, env=env, check=False)
    229                         if output:
    230                             self.assertEqual(result.returncode, 73, result.stderr)
    231                             self.assertEqual(marker.read_text(), output)
    232                         else:
    233                             self.assertNotEqual(result.returncode, 0, result.stderr)
    234                             self.assertFalse(marker.exists(), result.stderr)
    235 
    236 
    237 if __name__ == "__main__":
    238     unittest.main()