Files
broker/tests/test_build_cli.py
T
coreci da15bda6d2 docs(milestone): complete cli-packaging
---ci---
phase: 3
milestone: v0.2
status: complete
requirements:
  covered: [REQ-21, REQ-22, REQ-23]
  partial: []
---/ci---
2026-10-11 00:32:26 +00:00

163 lines
5.6 KiB
Python

"""Reproducibility and packaging tests for the coreci-broker CLI build.
Covers scripts/build_cli.py (REQ-21): two consecutive same-environment
builds are byte-identical, `_cli_version.VERSION` is embedded, the archive
contains exactly the staging allowlist, entry metadata is normalized to
constants (G-009), both new files py_compile green, and the built artifact
answers `--version` with exit code 0 (shebang + exec bit verified).
"""
import contextlib
import hashlib
import importlib.util
import io
import os
import pathlib
import py_compile
import subprocess
import sys
import tempfile
import unittest
import zipfile
REPO = pathlib.Path(__file__).resolve().parent.parent
BUILD_CLI_PATH = REPO / "scripts" / "build_cli.py"
SHIM_PATH = REPO / "cli" / "__main__.py"
VERSION = "v0.2.0"
UI_PAGES = (
"app_registrations.html",
"audit.html",
"dashboard.html",
"index.html",
"login.html",
"nav.js",
"shared.css",
"user_allowlist.html",
)
FILE_EXTERNAL_ATTR = 0o100644 << 16
DIR_EXTERNAL_ATTR = 0o40755 << 16
SHEBANG = b"#!/usr/bin/env python3\n"
FIXED_DATE_TIME = (1980, 1, 1, 0, 0, 0)
def _load_build_cli():
spec = importlib.util.spec_from_file_location("build_cli", BUILD_CLI_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
build_cli = _load_build_cli()
def _sha256(path):
digest = hashlib.sha256()
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(65536), b""):
digest.update(chunk)
return digest.hexdigest()
class BuildCliTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.build_root = tempfile.TemporaryDirectory(prefix="coreci-broker-pyz-")
cls.addClassCleanup(cls.build_root.cleanup)
cls.artifact = build_cli.build(
VERSION, repo=str(REPO), dist_dir=cls.build_root.name
)
cls.artifact_sha256 = _sha256(cls.artifact)
def test_two_builds_byte_identical(self):
digests = []
for _ in range(2):
with tempfile.TemporaryDirectory(prefix="coreci-broker-pyz-") as dist_dir:
artifact = build_cli.build(
VERSION, repo=str(REPO), dist_dir=dist_dir
)
digests.append(_sha256(artifact))
self.assertEqual(digests[0], digests[1])
self.assertEqual(self.artifact_sha256, digests[0])
print("two-build sha256: {}".format(digests[0]))
def test_cli_version_module_embedded(self):
with zipfile.ZipFile(self.artifact) as archive:
source = archive.read("_cli_version.py").decode("utf-8")
self.assertEqual('VERSION = "{}"\n'.format(VERSION), source)
namespace = {}
exec(compile(source, "_cli_version.py", "exec"), namespace)
self.assertEqual(VERSION, namespace["VERSION"])
def test_archive_allowlist_exact(self):
ui_dir = REPO / "coreci_broker_ui"
on_disk = {entry.name for entry in ui_dir.iterdir() if entry.is_file()}
self.assertEqual(set(UI_PAGES), on_disk)
expected = {
"__main__.py",
"_cli_version.py",
"coreci_broker.py",
"coreci_broker_client.py",
"coreci_broker_ui/",
}
expected.update("coreci_broker_ui/{}".format(name) for name in UI_PAGES)
with zipfile.ZipFile(self.artifact) as archive:
self.assertEqual(expected, set(archive.namelist()))
def test_entry_metadata_constants(self):
with zipfile.ZipFile(self.artifact) as archive:
infos = archive.infolist()
names = [info.filename for info in infos]
self.assertEqual(sorted(names), names)
self.assertTrue(any(info.is_dir() for info in infos))
self.assertTrue(any(not info.is_dir() for info in infos))
for info in infos:
self.assertEqual(FIXED_DATE_TIME, info.date_time)
if info.is_dir():
self.assertEqual(DIR_EXTERNAL_ATTR, info.external_attr)
else:
self.assertEqual(FILE_EXTERNAL_ATTR, info.external_attr)
self.assertEqual(zipfile.ZIP_DEFLATED, info.compress_type)
def test_py_compile_green(self):
for path in (SHIM_PATH, BUILD_CLI_PATH):
py_compile.compile(str(path), doraise=True)
def test_version_flag(self):
completed = subprocess.run(
[sys.executable, self.artifact, "--version"],
capture_output=True,
text=True,
timeout=60,
)
self.assertEqual(0, completed.returncode)
self.assertEqual(VERSION, completed.stdout.strip())
def test_shebang_and_exec_bit(self):
with open(self.artifact, "rb") as handle:
self.assertEqual(SHEBANG, handle.readline())
self.assertTrue(os.stat(self.artifact).st_mode & 0o111)
completed = subprocess.run(
[self.artifact, "--version"],
capture_output=True,
text=True,
timeout=60,
)
self.assertEqual(0, completed.returncode)
self.assertEqual(VERSION, completed.stdout.strip())
def test_artifact_name_no_double_v(self):
self.assertEqual("coreci-broker-v0.2.0.pyz", build_cli.artifact_name(VERSION))
self.assertEqual("coreci-broker-v0.2.0.pyz", build_cli.artifact_name("0.2.0"))
self.assertEqual("coreci-broker-v0.2.0.pyz", os.path.basename(self.artifact))
def test_cli_requires_version_argument(self):
with contextlib.redirect_stderr(io.StringIO()):
with self.assertRaises(SystemExit) as raised:
build_cli.main([])
self.assertEqual(2, raised.exception.code)
if __name__ == "__main__":
unittest.main()