da15bda6d2
---ci--- phase: 3 milestone: v0.2 status: complete requirements: covered: [REQ-21, REQ-22, REQ-23] partial: [] ---/ci---
163 lines
5.6 KiB
Python
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() |