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`;模板默认
|
- 跑真实环境前,复制 `.pouch/tester/local.yaml.example` 为 `local.yaml`;模板默认
|
||||||
`HOST: "127.0.0.1"`。如果目标不是本机,显式改成目标地址。也可以用环境变量
|
`HOST: "127.0.0.1"`。如果目标不是本机,显式改成目标地址。也可以用环境变量
|
||||||
`HX_TEST_HOST` 覆盖。
|
`HX_TEST_HOST` 覆盖。
|
||||||
|
|||||||
+26
-3
@@ -11,7 +11,30 @@ description: >-
|
|||||||
# Tester:项目测试目录
|
# Tester:项目测试目录
|
||||||
|
|
||||||
开始时解析本 `SKILL.md` 所在目录为 `<skill-dir>`。项目根用
|
开始时解析本 `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 执行目标优先来自项目根 `.pouch/agent.yaml` 的 `tester.<name>` binding,由共享
|
||||||
`agent-runtime` 解析 environment、host、project、path 和 profile。`.pouch/tester/local.yaml`
|
`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):
|
用脚本读写目录,不要把 `cases/` 整库灌进上下文。术语与命名理由见 [references/concepts.md](references/concepts.md):
|
||||||
|
|
||||||
```bash
|
```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> [参数]
|
--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. 准备执行目标
|
## 0. 准备执行目标
|
||||||
|
|
||||||
先按 `execution.md` 处理目标机器和项目目录,输出配置更新结果,再进入测试流程。目标项目必须包含
|
先按 `execution.md` 处理目标机器和项目目录,输出配置更新结果,再进入测试流程。目标测试项目必须包含
|
||||||
可读取的 `.pouch/tester/`;如果目标目录没有测试目录,记为 `environment-fail`,不要
|
可读取的 `.pouch/tester/catalog.yaml`。父仓库可能只是包含多个测试子项目;运行前用
|
||||||
偷偷回退到当前项目,也不要把测试文件复制过去。
|
`catalog.py roots --root <repo-root>` 发现并选择目标根,不能把父仓库无 catalog 当成失败,
|
||||||
|
也不能偷偷回退到另一个子项目或复制测试文件。每个目标根单独生成运行快照。
|
||||||
|
|
||||||
`catalog.py select` 输出的 `agent` 是本轮使用的配置快照。记录中要保留 Agent、目标
|
`catalog.py select` 输出的 `agent` 是本轮使用的配置快照。记录中要保留 Agent、目标
|
||||||
环境、主机、项目、路径、worktree id、terminal handle,以及 terminal 是否成功关闭。
|
环境、主机、项目、路径、worktree id、terminal handle,以及 terminal 是否成功关闭。
|
||||||
|
|||||||
@@ -55,6 +55,55 @@ def catalog_dir(root: Path) -> Path:
|
|||||||
return root / ".pouch" / "tester"
|
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:
|
def load_yaml(path: Path) -> Any:
|
||||||
text = path.read_text(encoding="utf-8")
|
text = path.read_text(encoding="utf-8")
|
||||||
data = yaml.safe_load(text)
|
data = yaml.safe_load(text)
|
||||||
@@ -829,6 +878,29 @@ def cmd_export_csv(root: Path) -> int:
|
|||||||
return run_export_csv(root)
|
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:
|
def cmd_next_id(root: Path, category: str) -> int:
|
||||||
meta = load_catalog_meta(root)
|
meta = load_catalog_meta(root)
|
||||||
cat_ids = [c.get("id") for c in meta.get("categories") or [] if isinstance(c, dict)]
|
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 = sub.add_parser("show", help="打印一条用例", parents=[parent])
|
||||||
p_show.add_argument("--id", required=True, dest="case_id")
|
p_show.add_argument("--id", required=True, dest="case_id")
|
||||||
sub.add_parser("validate", help="校验目录", parents=[parent])
|
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])
|
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 = sub.add_parser("next-id", help="分配下一个 TC-<CAT>-NNN", parents=[parent])
|
||||||
p_next.add_argument("--category", required=True)
|
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)
|
return cmd_show(root, args.case_id)
|
||||||
if args.cmd == "validate":
|
if args.cmd == "validate":
|
||||||
return cmd_validate(root)
|
return cmd_validate(root)
|
||||||
|
if args.cmd == "roots":
|
||||||
|
return cmd_roots(root)
|
||||||
if args.cmd == "export-csv":
|
if args.cmd == "export-csv":
|
||||||
return cmd_export_csv(root)
|
return cmd_export_csv(root)
|
||||||
if args.cmd == "next-id":
|
if args.cmd == "next-id":
|
||||||
|
|||||||
Reference in New Issue
Block a user