89 lines
3.7 KiB
Python
89 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
||
"""运行 topics/**/examples/ 下的示例代码(抽查“代码能跑”)。
|
||
|
||
- Go:含 go.mod 的目录执行 `go vet ./... && go test ./...`
|
||
- Python:*.py 直接运行(文件开头可写 `# requires: 包1, 包2`,缺包时跳过);test_*.py 用 pytest 运行
|
||
- TypeScript:*.ts 用 Node.js 直接运行(需要 Node 23.6+ 的类型剥离,推荐 Node 24 LTS)
|
||
- 文件开头写 `# 不自动运行` / `// 不自动运行` 的跳过(需要外部服务的示例)
|
||
|
||
用法:python3 tools/run_examples.py [路径过滤关键词]
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import importlib.util
|
||
import os
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
|
||
|
||
def run(cmd: list[str] | str, cwd: Path, timeout: int = 180) -> tuple[bool, str]:
|
||
try:
|
||
p = subprocess.run(cmd, cwd=cwd, shell=isinstance(cmd, str), capture_output=True, text=True, timeout=timeout,
|
||
env={**os.environ, "PYTHONPATH": str(cwd)})
|
||
return p.returncode == 0, (p.stdout + p.stderr)[-1500:]
|
||
except subprocess.TimeoutExpired:
|
||
return False, "超时"
|
||
|
||
|
||
def node_ok() -> bool:
|
||
if not shutil.which("node"):
|
||
return False
|
||
v = subprocess.run(["node", "-v"], capture_output=True, text=True).stdout.strip().lstrip("v").split(".")
|
||
return (int(v[0]), int(v[1])) >= (23, 6)
|
||
|
||
|
||
def main() -> int:
|
||
flt = sys.argv[1] if len(sys.argv) > 1 else ""
|
||
results: list[tuple[str, str, str]] = []
|
||
has_node = node_ok()
|
||
for ex in sorted(ROOT.glob("topics/**/examples/*")):
|
||
rel = ex.relative_to(ROOT).as_posix()
|
||
if flt and flt not in rel:
|
||
continue
|
||
if ex.is_dir() and (ex / "go.mod").exists():
|
||
if not shutil.which("go"):
|
||
results.append(("SKIP", rel, "没有安装 Go"))
|
||
continue
|
||
ok, out = run("go vet ./... && go test ./...", ex)
|
||
results.append(("PASS" if ok else "FAIL", rel, "" if ok else out))
|
||
continue
|
||
targets = [ex] if ex.is_file() else sorted(ex.rglob("*"))
|
||
for f in targets:
|
||
frel = f.relative_to(ROOT).as_posix()
|
||
if f.suffix not in (".py", ".ts") or "node_modules" in f.parts:
|
||
continue
|
||
head = f.read_text(encoding="utf-8")[:400]
|
||
if "不自动运行" in head:
|
||
results.append(("SKIP", frel, "标记为不自动运行"))
|
||
continue
|
||
if f.suffix == ".py":
|
||
m = re.search(r"^# requires:\s*(.+)$", head, re.M)
|
||
missing = [p.strip() for p in m.group(1).split(",")] if m else []
|
||
missing = [p for p in missing if importlib.util.find_spec(p) is None]
|
||
if missing:
|
||
results.append(("SKIP", frel, f"缺少依赖 {missing}"))
|
||
continue
|
||
cmd = [sys.executable, "-m", "pytest", "-q", f.name] if f.name.startswith("test_") \
|
||
else [sys.executable, f.name]
|
||
ok, out = run(cmd, f.parent)
|
||
else:
|
||
if not has_node:
|
||
results.append(("SKIP", frel, "需要 Node 23.6+"))
|
||
continue
|
||
ok, out = run(["node", f.name], f.parent)
|
||
results.append(("PASS" if ok else "FAIL", frel, "" if ok else out))
|
||
for status, rel, out in results:
|
||
print(f"{status:4} {rel}" + (f"\n{out}" if out and status == "FAIL" else f" ({out})" if out else ""))
|
||
n = {s: sum(r[0] == s for r in results) for s in ("PASS", "FAIL", "SKIP")}
|
||
print(f"\n通过 {n['PASS']},失败 {n['FAIL']},跳过 {n['SKIP']}")
|
||
return 1 if n["FAIL"] else 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|