Files
cthulhu/tests/test_build_script.py
2026-11-07 13:14:22 -05:00

188 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""Exercise build/toolchain boundaries without network access or a reader."""
import hashlib
import io
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tarfile
import tempfile
import unittest
MOCK_TOOL = r'''#!/usr/bin/env python3
import json
import os
from pathlib import Path
import shutil
import sys
tool = Path(sys.argv[0]).name
args = sys.argv[1:]
with open(os.environ["BUILD_LOG"], "a") as stream:
stream.write(json.dumps([tool, args, os.getcwd(), {
name: os.environ.get(name) for name in
("GOTOOLCHAIN", "GOFLAGS", "CGO_ENABLED", "CGO_CFLAGS", "GOMODCACHE")
}]) + "\n")
if tool == "uname":
print("Linux" if args == ["-s"] else "x86_64")
elif tool == "pkg-config":
sys.exit(1 if args[-1] == os.environ.get("MISSING_LIBRARY") else 0)
elif tool == "curl":
if args[-1].endswith(".sha256"):
print(os.environ["ARCHIVE_SHA"])
else:
shutil.copyfile(os.environ["ARCHIVE_PATH"], args[args.index("--output") + 1])
elif tool in ("go", "bootstrap-go"):
if args == ["version"]:
version = "1.24.0" if tool == "bootstrap-go" else os.environ["INSTALLED_GO"]
print("go version go" + version + " linux/amd64")
elif args[0] == "build":
if args[-1] == os.environ.get("FAIL_BUILD_TARGET"):
sys.exit(2)
Path(args[args.index("-o") + 1]).touch()
'''
class BuildScriptTests(unittest.TestCase):
def setUp(self):
self.tempDir = tempfile.TemporaryDirectory(prefix="cthulhu-build-test-")
self.addCleanup(self.tempDir.cleanup)
self.root = Path(self.tempDir.name)
self.project = self.root / "checkout"
self.project.mkdir()
source = Path(__file__).resolve().parents[1] / "build-cthulhu.sh"
self.script = self.project / source.name
shutil.copyfile(source, self.script)
(self.project / "go.mod").write_text("module example.invalid/reader\n\ngo 1.24.0\n")
self.caller = self.root / "caller directory"
self.caller.mkdir()
self.binDir = self.root / "bin"
self.binDir.mkdir()
for name in ("go", "curl", "pkg-config", "uname"):
toolPath = self.binDir / name
toolPath.write_text(MOCK_TOOL)
toolPath.chmod(0o755)
# Give the downloaded Go its own name for log assertions.
bootstrap = MOCK_TOOL.replace('Path(sys.argv[0]).name', '"bootstrap-go"')
self.archive = self.root / "go.tar.gz"
with tarfile.open(self.archive, "w:gz") as archive:
data = bootstrap.encode()
entry = tarfile.TarInfo("go/bin/go")
entry.size = len(data)
entry.mode = 0o755
archive.addfile(entry, io.BytesIO(data))
self.logPath = self.root / "calls.jsonl"
self.env = dict(os.environ, PATH=f"{self.binDir}:/usr/bin:/bin",
INSTALLED_GO="1.24.0", BUILD_LOG=str(self.logPath),
XDG_CACHE_HOME=str(self.root / "cache"),
ARCHIVE_PATH=str(self.archive),
ARCHIVE_SHA=hashlib.sha256(self.archive.read_bytes()).hexdigest(),
GOTOOLCHAIN="auto", CGO_ENABLED="1", GOFLAGS="-mod=readonly",
CGO_CFLAGS="-O2", GOMODCACHE=str(self.root / "modules"))
def run_build(self, *args):
return subprocess.run(["/bin/bash", str(self.script), *args], cwd=self.caller,
env=self.env, text=True, capture_output=True, check=False)
def calls(self, name):
if not self.logPath.exists():
return []
return [call for line in self.logPath.read_text().splitlines()
if (call := json.loads(line))[0] == name]
def test_local_build_paths_flags_and_environment(self):
self.env["GOTOOLCHAIN"] = "local"
result = self.run_build("--output-dir", "output directory", "--", "-trimpath")
self.assertEqual(result.returncode, 0, result.stderr)
for binary in ("cthulhu", "cthulhu-braille", "cthulhu-input-helper"):
self.assertTrue((self.caller / "output directory" / binary).is_file())
builds = [call for call in self.calls("go") if call[1][0] == "build"]
self.assertEqual(len(builds), 3)
for call in builds:
self.assertIn("-trimpath", call[1])
self.assertEqual(call[2], str(self.project))
for name in ("GOTOOLCHAIN", "GOFLAGS", "CGO_CFLAGS", "GOMODCACHE"):
self.assertEqual(call[3][name], self.env[name])
self.assertIn("brlapi", builds[1][1])
self.assertNotIn("brlapi", builds[2][1])
self.assertFalse(self.calls("curl"))
def test_old_go_bootstrap_and_cache_reuse(self):
self.env["INSTALLED_GO"] = "1.20.14"
result = self.run_build()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue((self.project / "cthulhu-braille").is_file())
self.assertEqual(len(self.calls("curl")), 2)
result = self.run_build()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(len(self.calls("curl")), 2)
self.assertEqual(len([c for c in self.calls("bootstrap-go") if c[1][0] == "build"]), 6)
def test_checksum_failure_never_executes_download(self):
self.env.update(INSTALLED_GO="1.20.14", ARCHIVE_SHA="0" * 64)
result = self.run_build()
self.assertNotEqual(result.returncode, 0)
self.assertIn("checksum mismatch", result.stderr)
self.assertFalse(self.calls("bootstrap-go"))
self.assertFalse(list((self.root / "cache").rglob("*.download.*")))
def test_bootstrap_without_installed_go(self):
(self.binDir / "go").unlink()
for name in ("python3", "dirname", "awk", "mkdir", "mktemp", "tar",
"gzip", "sha256sum", "mv", "rm"):
toolPath = sys.executable if name == "python3" else shutil.which(name)
(self.binDir / name).symlink_to(toolPath)
self.env["PATH"] = str(self.binDir)
result = self.run_build()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(len(self.calls("curl")), 2)
self.assertTrue((self.project / "cthulhu").is_file())
def test_local_old_go_does_not_download(self):
self.env.update(INSTALLED_GO="1.20.14", GOTOOLCHAIN="local")
result = self.run_build()
self.assertNotEqual(result.returncode, 0)
self.assertIn("Install Go 1.24.0", result.stderr)
self.assertFalse(self.calls("curl"))
def test_preflight_and_arguments_do_not_download(self):
for args in (("--output-dir",), ("--unknown",)):
with self.subTest(args=args):
self.assertNotEqual(self.run_build(*args).returncode, 0)
self.assertEqual(self.run_build("--help").returncode, 0)
self.assertFalse(self.logPath.exists())
self.env.update(INSTALLED_GO="1.20.14", MISSING_LIBRARY="xres")
result = self.run_build()
self.assertIn("xres", result.stderr)
self.assertNotEqual(result.returncode, 0)
self.assertFalse(self.calls("curl"))
def test_helper_failure_is_not_reported_as_success(self):
for helper in ("cthulhu-braille", "cthulhu-input-helper"):
with self.subTest(helper=helper):
self.env["FAIL_BUILD_TARGET"] = f"./cmd/{helper}"
result = self.run_build()
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("Cthulhu has been built", result.stdout)
def test_disabled_cgo_and_output_conflicts_fail_before_building(self):
self.env["CGO_ENABLED"] = "0"
result = self.run_build()
self.assertNotEqual(result.returncode, 0)
self.assertIn("CGO_ENABLED=1", result.stderr)
self.env["CGO_ENABLED"] = "1"
(self.caller / "output").write_text("existing file")
self.assertNotEqual(self.run_build("-o", "output").returncode, 0)
(self.project / "cthulhu-braille").mkdir()
self.assertNotEqual(self.run_build().returncode, 0)
self.assertFalse(self.calls("go"))
if __name__ == "__main__":
unittest.main()