Files
Genarrative/scripts/test_gitea_cache_image_context.py
T
lhk229 7bc411119c
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
修复 CI 镜像脚本的 Buildx 驱动检查兼容性
读取 buildx inspect 的 Driver 字段,移除不受支持的 --format 参数。
收紧 Docker 测试替身参数检查,并覆盖错误驱动拒绝行为。
更新部署文档与共享记忆,记录真实 Buildx 兼容性验证要求。
2026-09-22 11:45:22 +00:00

201 lines
8.8 KiB
Python

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