#!/usr/bin/env python3 """Regression tests for the minimal trusted Gitea CI download-cache build context.""" from __future__ import annotations import os from pathlib import Path import shutil import shlex import stat import subprocess import tarfile import tempfile import textwrap import tomllib import unittest REPOSITORY_ROOT = Path(__file__).resolve().parent.parent IMAGE_SCRIPT = REPOSITORY_ROOT / "scripts" / "gitea-ci-job-image.sh" STATIC_CONTEXT_FILES = ( "deploy/container/gitea-ci-job.Dockerfile", "deploy/container/gitea-ci-job.Dockerfile.dockerignore", "deploy/container/gitea-ci-buildkitd.toml", "deploy/container/gitea-ci-checkout.sh", "scripts/export-ci-npm-download-cache.mjs", "package.json", "package-lock.json", "apps/admin-web/package.json", "apps/ai-game-creator-shell/package.json", "apps/desktop-shell/package.json", "apps/mobile-shell/package.json", "apps/preview-deployer-web/package.json", "packages/image-canvas-core/package.json", "packages/image-canvas-react/package.json", "packages/shared/package.json", "tools/spine-json-export-validator/package.json", "apps/ai-game-creator-shell/src-tauri/Cargo.toml", "apps/ai-game-creator-shell/src-tauri/Cargo.lock", "server-rs/Cargo.toml", "server-rs/Cargo.lock", "apps/desktop-shell/src-tauri/Cargo.toml", "apps/desktop-shell/src-tauri/Cargo.lock", ) class GiteaCiImageContextTest(unittest.TestCase): def setUp(self) -> None: self.temporary_directory = tempfile.TemporaryDirectory(ignore_cleanup_errors=True) self.root = Path(self.temporary_directory.name) self.context_archive = self.root / "context.tar" self.bin = self.root / "bin" self.bin.mkdir() self.write_fake_docker() self.execution_bin = self.wsl_path(self.bin) self.wsl_fake_root: str | None = None if os.name == "nt": self.wsl_fake_root = f"/tmp/gitea-ci-image-context-{self.root.name}" subprocess.run( [ "bash", "-c", "rm -rf {root}; mkdir -p {root}/bin; cp {source}/* {root}/bin/; chmod +x {root}/bin/*".format( root=shlex.quote(self.wsl_fake_root), source=shlex.quote(self.wsl_path(self.bin)), ), ], check=True, ) self.execution_bin = f"{self.wsl_fake_root}/bin" def tearDown(self) -> None: if self.wsl_fake_root is not None: subprocess.run( ["bash", "-c", f"rm -rf {shlex.quote(self.wsl_fake_root)}"], check=False ) self.temporary_directory.cleanup() @staticmethod def wsl_path(path: Path) -> str: value = path.resolve().as_posix() if len(value) >= 3 and value[1] == ":": return f"/mnt/{value[0].lower()}{value[2:]}" return value def write_fake_docker(self) -> None: docker = self.bin / "docker" docker.write_bytes(textwrap.dedent( """#!/usr/bin/env bash set -eu if [[ "$1" == buildx && "$2" == version ]]; then exit 0; fi if [[ "$1" == buildx && "$2" == inspect ]]; then if [[ "$#" != 3 ]]; then echo 'unsupported buildx inspect arguments' >&2; exit 2; fi printf 'Name: genarrative-ci-images\\nDriver: %s\\n' "${FAKE_BUILDX_DRIVER:-docker-container}" exit 0 fi if [[ "$1" == buildx && "$2" == build ]]; then cat > "$TAR_CAPTURE" exit 0 fi if [[ "$1" == image && "$2" == inspect ]]; then printf 'sha256:%064d\\n' 0 exit 0 fi if [[ "$1" == run ]]; then exit 0; fi echo "unexpected docker invocation" >&2 exit 9 """ ).encode("utf-8")) docker.chmod(docker.stat().st_mode | stat.S_IXUSR) def run_script(self, script: Path, *arguments: str, capture_context: bool = False, builder_driver: str = "docker-container") -> subprocess.CompletedProcess[str]: exports = [f"export PATH={shlex.quote(self.execution_bin)}:\"$PATH\""] exports.append(f"export FAKE_BUILDX_DRIVER={shlex.quote(builder_driver)}") if capture_context: exports.append(f"export TAR_CAPTURE={shlex.quote(self.wsl_path(self.context_archive))}") command = "; ".join(exports) + "; cd /; exec bash " + shlex.quote(self.wsl_path(script)) command += " " + " ".join(shlex.quote(argument) for argument in arguments) return subprocess.run( ["bash", "-c", command], env=os.environ, text=True, capture_output=True, check=False ) @staticmethod def dependency_paths(value): if not isinstance(value, dict): return for key, child in value.items(): if key in {"dependencies", "build-dependencies", "dev-dependencies"} and isinstance(child, dict): for dependency in child.values(): if isinstance(dependency, dict) and isinstance(dependency.get("path"), str): yield dependency["path"] yield from GiteaCiImageContextTest.dependency_paths(child) def test_build_rejects_a_builder_with_the_wrong_driver(self) -> None: result = self.run_script(IMAGE_SCRIPT, "build", capture_context=True, builder_driver="docker") self.assertNotEqual(result.returncode, 0) self.assertIn("must use the isolated docker-container driver", result.stderr) self.assertFalse(self.context_archive.exists()) def test_build_context_contains_all_local_dependency_manifests_and_no_source(self) -> None: result = self.run_script(IMAGE_SCRIPT, "build", capture_context=True) self.assertEqual(result.returncode, 0, result.stderr) with tarfile.open(self.context_archive) as archive: names = {member.name.removeprefix("./") for member in archive.getmembers() if member.isfile()} manifests = {Path(name) for name in names if name.endswith("Cargo.toml")} self.assertTrue(manifests) expected = set() for manifest in manifests: data = tomllib.loads((REPOSITORY_ROOT / manifest).read_text(encoding="utf-8")) for path in self.dependency_paths(data): dependency = (REPOSITORY_ROOT / manifest.parent / path).resolve() try: cargo_toml = (dependency / "Cargo.toml").relative_to(REPOSITORY_ROOT) except ValueError: continue expected.add(cargo_toml) self.assertTrue(expected) self.assertTrue(expected.issubset(manifests), sorted(expected - manifests)) self.assertFalse(any(Path(name).suffix in {".rs", ".c", ".cc", ".cpp", ".h"} for name in names)) self.assertFalse(any("target" in Path(name).parts for name in names)) self.assertFalse(any( Path(name).name.startswith(".env") or Path(name).suffix in {".pem", ".key"} for name in names )) def test_revision_tracks_vendor_manifests_but_ignores_regular_source(self) -> None: fixture = self.root / "revision-fixture" script = fixture / "scripts" / "gitea-ci-job-image.sh" script.parent.mkdir(parents=True) shutil.copy2(IMAGE_SCRIPT, script) script.chmod(script.stat().st_mode | stat.S_IXUSR) for name in STATIC_CONTEXT_FILES: target = fixture / name target.parent.mkdir(parents=True, exist_ok=True) target.write_text("fixture\n", encoding="utf-8") vendor_manifest = fixture / "apps/ai-game-creator-shell/src-tauri/vendor/example/Cargo.toml" vendor_manifest.parent.mkdir(parents=True) vendor_manifest.write_text('[package]\nname = "example"\nversion = "0.1.0"\n', encoding="utf-8") bridge_manifest = fixture / "plugins/agc-example-editor/native/example-editor-bridge/Cargo.toml" bridge_manifest.parent.mkdir(parents=True) bridge_manifest.write_text('[package]\nname = "bridge"\nversion = "0.1.0"\n', encoding="utf-8") (fixture / "server-rs/crates").mkdir(parents=True) first = self.run_script(script, "revision") self.assertEqual(first.returncode, 0, first.stderr) source = fixture / "apps/ai-game-creator-shell/src-tauri/src/lib.rs" source.parent.mkdir(parents=True) source.write_text("pub fn ignored() {}\n", encoding="utf-8") self.assertEqual(self.run_script(script, "revision").stdout, first.stdout) vendor_manifest.write_text('[package]\nname = "example"\nversion = "0.2.0"\n', encoding="utf-8") changed = self.run_script(script, "revision") self.assertEqual(changed.returncode, 0, changed.stderr) self.assertNotEqual(changed.stdout, first.stdout) if __name__ == "__main__": unittest.main()