151 lines
7.4 KiB
Python
151 lines
7.4 KiB
Python
"""按改动选检查(scripts/test_affected.py):临时 Git 仓库 + 本地裸 origin,全部离线。"""
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
SOURCE = Path(__file__).resolve().parents[1]
|
||
SCRIPT = SOURCE / "scripts" / "test_affected.py"
|
||
|
||
|
||
class TestAffectedTests(unittest.TestCase):
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory(prefix="test-affected-")
|
||
self.addCleanup(self.tmp.cleanup)
|
||
root = Path(self.tmp.name)
|
||
self.repo = root / "repo"
|
||
self.env = {k: v for k, v in os.environ.items()
|
||
if not k.startswith(("GIT_", "MAKE")) and k not in {"BASE", "TASK_TARGET_BRANCH"}}
|
||
self.env.update({"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null",
|
||
"GIT_TERMINAL_PROMPT": "0", "HOME": str(root / "no-home"),
|
||
"GIT_AUTHOR_NAME": "Affected Test", "GIT_AUTHOR_EMAIL": "affected@example.invalid",
|
||
"GIT_COMMITTER_NAME": "Affected Test",
|
||
"GIT_COMMITTER_EMAIL": "affected@example.invalid"})
|
||
self.git(root, "init", "-q", "--bare", "--initial-branch=main", str(root / "origin.git"))
|
||
self.git(root, "init", "-q", "--initial-branch=main", str(self.repo))
|
||
self.git(self.repo, "remote", "add", "origin", str(root / "origin.git"))
|
||
self.write("README.md", "fixture\n")
|
||
self.write("modules/ui/notes.txt", "ui\n")
|
||
self.commit("Create fixture")
|
||
self.git(self.repo, "push", "-q", "origin", "main")
|
||
self.git(self.repo, "checkout", "-q", "-b", "feature/work")
|
||
|
||
def git(self, cwd, *args):
|
||
return subprocess.run(["git", *args], cwd=cwd, env=self.env, capture_output=True, text=True,
|
||
check=True).stdout.strip()
|
||
|
||
def write(self, rel, text):
|
||
path = self.repo / rel
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(text, encoding="utf-8")
|
||
|
||
def commit(self, message, gitlinks=()):
|
||
self.git(self.repo, "add", "-A")
|
||
sha = self.git(self.repo, "rev-parse", "HEAD") if gitlinks else None
|
||
for path in gitlinks:
|
||
self.git(self.repo, "update-index", "--add", "--cacheinfo", f"160000,{sha},{path}")
|
||
self.git(self.repo, "commit", "-q", "-m", message)
|
||
|
||
def run_affected(self, *args, base=None):
|
||
env = self.env | ({"BASE": base} if base is not None else {})
|
||
result = subprocess.run([sys.executable, str(SCRIPT), *args], cwd=self.repo, env=env,
|
||
capture_output=True, text=True, timeout=60)
|
||
return result.returncode, result.stdout + result.stderr
|
||
|
||
def summary(self, out):
|
||
lines = [line for line in out.splitlines() if line.startswith("[affected] base=")]
|
||
self.assertEqual(len(lines), 1, out)
|
||
return dict(part.split("=", 1) for part in lines[0].split()[1:])
|
||
|
||
def test_mapping_dry_run_orders_and_dedupes_targets(self):
|
||
self.write("scripts/tool.py", "print('x')\n")
|
||
self.write("tests/test_tool.py", "\n")
|
||
self.write("modules/core/a.go", "package a\n")
|
||
self.write("deps/agent-native/x.go", "package x\n")
|
||
self.write("deps/agent-squad/y.go", "package y\n")
|
||
self.write("docs/guide.md", "doc\n")
|
||
self.write(".task/card.md", "card\n")
|
||
self.commit("Branch work", gitlinks=["deps/agent-command"])
|
||
self.write("modules/ui/notes.txt", "uncommitted tracked change\n")
|
||
code, out = self.run_affected("--dry-run")
|
||
self.assertEqual(code, 0, out)
|
||
info = self.summary(out)
|
||
self.assertEqual(info["base"], "origin/main")
|
||
self.assertEqual(info["changed"], "9", out)
|
||
self.assertEqual(info["targets"], "test-worktree,test-governance,test-boundaries,test-pins,test-libs,"
|
||
"test-core,test-ui,test-agentcmd", out)
|
||
self.assertIn("[affected] dry-run: make test-worktree test-governance", out)
|
||
|
||
def test_gitlink_and_gitmodules_select_pins_and_module_target(self):
|
||
self.write(".gitmodules", "[submodule \"modules/backend\"]\n\tpath = modules/backend\n")
|
||
self.commit("Bump backend", gitlinks=["modules/backend"])
|
||
code, out = self.run_affected("--dry-run")
|
||
self.assertEqual(code, 0, out)
|
||
self.assertEqual(self.summary(out)["targets"],
|
||
"test-worktree,test-boundaries,test-pins,test-backend", out)
|
||
|
||
def test_lint_boundaries_and_governance_docs_select_targets(self):
|
||
self.write("scripts/lint-boundaries.sh", "#!/bin/sh\n")
|
||
self.write("AGENT-EXECUTION.md", "x\n")
|
||
self.commit("Lint")
|
||
code, out = self.run_affected("--dry-run")
|
||
self.assertEqual(code, 0, out)
|
||
self.assertEqual(self.summary(out)["targets"], "test-worktree,test-governance,test-boundaries", out)
|
||
|
||
def test_docs_only_change_runs_nothing(self):
|
||
self.write("docs/guide.md", "doc\n")
|
||
self.write(".task/card.md", "card\n")
|
||
self.write("NOTES.md", "notes\n")
|
||
self.commit("Docs only")
|
||
self.write("README.md", "edited\n")
|
||
code, out = self.run_affected()
|
||
self.assertEqual(code, 0, out)
|
||
info = self.summary(out)
|
||
self.assertEqual((info["changed"], info["targets"]), ("4", "none"), out)
|
||
self.assertIn("[affected] nothing to test", out)
|
||
self.assertNotIn("dry-run", out)
|
||
|
||
def test_base_env_resolves_bare_branch_to_origin(self):
|
||
self.write("scripts/tool.py", "x\n")
|
||
self.commit("Tool")
|
||
code, out = self.run_affected("--dry-run", base="main")
|
||
self.assertEqual(code, 0, out)
|
||
self.assertEqual(self.summary(out)["base"], "origin/main")
|
||
|
||
def test_base_env_local_branch_limits_changes_since_that_base(self):
|
||
self.write("modules/core/a.go", "package a\n")
|
||
self.commit("Core")
|
||
self.git(self.repo, "branch", "local-base")
|
||
self.write("docs/later.md", "later\n")
|
||
self.commit("Later docs")
|
||
code, out = self.run_affected("--dry-run", base="local-base")
|
||
self.assertEqual(code, 0, out)
|
||
info = self.summary(out)
|
||
self.assertEqual((info["base"], info["changed"], info["targets"]), ("local-base", "1", "none"), out)
|
||
code, out = self.run_affected("--dry-run", "--base", "origin/main", base="local-base")
|
||
self.assertEqual(self.summary(out)["base"], "origin/main", out)
|
||
|
||
def test_unresolvable_base_is_a_usage_error(self):
|
||
code, out = self.run_affected("--dry-run", base="no-such-branch")
|
||
self.assertEqual(code, 2, out)
|
||
self.assertIn("[affected] ERROR base no-such-branch does not resolve", out)
|
||
|
||
def test_default_base_is_card_immediate_target(self):
|
||
self.git(self.repo, "push", "-q", "origin", "main:feature/init")
|
||
self.git(self.repo, "fetch", "-q", "origin")
|
||
self.write(".task/child.md", "# child\n- 工作区:/tmp/x;分支:feature/work\n"
|
||
"- 分支类型:child;initiative_id:init;root_target:main;immediate_target:feature/init\n")
|
||
self.commit("Card")
|
||
code, out = self.run_affected("--dry-run")
|
||
self.assertEqual(code, 0, out)
|
||
self.assertEqual(self.summary(out)["base"], "origin/feature/init", out)
|
||
self.git(self.repo, "checkout", "-q", "--detach")
|
||
code, out = self.run_affected("--dry-run")
|
||
self.assertEqual(self.summary(out)["base"], "origin/main", out)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|