fix(tester): discover catalogs in subprojects
This commit is contained in:
@@ -13,7 +13,9 @@
|
||||
|
||||
## 使用前准备
|
||||
|
||||
- 目录在项目根的 `.pouch/tester/`。没有就说「初始化 tester」。
|
||||
- 目录在目标测试项目根的 `.pouch/tester/`。父仓库和 `app` 等 Git submodule 可以各自有
|
||||
catalog;先运行 `catalog.py roots --root <repo-root>` 发现它们,不要把两个项目的 cases 合并。
|
||||
没有任何 catalog 才说「初始化 tester」。
|
||||
- 跑真实环境前,复制 `.pouch/tester/local.yaml.example` 为 `local.yaml`;模板默认
|
||||
`HOST: "127.0.0.1"`。如果目标不是本机,显式改成目标地址。也可以用环境变量
|
||||
`HX_TEST_HOST` 覆盖。
|
||||
|
||||
+26
-3
@@ -11,7 +11,30 @@ description: >-
|
||||
# Tester:项目测试目录
|
||||
|
||||
开始时解析本 `SKILL.md` 所在目录为 `<skill-dir>`。项目根用
|
||||
`git rev-parse --show-toplevel`。目录固定为 `<root>/.pouch/tester/`。
|
||||
`git rev-parse --show-toplevel`。目录固定为目标测试项目的 `<root>/.pouch/tester/`;父仓库
|
||||
和 Git submodule 可以各自拥有独立目录。
|
||||
|
||||
## 多测试项目 / 子项目
|
||||
|
||||
一个仓库可能同时包含后端测试目录和 `app` 等子项目测试目录。用例属于拥有
|
||||
`catalog.yaml` 的项目,不能把多个 catalog 的 cases 合并成一份清单或共用一次运行快照。
|
||||
|
||||
开始列出或运行前,先发现目标仓库下的独立测试根:
|
||||
|
||||
```bash
|
||||
python3 <skill-dir>/scripts/catalog.py roots --root <repo-root>
|
||||
```
|
||||
|
||||
选择规则:
|
||||
|
||||
- `<repo-root>/.pouch/tester/catalog.yaml` 存在时,它是一个独立测试根;
|
||||
- 同时检查 Git submodule 和一层普通子项目目录;
|
||||
- 只有一个嵌套 catalog 时,可直接用该路径继续 `list/select/run`;
|
||||
- 有多个 catalog 时按用户指定的组件分别执行,例如 `--root app`,不要猜测或合并;
|
||||
- 每个测试根单独生成 `.pouch/tester/runs/*.yaml`/`.csv`,交付记录可分别引用这些快照。
|
||||
|
||||
如果父仓库没有 catalog 但子项目有 catalog,不能把“父目录无 catalog”报告为环境失败;先用
|
||||
`roots` 找到子项目并继续。只有发现不到任何 catalog,才按缺少 tester 目录处理。
|
||||
|
||||
Agent 执行目标优先来自项目根 `.pouch/agent.yaml` 的 `tester.<name>` binding,由共享
|
||||
`agent-runtime` 解析 environment、host、project、path 和 profile。`.pouch/tester/local.yaml`
|
||||
@@ -21,7 +44,7 @@ Agent 执行目标优先来自项目根 `.pouch/agent.yaml` 的 `tester.<name>`
|
||||
用脚本读写目录,不要把 `cases/` 整库灌进上下文。术语与命名理由见 [references/concepts.md](references/concepts.md):
|
||||
|
||||
```bash
|
||||
python3 <skill-dir>/scripts/catalog.py <list|select|show|validate|next-id|run-script> \
|
||||
python3 <skill-dir>/scripts/catalog.py <roots|list|select|show|validate|next-id|run-script> \
|
||||
--root <project-root> [参数]
|
||||
```
|
||||
|
||||
@@ -34,7 +57,7 @@ tester agent 配置。
|
||||
|
||||
## 选择模式
|
||||
|
||||
- 还没有 `.pouch/tester/catalog.yaml`,或用户说初始化 tester:执行「初始化」。
|
||||
- 先用 `roots` 检查当前仓库及子项目;没有任何 `.pouch/tester/catalog.yaml`,或用户说初始化 tester:执行「初始化」。
|
||||
- 列出、有哪些测试、一共多少条:执行「列出」。
|
||||
- 看某一条怎么测:执行「查看」。
|
||||
- 加一条 / 完善 / 改验证 / 退役:执行「维护」。
|
||||
|
||||
@@ -9,9 +9,10 @@ Agent、执行目标和临时 terminal 的完整协议见 [execution.md](executi
|
||||
|
||||
## 0. 准备执行目标
|
||||
|
||||
先按 `execution.md` 处理目标机器和项目目录,输出配置更新结果,再进入测试流程。目标项目必须包含
|
||||
可读取的 `.pouch/tester/`;如果目标目录没有测试目录,记为 `environment-fail`,不要
|
||||
偷偷回退到当前项目,也不要把测试文件复制过去。
|
||||
先按 `execution.md` 处理目标机器和项目目录,输出配置更新结果,再进入测试流程。目标测试项目必须包含
|
||||
可读取的 `.pouch/tester/catalog.yaml`。父仓库可能只是包含多个测试子项目;运行前用
|
||||
`catalog.py roots --root <repo-root>` 发现并选择目标根,不能把父仓库无 catalog 当成失败,
|
||||
也不能偷偷回退到另一个子项目或复制测试文件。每个目标根单独生成运行快照。
|
||||
|
||||
`catalog.py select` 输出的 `agent` 是本轮使用的配置快照。记录中要保留 Agent、目标
|
||||
环境、主机、项目、路径、worktree id、terminal handle,以及 terminal 是否成功关闭。
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user