#!/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()