1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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()
|