feat(ack): shard task records for merge-safe workspaces
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 .pouch/tester/cases/*.yaml 生成给人看的 cases.csv。不要手改 CSV。"""
|
||||
"""从 .pouch/tester/cases/*.yaml 生成给人看的 cases.csv。不要手改 CSV。
|
||||
|
||||
运行结果属于 runs/ 下的追加式记录,不回写本清单,避免多个工作区共同
|
||||
刷新运行状态造成合并冲突。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -32,11 +36,6 @@ FIELDS = (
|
||||
"how",
|
||||
"expected",
|
||||
"notes",
|
||||
"last_run",
|
||||
"last_result",
|
||||
"last_executed",
|
||||
"last_evidence",
|
||||
"last_notes",
|
||||
)
|
||||
|
||||
|
||||
@@ -148,28 +147,8 @@ def sort_cases(root: Path, cases: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
)
|
||||
|
||||
|
||||
def last_run_lookup(root: Path) -> tuple[str, dict[str, dict[str, Any]]]:
|
||||
from generate_run_csv import latest_run_yaml, load_run, results_by_id
|
||||
|
||||
run_path = latest_run_yaml(root)
|
||||
if run_path is None:
|
||||
return "", {}
|
||||
try:
|
||||
run = load_run(run_path)
|
||||
except SystemExit:
|
||||
return "", {}
|
||||
try:
|
||||
rel = str(run_path.relative_to(root))
|
||||
except ValueError:
|
||||
rel = str(run_path)
|
||||
return rel, results_by_id(run)
|
||||
|
||||
|
||||
def export_cases_csv(root: Path, out: Path | None = None) -> Path:
|
||||
from generate_run_csv import executed_cell
|
||||
|
||||
cases = sort_cases(root, load_cases(root))
|
||||
last_run, last_by_id = last_run_lookup(root)
|
||||
path = out or (tester_dir(root) / "cases.csv")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8-sig", newline="") as handle:
|
||||
@@ -177,8 +156,6 @@ def export_cases_csv(root: Path, out: Path | None = None) -> Path:
|
||||
writer.writeheader()
|
||||
for case in cases:
|
||||
cid = str(case.get("id") or "")
|
||||
last = last_by_id.get(cid) or {}
|
||||
last_result = str(last.get("result") or "") if last else ""
|
||||
writer.writerow(
|
||||
{
|
||||
"category": case.get("category") or "",
|
||||
@@ -197,11 +174,6 @@ def export_cases_csv(root: Path, out: Path | None = None) -> Path:
|
||||
"how": how_brief(case.get("how")),
|
||||
"expected": one_line(case.get("expected")),
|
||||
"notes": one_line(case.get("notes")),
|
||||
"last_run": last_run if last else "",
|
||||
"last_result": last_result,
|
||||
"last_executed": executed_cell(last_result) if last else "",
|
||||
"last_evidence": one_line(last.get("evidence")) if last else "",
|
||||
"last_notes": one_line(last.get("notes")) if last else "",
|
||||
}
|
||||
)
|
||||
return path
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
@@ -19,6 +20,7 @@ except ImportError: # pragma: no cover
|
||||
from generate_cases_csv import load_cases, one_line, resolve_root, tester_dir
|
||||
|
||||
RESULTS = ("pass", "fail", "environment-fail", "skipped", "not-run")
|
||||
RUN_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||
EXECUTED_YES = frozenset({"pass", "fail", "environment-fail"})
|
||||
EXECUTED_NO = frozenset({"skipped", "not-run"})
|
||||
FIELDS = (
|
||||
@@ -56,6 +58,22 @@ def latest_run_yaml(root: Path) -> Path | None:
|
||||
return files[-1] if files else None
|
||||
|
||||
|
||||
def new_run_id(scope: str, suffix: str | None = None) -> str:
|
||||
"""Return a collision-resistant filename/id for a new run snapshot."""
|
||||
from datetime import datetime, timezone
|
||||
from secrets import token_hex
|
||||
|
||||
clean_scope = re.sub(r"[^A-Za-z0-9._-]+", "-", scope).strip("-") or "run"
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||
clean_suffix = re.sub(r"[^A-Za-z0-9._-]+", "-", suffix or "").strip("-")
|
||||
random_part = token_hex(3)
|
||||
parts = [stamp, clean_scope]
|
||||
if clean_suffix:
|
||||
parts.append(clean_suffix)
|
||||
parts.append(random_part)
|
||||
return "-".join(parts)
|
||||
|
||||
|
||||
def load_run(path: Path) -> dict[str, Any]:
|
||||
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||
if not isinstance(data, dict):
|
||||
@@ -63,6 +81,11 @@ def load_run(path: Path) -> dict[str, Any]:
|
||||
results = data.get("results")
|
||||
if not isinstance(results, list):
|
||||
raise SystemExit(f"{path} 缺少 results 列表")
|
||||
run_id = data.get("runId")
|
||||
if run_id is not None and (
|
||||
not isinstance(run_id, str) or RUN_ID_RE.fullmatch(run_id) is None
|
||||
):
|
||||
raise SystemExit(f"{path} 的 runId 格式无效")
|
||||
return data
|
||||
|
||||
|
||||
@@ -88,7 +111,6 @@ def export_run_csv(
|
||||
root: Path,
|
||||
run_path: Path | None = None,
|
||||
out: Path | None = None,
|
||||
refresh_cases: bool = True,
|
||||
) -> Path:
|
||||
path = run_path or latest_run_yaml(root)
|
||||
if path is None:
|
||||
@@ -122,10 +144,6 @@ def export_run_csv(
|
||||
"maps_to": case.get("maps_to") or "",
|
||||
}
|
||||
)
|
||||
if refresh_cases:
|
||||
from generate_cases_csv import export_cases_csv
|
||||
|
||||
export_cases_csv(root)
|
||||
return dest
|
||||
|
||||
|
||||
@@ -137,7 +155,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
parser.add_argument(
|
||||
"--no-refresh-cases",
|
||||
action="store_true",
|
||||
help="不要顺便刷新 cases.csv 的 last_* 列",
|
||||
help="兼容旧命令;cases.csv 已不会被运行结果刷新",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
root = resolve_root(args.root)
|
||||
@@ -147,7 +165,6 @@ def main(argv: list[str] | None = None) -> int:
|
||||
root,
|
||||
run_path=run_path,
|
||||
out=out,
|
||||
refresh_cases=not args.no_refresh_cases,
|
||||
)
|
||||
counts: Counter[str] = Counter()
|
||||
with path.open(encoding="utf-8-sig", newline="") as handle:
|
||||
|
||||
Reference in New Issue
Block a user