"""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 /zig-/ (stripping the upstream wrapper dir), and symlinks /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-/ 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()