Files
.pouch/tests/test_builder_ci.py
laily 9ba9b3da0f feat(builder): query CI via project status script
When BUILD_VENUE=ci, init confirms tea/glab/gh/curl (or a custom
command) and writes .pouch/builder/ci-status.sh. wait_ci.sh only
polls that script and no longer guesses forge CLIs.
2026-10-15 09:56:51 +08:00

222 lines
8.6 KiB
Python

from __future__ import annotations
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = REPO_ROOT / "skills" / "builder" / "scripts"
WAIT_CI = SCRIPTS / "wait_ci.sh"
INIT_CI = SCRIPTS / "init_ci_status.sh"
TEMPLATES = REPO_ROOT / "skills" / "builder" / "templates" / "ci-status"
def git(cwd: Path, *args: str) -> str:
result = subprocess.run(
["git", *args],
cwd=cwd,
text=True,
capture_output=True,
check=False,
env={
**os.environ,
"GIT_AUTHOR_NAME": "Test",
"GIT_AUTHOR_EMAIL": "test@example.com",
"GIT_COMMITTER_NAME": "Test",
"GIT_COMMITTER_EMAIL": "test@example.com",
},
)
if result.returncode != 0:
raise AssertionError(f"git {args} failed: {result.stderr}")
return result.stdout.strip()
class BuilderCiTests(unittest.TestCase):
def make_repo(self, root: Path, origin: str = "https://gitea.example.test/acme/widget.git") -> str:
git(root, "init", "-b", "main")
git(root, "config", "user.email", "test@example.com")
git(root, "config", "user.name", "Test")
(root / "README").write_text("x\n", encoding="utf-8")
git(root, "add", "README")
git(root, "commit", "-m", "init")
git(root, "remote", "add", "origin", origin)
return git(root, "rev-parse", "HEAD")
def write_makefile(self, root: Path, venue: str = "ci") -> None:
(root / "makefile.builder").write_text(
"BUILD_VENUE ?= "
+ venue
+ "\n"
"CI_STATUS_SCRIPT ?= .pouch/builder/ci-status.sh\n"
"print-%:\n"
"\t@echo $($*)\n"
"venue:\n"
"\t@echo $(BUILD_VENUE)\n",
encoding="utf-8",
)
def write_status_script(self, root: Path, body: str, executable: bool = True) -> Path:
path = root / ".pouch" / "builder" / "ci-status.sh"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("#!/bin/sh\n" + body + "\n", encoding="utf-8")
if executable:
path.chmod(0o755)
return path
def fake_command(self, bin_dir: Path, name: str, body: str) -> None:
command = bin_dir / name
command.write_text(f"#!/bin/sh\n{body}\n", encoding="utf-8")
command.chmod(0o755)
def run_wait(
self,
root: Path,
sha: str,
bin_dir: Path | None = None,
extra_env: dict[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
path = os.environ["PATH"]
if bin_dir is not None:
path = f"{bin_dir}{os.pathsep}{path}"
env = {
**os.environ,
"PATH": path,
"BUILDER_CI_POLL_SEC": "0",
"BUILDER_CI_APPEAR_SEC": "0",
}
if extra_env:
env.update(extra_env)
return subprocess.run(
["bash", str(WAIT_CI), "--sha", sha, "--root", str(root)],
cwd=root,
env=env,
text=True,
capture_output=True,
check=False,
)
def test_init_copies_each_kind_template(self) -> None:
for kind in ("tea", "glab", "gh", "curl"):
with self.subTest(kind=kind), tempfile.TemporaryDirectory() as temp:
root = Path(temp)
result = subprocess.run(
["bash", str(INIT_CI), "--root", str(root), "--kind", kind],
text=True,
capture_output=True,
check=False,
)
dest = root / ".pouch" / "builder" / "ci-status.sh"
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), str(dest))
self.assertTrue(dest.is_file())
self.assertTrue(os.access(dest, os.X_OK))
self.assertEqual(
dest.read_text(encoding="utf-8"),
(TEMPLATES / f"{kind}.sh").read_text(encoding="utf-8"),
)
def test_init_rejects_unknown_kind(self) -> None:
with tempfile.TemporaryDirectory() as temp:
result = subprocess.run(
["bash", str(INIT_CI), "--root", temp, "--kind", "jenkins"],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 2)
self.assertIn("--kind must be tea, glab, gh, or curl", result.stderr)
def test_success_reads_project_script_not_forge_cli(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
sha = self.make_repo(root)
self.write_makefile(root)
self.write_status_script(
root,
'echo "STATUS=success ID=12 PIPELINE=https://gitea.example.test/run/12 JOBS=build=success"',
)
bin_dir = root / "bin"
bin_dir.mkdir()
for name in ("curl", "tea", "glab", "gh"):
self.fake_command(bin_dir, name, "exit 9")
result = self.run_wait(root, sha, bin_dir)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("OK pipeline=https://gitea.example.test/run/12", result.stdout)
self.assertIn("id=12", result.stdout)
self.assertIn("jobs=build=success", result.stdout)
def test_failed_status_is_failure(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
sha = self.make_repo(root)
self.write_makefile(root)
self.write_status_script(
root,
'echo "STATUS=failed ID=21 PIPELINE=https://gitea.example.test/run/21 JOBS=test=failure"',
)
result = self.run_wait(root, sha)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn("reason=job_failed", result.stderr)
self.assertIn("test=failure", result.stderr)
def test_empty_status_is_no_pipeline(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
sha = self.make_repo(root)
self.write_makefile(root)
self.write_status_script(root, 'echo "STATUS=empty"')
result = self.run_wait(root, sha)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn("reason=no_pipeline", result.stderr)
def test_missing_script_is_usage_error(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
sha = self.make_repo(root)
self.write_makefile(root)
result = self.run_wait(root, sha)
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
self.assertIn("CI status script missing", result.stderr)
def test_non_executable_script_is_usage_error(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
sha = self.make_repo(root)
self.write_makefile(root)
self.write_status_script(root, 'echo "STATUS=success"', executable=False)
result = self.run_wait(root, sha)
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
self.assertIn("not executable", result.stderr)
def test_relative_script_outside_builder_dir_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
sha = self.make_repo(root)
self.write_makefile(root)
rogue = root / "ci-status.sh"
rogue.write_text('#!/bin/sh\necho STATUS=success\n', encoding="utf-8")
rogue.chmod(0o755)
result = self.run_wait(
root, sha, extra_env={"CI_STATUS_SCRIPT": "ci-status.sh"}
)
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
self.assertIn("must be under .pouch/builder/", result.stderr)
def test_script_exit_3_is_cannot_query(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
sha = self.make_repo(root)
self.write_makefile(root)
self.write_status_script(root, 'echo "Error: not logged in" >&2; exit 3')
result = self.run_wait(root, sha)
self.assertEqual(result.returncode, 3, result.stdout + result.stderr)
self.assertIn("CI status script exited 3", result.stderr)
self.assertIn("not logged in", result.stderr)
if __name__ == "__main__":
unittest.main()