aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/zig-pin/test_zig_pin.py203
1 files changed, 203 insertions, 0 deletions
diff --git a/tests/zig-pin/test_zig_pin.py b/tests/zig-pin/test_zig_pin.py
new file mode 100644
index 0000000..f6d87e5
--- /dev/null
+++ b/tests/zig-pin/test_zig_pin.py
@@ -0,0 +1,203 @@
+"""Tests for the zig_install_from_tarball helper in the archsetup installer.
+
+zig_install_from_tarball is the verify-and-install core of the zig 0.15.2 pin:
+given a downloaded tarball it checks the sha256, extracts the tree to
+<opt_root>/zig-<version>/ (stripping the upstream wrapper dir), and symlinks
+<bindir>/zig at the extracted binary. It refuses — extracting nothing — on a
+sha256 mismatch, a missing tarball, or a tree with no zig binary, and it
+short-circuits when a correct install already exists. The network download is
+the thin outer install_zig_pin's job, not this function's, so this is unit
+testable.
+
+These tests exercise the REAL function body, extracted from the `archsetup`
+script at run time (not a copy), against real temp dirs and real tarballs the
+test builds. The helper is self-contained (prints its own refusal reasons to
+stderr, calls no installer logger), so no stub is needed.
+
+Run from repo root:
+ python3 -m unittest tests.zig-pin.test_zig_pin
+"""
+
+import hashlib
+import os
+import shutil
+import subprocess
+import tarfile
+import tempfile
+import unittest
+
+
+REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+ARCHSETUP = os.path.join(REPO_ROOT, "archsetup")
+VERSION = "0.15.2"
+
+
+def sha256_of(path):
+ h = hashlib.sha256()
+ with open(path, "rb") as f:
+ for chunk in iter(lambda: f.read(8192), b""):
+ h.update(chunk)
+ return h.hexdigest()
+
+
+class ZigPinHarness(unittest.TestCase):
+ """Source zig_install_from_tarball out of the real archsetup script."""
+
+ def setUp(self):
+ self.tmp = tempfile.mkdtemp(prefix="zig-pin-test-")
+ # Wrapper that extracts just zig_install_from_tarball from the real
+ # installer and invokes it. Sourcing the sed-extracted function means
+ # we test the production code path, not a reimplementation.
+ self.wrapper = os.path.join(self.tmp, "run.sh")
+ with open(self.wrapper, "w") as f:
+ f.write(
+ "#!/bin/bash\n"
+ 'ARCHSETUP="$1"; shift\n'
+ "source <(sed -n "
+ "'/^zig_install_from_tarball() {/,/^}/p' \"$ARCHSETUP\")\n"
+ 'zig_install_from_tarball "$@"\n'
+ )
+ os.chmod(self.wrapper, 0o755)
+
+ def tearDown(self):
+ shutil.rmtree(self.tmp, ignore_errors=True)
+
+ def make_tarball(self, name="tarball.tar.xz", with_zig=True):
+ """Build an upstream-shaped tarball: a single top-level dir
+ zig-x86_64-linux-<version>/ optionally containing an executable `zig`.
+ Returns (tarball_path, sha256_hex)."""
+ builddir = os.path.join(self.tmp, "build")
+ topdir = "zig-x86_64-linux-%s" % VERSION
+ treedir = os.path.join(builddir, topdir)
+ os.makedirs(treedir, exist_ok=True)
+ # a sibling file so we can confirm the whole tree lands, not just zig
+ with open(os.path.join(treedir, "LICENSE"), "w") as f:
+ f.write("MIT")
+ if with_zig:
+ zigbin = os.path.join(treedir, "zig")
+ with open(zigbin, "w") as f:
+ f.write("#!/bin/sh\necho 0.15.2\n")
+ os.chmod(zigbin, 0o755)
+ tarball = os.path.join(self.tmp, name)
+ with tarfile.open(tarball, "w:xz") as tar:
+ tar.add(treedir, arcname=topdir)
+ shutil.rmtree(builddir)
+ return tarball, sha256_of(tarball)
+
+ def run_install(self, tarball, want_sha, opt_root, bindir, version=VERSION):
+ return subprocess.run(
+ ["bash", self.wrapper, ARCHSETUP,
+ tarball, want_sha, version, opt_root, bindir],
+ capture_output=True, text=True, timeout=30,
+ )
+
+ def dirs(self):
+ opt_root = os.path.join(self.tmp, "opt")
+ bindir = os.path.join(self.tmp, "bin")
+ return opt_root, bindir
+
+
+# -----------------------------------------------------------------------------
+# Normal cases
+# -----------------------------------------------------------------------------
+
+class TestZigPinNormal(ZigPinHarness):
+
+ def test_valid_tarball_extracts_and_symlinks(self):
+ tarball, sha = self.make_tarball()
+ opt_root, bindir = self.dirs()
+ result = self.run_install(tarball, sha, opt_root, bindir)
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
+ dest = os.path.join(opt_root, "zig-%s" % VERSION)
+ self.assertTrue(os.access(os.path.join(dest, "zig"), os.X_OK),
+ "zig binary should be extracted and executable")
+ self.assertTrue(os.path.isfile(os.path.join(dest, "LICENSE")),
+ "whole tree should land, not just the binary")
+ link = os.path.join(bindir, "zig")
+ self.assertTrue(os.path.islink(link), "bindir/zig should be a symlink")
+ self.assertEqual(os.path.realpath(link), os.path.join(dest, "zig"))
+
+ def test_idempotent_second_run_skips_without_tarball(self):
+ tarball, sha = self.make_tarball()
+ opt_root, bindir = self.dirs()
+ self.assertEqual(self.run_install(tarball, sha, opt_root, bindir).returncode, 0)
+ # Second run: install already correct, so it must short-circuit before
+ # touching the tarball — pass a nonexistent path to prove no re-extract.
+ again = self.run_install(os.path.join(self.tmp, "gone.tar.xz"),
+ sha, opt_root, bindir)
+ self.assertEqual(again.returncode, 0, msg=again.stderr)
+
+
+# -----------------------------------------------------------------------------
+# Boundary cases
+# -----------------------------------------------------------------------------
+
+class TestZigPinBoundary(ZigPinHarness):
+
+ def test_stale_dest_without_link_is_reinstalled(self):
+ # A prior half-install left a dest dir but no/wrong symlink. A fresh
+ # valid run should replace it and create the link.
+ opt_root, bindir = self.dirs()
+ dest = os.path.join(opt_root, "zig-%s" % VERSION)
+ os.makedirs(dest)
+ with open(os.path.join(dest, "stale.txt"), "w") as f:
+ f.write("junk")
+ tarball, sha = self.make_tarball()
+ result = self.run_install(tarball, sha, opt_root, bindir)
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
+ self.assertFalse(os.path.exists(os.path.join(dest, "stale.txt")),
+ "stale contents should be cleared on reinstall")
+ self.assertTrue(os.path.islink(os.path.join(bindir, "zig")))
+
+ def test_existing_link_repointed_to_new_version(self):
+ # bindir/zig already points somewhere else; install should repoint it.
+ opt_root, bindir = self.dirs()
+ os.makedirs(bindir)
+ os.symlink("/usr/bin/zig", os.path.join(bindir, "zig"))
+ tarball, sha = self.make_tarball()
+ result = self.run_install(tarball, sha, opt_root, bindir)
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
+ dest = os.path.join(opt_root, "zig-%s" % VERSION)
+ self.assertEqual(os.path.realpath(os.path.join(bindir, "zig")),
+ os.path.join(dest, "zig"))
+
+
+# -----------------------------------------------------------------------------
+# Error cases
+# -----------------------------------------------------------------------------
+
+class TestZigPinErrors(ZigPinHarness):
+
+ def test_sha256_mismatch_refuses_and_installs_nothing(self):
+ tarball, _ = self.make_tarball()
+ opt_root, bindir = self.dirs()
+ bad = "0" * 64
+ result = self.run_install(tarball, bad, opt_root, bindir)
+ self.assertNotEqual(result.returncode, 0,
+ "a sha256 mismatch must fail")
+ self.assertFalse(os.path.exists(os.path.join(opt_root, "zig-%s" % VERSION)),
+ "no tree may be extracted on a sha mismatch")
+ self.assertFalse(os.path.exists(os.path.join(bindir, "zig")),
+ "no symlink may be created on a sha mismatch")
+
+ def test_missing_tarball_refuses(self):
+ opt_root, bindir = self.dirs()
+ tarball, sha = self.make_tarball()
+ os.remove(tarball)
+ result = self.run_install(tarball, sha, opt_root, bindir)
+ self.assertNotEqual(result.returncode, 0)
+
+ def test_tarball_without_zig_binary_refuses_and_cleans_up(self):
+ tarball, sha = self.make_tarball(with_zig=False)
+ opt_root, bindir = self.dirs()
+ result = self.run_install(tarball, sha, opt_root, bindir)
+ self.assertNotEqual(result.returncode, 0,
+ "a tree with no zig binary must fail")
+ self.assertFalse(os.path.exists(os.path.join(opt_root, "zig-%s" % VERSION)),
+ "the bad extracted tree should be cleaned up")
+ self.assertFalse(os.path.exists(os.path.join(bindir, "zig")),
+ "no symlink should be left behind")
+
+
+if __name__ == "__main__":
+ unittest.main()