fix(tester): discover catalogs in subprojects

This commit is contained in:
2026-10-14 09:26:39 +08:00
parent 95946d4278
commit f8e490e9b5
4 changed files with 108 additions and 7 deletions
+75
View File
@@ -55,6 +55,55 @@ def catalog_dir(root: Path) -> Path:
return root / ".pouch" / "tester"
def catalog_path(root: Path) -> Path:
return catalog_dir(root) / "catalog.yaml"
def discover_catalog_roots(root: Path) -> list[Path]:
"""Find tester projects owned by a repository and its Git submodules.
A monorepo or a parent repository may contain more than one independent
tester catalog. Cases must stay scoped to the project that owns them; this
helper only discovers roots and never merges their cases.
"""
root = root.resolve()
candidates: list[Path] = []
if catalog_path(root).is_file():
candidates.append(root)
# Prefer the authoritative submodule list so generated directories and
# vendored dependencies are not accidentally treated as test projects.
try:
proc = subprocess.run(
["git", "-C", str(root), "config", "--file", ".gitmodules", "--get-regexp", r"^submodule\..*\.path$"],
capture_output=True,
text=True,
check=False,
)
for line in proc.stdout.splitlines():
_, _, raw_path = line.partition(" ")
subroot = (root / raw_path.strip()).resolve()
if raw_path.strip() and catalog_path(subroot).is_file() and subroot not in candidates:
candidates.append(subroot)
except OSError:
pass
# Also support a plain nested package that is not declared as a submodule.
# Only inspect a shallow set of project directories; callers can pass that
# directory explicitly when a deeper layout is intentional.
ignored = {".git", ".pouch", "node_modules", "vendor", "third_party", "dist", "build"}
try:
children = list(root.iterdir())
except OSError:
children = []
for child in children:
if not child.is_dir() or child.name in ignored or child.name.startswith("."):
continue
if catalog_path(child).is_file() and child.resolve() not in candidates:
candidates.append(child.resolve())
return sorted(candidates)
def load_yaml(path: Path) -> Any:
text = path.read_text(encoding="utf-8")
data = yaml.safe_load(text)
@@ -829,6 +878,29 @@ def cmd_export_csv(root: Path) -> int:
return run_export_csv(root)
def cmd_roots(root: Path) -> int:
"""List independent tester catalog roots below a repository."""
roots = discover_catalog_roots(root)
entries: list[dict[str, str]] = []
for item in roots:
project = ""
try:
meta = load_catalog_meta(item)
project = str((meta.get("project") or {}).get("name") or "")
except (SystemExit, AttributeError):
pass
entries.append(
{
"root": str(item),
"catalog": str(catalog_path(item)),
"project": project,
"relative": str(item.relative_to(root.resolve())) if item != root.resolve() else ".",
}
)
print(json.dumps({"base": str(root.resolve()), "count": len(entries), "roots": entries}, ensure_ascii=False, indent=2))
return 0
def cmd_next_id(root: Path, category: str) -> int:
meta = load_catalog_meta(root)
cat_ids = [c.get("id") for c in meta.get("categories") or [] if isinstance(c, dict)]
@@ -988,6 +1060,7 @@ def build_parser() -> argparse.ArgumentParser:
p_show = sub.add_parser("show", help="打印一条用例", parents=[parent])
p_show.add_argument("--id", required=True, dest="case_id")
sub.add_parser("validate", help="校验目录", parents=[parent])
sub.add_parser("roots", help="发现当前仓库及子项目的 tester 根目录", parents=[parent])
sub.add_parser("export-csv", help="写出给人看的 cases.csv", parents=[parent])
p_next = sub.add_parser("next-id", help="分配下一个 TC-<CAT>-NNN", parents=[parent])
p_next.add_argument("--category", required=True)
@@ -1008,6 +1081,8 @@ def main(argv: list[str] | None = None) -> int:
return cmd_show(root, args.case_id)
if args.cmd == "validate":
return cmd_validate(root)
if args.cmd == "roots":
return cmd_roots(root)
if args.cmd == "export-csv":
return cmd_export_csv(root)
if args.cmd == "next-id":