Files
AirPlan-V2/lib/air_runtime/test_runtime.py
AirPlan 6130478c96 feat: AirPlan V2 — 全专家插件强制路由 + 事件系统规范化
P0-8 扩大: do_mode.py finish_worker 全专家插件强制路由
- GUI→XDB, network→NDB, C/C++→SDB, done→Rvr, blocked/failed→Dbg
- 证据去重: 已有 xdbSessions/ndbSessions/sdbReports/rvrReviewed 则跳过

P1-GAP17: 事件 emit 规范化
- 新增 7 个事件常量 (TASK_ENTERED, TASK_FINISHED, ENGINE_ENTERED 等)
- 全部 emit 调用替换字符串字面量为常量,零残留
- 30 个事件类型常量全部定义且唯一

P1-GAP18: 事件日志原子轮转
- emit 计数器每 128 次检查轮转,避免每次 emit 读文件
- 清除未使用的 _emit_with_completion/_pending_merge_complete
- 原子轮转: tempfile+os.replace 保证不损坏

eng 极端接管: 强制调用全部专家插件 (Dbg/XDB/NDB/SDB/Rvr)
commands/do.md: 更新为全专家插件路由文档

全量测试: 69 通过, 0 失败

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 15:56:44 +08:00

184 lines
6.6 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
AirTst 测试运行器运行时 — V2 新增组件。
统一测试执行接口,支持多框架,产出结构化结果。
"""
from __future__ import annotations
import json
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from air_runtime.io import atomic_json_write
from air_runtime.paths import tst_state_path
@dataclass
class TestCase:
name: str
status: str # passed | failed | skipped
duration: str = ""
message: str = ""
@dataclass
class TestSuite:
name: str
total: int = 0
passed: int = 0
failed: int = 0
skipped: int = 0
cases: list[TestCase] = field(default_factory=list)
@dataclass
class TestRunResult:
framework: str
total: int = 0
passed: int = 0
failed: int = 0
disabled: int = 0
duration: str = ""
suites: list[TestSuite] = field(default_factory=list)
failures: list[dict] = field(default_factory=list)
class TestRunner:
"""统一测试执行器。"""
FRAMEWORKS = {
"pytest": ["python", "-m", "pytest", "--json-report", "-q"],
"googletest": ["ctest", "--output-on-failure"],
"jest": ["npx", "jest", "--json"],
"vitest": ["npx", "vitest", "run", "--reporter=json"],
"go": ["go", "test", "-json", "./..."],
"cargo": ["cargo", "test", "--", "--format=json"],
}
def run(self, task_id: str, project_root: Path, framework: str,
target_path: Path | None = None,
extra_args: list[str] | None = None) -> TestRunResult:
if framework not in self.FRAMEWORKS:
return TestRunResult(framework=framework, failures=[{"error": f"unsupported framework: {framework}"}])
cmd = list(self.FRAMEWORKS[framework])
if target_path:
cmd.append(str(target_path))
if extra_args:
cmd.extend(extra_args)
try:
result = subprocess.run(cmd, capture_output=True, text=True,
timeout=600, cwd=str(project_root))
except subprocess.TimeoutExpired:
return TestRunResult(framework=framework, failures=[{"error": "timeout"}])
run_result = self._parse_result(framework, result.stdout)
self._save_report(task_id, project_root, run_result)
return run_result
def _parse_result(self, framework: str, stdout: str) -> TestRunResult:
if framework == "pytest":
return self._parse_pytest(stdout)
if framework in ("jest", "vitest"):
return self._parse_jest(stdout)
if framework == "googletest":
return self._parse_googletest(stdout)
if framework == "go":
return self._parse_go(stdout)
if framework == "cargo":
return self._parse_cargo(stdout)
return TestRunResult(framework=framework, total=0)
def _parse_pytest(self, stdout: str) -> TestRunResult:
try:
data = json.loads(stdout)
except json.JSONDecodeError:
return TestRunResult(framework="pytest", failures=[{"error": "json parse failed"}])
return TestRunResult(
framework="pytest",
total=data.get("summary", {}).get("total", 0),
passed=data.get("summary", {}).get("passed", 0),
failed=data.get("summary", {}).get("failed", 0),
duration=str(data.get("duration", "")),
)
def _parse_jest(self, stdout: str) -> TestRunResult:
try:
data = json.loads(stdout)
except json.JSONDecodeError:
return TestRunResult(framework="jest", failures=[{"error": "json parse failed"}])
return TestRunResult(
framework="jest",
total=data.get("numTotalTests", 0),
passed=data.get("numPassedTests", 0),
failed=data.get("numFailedTests", 0),
)
def _parse_googletest(self, stdout: str) -> TestRunResult:
"""解析 ctest 输出。ctest 不输出 JSON从文本提取统计。"""
import re
total = passed = failed = disabled = 0
for line in stdout.splitlines():
m = re.match(r"(\d+)% tests passed, (\d+) tests failed out of (\d+)", line)
if m:
failed = int(m.group(2))
total = int(m.group(3))
passed = total - failed
# GoogleTest 也支持 --gtest_output=json
try:
data = json.loads(stdout)
if isinstance(data, dict):
total = sum(s.get("tests", 0) for s in data.get("testsuites", []))
failed = sum(s.get("failures", 0) for s in data.get("testsuites", []))
passed = total - failed
disabled = sum(s.get("disabled", 0) for s in data.get("testsuites", []))
except json.JSONDecodeError:
pass
return TestRunResult(
framework="googletest", total=total, passed=passed, failed=failed, disabled=disabled,
)
def _parse_go(self, stdout: str) -> TestRunResult:
"""解析 go test -json 输出JSONL 格式,每行一个事件)。"""
total = passed = failed = 0
for line in stdout.splitlines():
try:
ev = json.loads(line)
except json.JSONDecodeError:
continue
action = ev.get("Action", "")
if action == "pass":
passed += 1
total += 1
elif action == "fail":
failed += 1
total += 1
elif action == "skip":
total += 1
return TestRunResult(framework="go", total=total, passed=passed, failed=failed)
def _parse_cargo(self, stdout: str) -> TestRunResult:
"""解析 cargo test --format=json 输出JSONL 格式)。"""
total = passed = failed = 0
for line in stdout.splitlines():
try:
ev = json.loads(line)
except json.JSONDecodeError:
continue
if ev.get("type") == "test":
total += 1
if ev.get("event") == "ok":
passed += 1
elif ev.get("event") == "failed":
failed += 1
return TestRunResult(framework="cargo", total=total, passed=passed, failed=failed)
def _save_report(self, task_id: str, project_root: Path, result: TestRunResult) -> None:
report_dir = tst_state_path(project_root).parent / "reports"
report_dir.mkdir(parents=True, exist_ok=True)
from air_runtime.utils import session_stamp
report_path = report_dir / f"{task_id}-{session_stamp()}.json"
atomic_json_write(report_path, result.__dict__)