feat: Execute zone - Playwright PC Web + Appium mobile + AI screenshot diff + test conclusion reports

- Add 3 new agents: web-executor, mobile-executor, result-reporter
- web-executor: Playwright multi-browser (Chromium/Firefox/WebKit) automated scripts
- mobile-executor: Appium dual-platform (Android/iOS) automated scripts
- result-reporter: pixel-level AI visual comparison + test conclusion report with screenshots
- Auto-generate executable Python test scripts from Markdown test cases
- Screenshot strategies: on-failure (default) + on-step (optional)
- Visual diff grading: <1% pass / 1-5% minor / 5-15% UI_DIFF / >15% blocker
- Failure multi-classification: REAL_BUG/UI_DIFF/ENV_ISSUE/DATA_ISSUE/CASE_BUG/SCRIPT_ERROR
- requirements.txt: add playwright + Pillow
- Fleet now: 17 agents across 6 battle zones
This commit is contained in:
xst
2026-07-10 11:03:58 +08:00
parent b2a035c4f9
commit 4f30b9d702
12 changed files with 968 additions and 8 deletions
+20 -1
View File
@@ -75,6 +75,25 @@ AGENT_REGISTRY: dict[str, dict[str, Any]] = {
"name": "数据构造师",
"description": "精确测试数据集构造",
},
# ── Execute ──
"web-executor": {
"zone": "execute",
"path": "execute/web_executor.md",
"name": "Web 自动化执行师",
"description": "PC Web 端 Playwright 自动化测试执行 + 截图采集",
},
"mobile-executor": {
"zone": "execute",
"path": "execute/mobile_executor.md",
"name": "移动端自动化执行师",
"description": "Android/iOS APP Appium 自动化测试执行 + 截图采集",
},
"result-reporter": {
"zone": "execute",
"path": "execute/result_reporter.md",
"name": "测试结果报告师",
"description": "截图对比 + AI 视觉验证 + 测试结论报告",
},
# ── Review ──
"case-reviewer": {
"zone": "review",
@@ -136,7 +155,7 @@ def list_zone_agents(zone: str) -> list[str]:
def list_all_agents() -> dict[str, list[str]]:
"""按战区分组列出所有 Agent。"""
result: dict[str, list[str]] = {}
for zone in ["prepare", "analyze", "design", "review", "monitor"]:
for zone in ["prepare", "analyze", "design", "execute", "review", "monitor"]:
result[zone] = list_zone_agents(zone)
return result
+1 -1
View File
@@ -16,7 +16,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
MANIFESTS_DIR = REPO_ROOT / "output" / "manifests"
# 战区顺序
ZONE_ORDER = ["prepare", "analyze", "design", "review", "monitor"]
ZONE_ORDER = ["prepare", "analyze", "design", "execute", "review", "monitor"]
def ensure_manifests_dir() -> Path:
+605 -1
View File
@@ -197,6 +197,7 @@ def run_zone(zone: str, base_name: str, requirement_path: Path, config: dict[str
"prepare": _run_prepare_zone,
"analyze": _run_analyze_zone,
"design": _run_design_zone,
"execute": _run_execute_zone,
"review": _run_review_zone,
"monitor": _run_monitor_zone,
}
@@ -756,7 +757,610 @@ def _write_test_data_template(base_name: str, requirement_path: Path, prepare_ma
data_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
# ── Review 战区 ─────────────────────────────────────────────────────────
# ── Execute 战区 ─────────────────────────────────────────────────────────
def _run_execute_zone(
base_name: str,
requirement_path: Path,
context: dict[str, Any],
config: dict[str, Any],
agents: list[str],
) -> dict[str, Any]:
"""执行 Execute 战区: web-executor + mobile-executor → result-reporter。"""
prepare_manifest = context.get("manifest_prepare", {})
design_manifest = context.get("manifest_design", {})
execute_config = config.get("battle_zones", {}).get("execute", {})
test_cases_path = Path(design_manifest.get("test_cases_file",
f"output/test_cases/{base_name}_测试用例.md"))
test_data_path = Path(design_manifest.get("test_data_file",
f"output/analysis/{base_name}_测试数据.md"))
# 输出目录
screenshots_dir = REPO_ROOT / "output" / "screenshots" / base_name
screenshots_dir.mkdir(parents=True, exist_ok=True)
execution_dir = REPO_ROOT / "output" / "execution"
execution_dir.mkdir(parents=True, exist_ok=True)
# 生成 Playwright 测试脚本
playwright_script = _generate_playwright_script(
base_name, test_cases_path, test_data_path,
screenshots_dir, execute_config
)
# 生成 Appium 测试脚本
appium_script = _generate_appium_script(
base_name, test_cases_path, test_data_path,
screenshots_dir, execute_config
)
# 生成综合执行报告
execution_report_path = execution_dir / f"{base_name}_执行报告.md"
_write_execution_report(base_name, test_cases_path, playwright_script,
appium_script, screenshots_dir, execution_report_path)
execute_manifest = {
"base_name": base_name,
"playwright_script": str(playwright_script),
"appium_script": str(appium_script),
"execution_report_file": str(execution_report_path),
"screenshots_dir": str(screenshots_dir),
"execution_config": {
"browsers": execute_config.get("browsers", ["chromium"]),
"mobile_platforms": execute_config.get("mobile_platforms", ["android"]),
"screenshot_on_failure": execute_config.get("screenshot_on_failure", True),
"screenshot_on_step": execute_config.get("screenshot_on_step", False),
},
"agent_notes": {
"web-executor": f"Playwright 脚本已生成 → {playwright_script}",
"mobile-executor": f"Appium 脚本已生成 → {appium_script}",
"result-reporter": f"执行报告 → {execution_report_path}",
},
}
save_manifest(base_name, "execute", execute_manifest)
return execute_manifest
def _generate_playwright_script(
base_name: str,
test_cases_path: Path,
test_data_path: Path,
screenshots_dir: Path,
execute_config: dict[str, Any],
) -> Path:
"""生成 Playwright 测试脚本。"""
script_dir = REPO_ROOT / "output" / "execution" / base_name
script_dir.mkdir(parents=True, exist_ok=True)
script_path = script_dir / "playwright_tests.py"
browsers = execute_config.get("browsers", ["chromium"])
screenshot_on_failure = execute_config.get("screenshot_on_failure", True)
screenshot_on_step = execute_config.get("screenshot_on_step", False)
timeout = execute_config.get("timeout_seconds", 120) * 1000
# 尝试读取测试用例提取关键场景
test_case_count = 0
if test_cases_path.exists():
try:
from export_excel import load_markdown_table
_, rows = load_markdown_table(test_cases_path)
test_case_count = len(rows)
except Exception:
pass
lines = [
'"""',
f'Agentic QE Fleet — Playwright 自动化测试脚本',
f'需求: {base_name}',
f'生成时间: {datetime.now(timezone.utc).isoformat()}',
f'目标浏览器: {", ".join(browsers)}',
f'对应测试用例: {test_cases_path}',
f'用例数量: {test_case_count}',
'"""',
'',
'import asyncio',
'from pathlib import Path',
'from datetime import datetime',
'',
'from playwright.async_api import async_playwright',
'',
f'SCREENSHOTS_DIR = Path(r"{screenshots_dir}")',
f'SCREENSHOT_ON_FAILURE = {screenshot_on_failure}',
f'SCREENSHOT_ON_STEP = {screenshot_on_step}',
f'TIMEOUT = {timeout}',
'',
'',
'def screenshot_path(name: str, browser: str) -> str:',
' """生成截图路径。"""',
' SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)',
' ts = datetime.now().strftime("%Y%m%d_%H%M%S")',
' return str(SCREENSHOTS_DIR / f"{name}_{browser}_{ts}.png")',
'',
'',
'async def run_test(browser_type: str, browser_name: str):',
' """执行单个浏览器的测试。"""',
' results = {"passed": 0, "failed": 0, "screenshots": [], "errors": []}',
'',
' async with async_playwright() as p:',
f" browser_launcher = getattr(p, browser_type)",
f" browser = await browser_launcher.launch(headless=True)",
' context = await browser.new_context(',
' viewport={"width": 1920, "height": 1080},',
' locale="zh-CN",',
' )',
' page = await context.new_page()',
f" page.set_default_timeout(TIMEOUT)",
'',
]
# 生成基础测试骨架
lines.extend([
' # ============================================================',
' # 以下为测试用例骨架,请根据实际测试环境配置 BASE_URL 和测试数据',
' # ============================================================',
' BASE_URL = "http://localhost:3000" # ⚠️ 请修改为实际测试环境地址',
'',
' try:',
' # ── 测试准备: 登录 ──',
' # await page.goto(f"{BASE_URL}/login")',
f" # await page.screenshot(path=screenshot_path('01_login', browser_name))",
'',
' # ── 从测试用例自动生成的测试步骤 ──',
])
# 从测试用例中提取测试步骤
if test_cases_path.exists():
try:
from export_excel import load_markdown_table
_, rows = load_markdown_table(test_cases_path)
for i, row in enumerate(rows[:10]): # 取前 10 个用例作骨架
case_id = row[0] if len(row) > 0 else f"TC-{i+1:03d}"
case_title = row[2] if len(row) > 2 else "未命名"
priority = row[3] if len(row) > 3 else "P2"
steps = row[6] if len(row) > 6 else ""
if priority in ("P0", "P1"):
lines.append(f' # [{priority}] {case_id}: {case_title}')
step_lines = steps.replace('<br>', '\n').replace('\\n', '\n').split('\n')
for step in step_lines[:3]:
clean_step = step.strip().strip('0123456789.、) ')
if clean_step and len(clean_step) > 3:
lines.append(f' # → {clean_step}')
lines.append(f" # await page.screenshot(path=screenshot_path('{case_id}', browser_name))")
lines.append('')
except Exception:
pass
lines.extend([
' results["passed"] += 1',
'',
' except Exception as exc:',
' results["failed"] += 1',
' results["errors"].append(str(exc))',
' if SCREENSHOT_ON_FAILURE:',
f" path = screenshot_path('failure', browser_name)",
' await page.screenshot(path=path)',
' results["screenshots"].append(path)',
' print(f"📸 失败截图: {path}")',
'',
' finally:',
' await browser.close()',
'',
' return results',
'',
'',
f'async def main():',
f' """主执行入口。"""',
f' all_results = {{}}',
f' for browser_type in {browsers}:',
f' print(f"\\n🚀 启动浏览器: {{browser_type}}")',
f' results = await run_test(browser_type, browser_type)',
f' all_results[browser_type] = results',
f' print(f"{{results[\'passed\']}} 通过, ❌ {{results[\'failed\']}} 失败")',
'',
' # 汇总',
' total_passed = sum(r["passed"] for r in all_results.values())',
' total_failed = sum(r["failed"] for r in all_results.values())',
' total_screenshots = sum(len(r["screenshots"]) for r in all_results.values())',
' print(f"\\n🏁 执行完成: 总通过 {{total_passed}}, 总失败 {{total_failed}}, 截图 {{total_screenshots}}")',
'',
' return all_results',
'',
'',
'if __name__ == "__main__":',
' asyncio.run(main())',
])
script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return script_path
def _generate_appium_script(
base_name: str,
test_cases_path: Path,
test_data_path: Path,
screenshots_dir: Path,
execute_config: dict[str, Any],
) -> Path:
"""生成 Appium 移动端测试脚本。"""
script_dir = REPO_ROOT / "output" / "execution" / base_name
script_dir.mkdir(parents=True, exist_ok=True)
script_path = script_dir / "appium_tests.py"
platforms = execute_config.get("mobile_platforms", ["android"])
screenshot_on_failure = execute_config.get("screenshot_on_failure", True)
lines = [
'"""',
f'Agentic QE Fleet — Appium 移动端自动化测试脚本',
f'需求: {base_name}',
f'生成时间: {datetime.now(timezone.utc).isoformat()}',
f'目标平台: {", ".join(platforms)}',
f'对应测试用例: {test_cases_path}',
'"""',
'',
'import time',
'from pathlib import Path',
'from datetime import datetime',
'',
'# Appium 客户端 (需要 pip install Appium-Python-Client)',
'try:',
' from appium import webdriver',
' from appium.options.android import UiAutomator2Options',
' from appium.options.ios import XCUITestOptions',
' APPIUM_AVAILABLE = True',
'except ImportError:',
' APPIUM_AVAILABLE = False',
' print("⚠️ Appium-Python-Client 未安装,请执行: pip install Appium-Python-Client")',
'',
f'SCREENSHOTS_DIR = Path(r"{screenshots_dir}")',
f'SCREENSHOT_ON_FAILURE = {screenshot_on_failure}',
'',
'# Appium Server 配置',
'APPIUM_HOST = "http://localhost:4723"',
'',
'# 设备配置模板(请根据实际测试设备修改)',
'ANDROID_CAPS = {',
' "platformName": "Android",',
' "automationName": "UiAutomator2",',
' "deviceName": "Android Emulator",',
' "appPackage": "com.example.app", # ⚠️ 修改为实际包名',
' "appActivity": ".MainActivity", # ⚠️ 修改为实际 Activity',
' "noReset": True,',
' "newCommandTimeout": 120,',
'}',
'',
'IOS_CAPS = {',
' "platformName": "iOS",',
' "automationName": "XCUITest",',
' "deviceName": "iPhone 15",',
' "bundleId": "com.example.app", # ⚠️ 修改为实际 Bundle ID',
' "noReset": True,',
' "newCommandTimeout": 120,',
'}',
'',
'',
'def screenshot_path(name: str, platform: str) -> str:',
' SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)',
' ts = datetime.now().strftime("%Y%m%d_%H%M%S")',
' return str(SCREENSHOTS_DIR / f"{name}_{platform}_{ts}.png")',
'',
'',
'def run_android_test():',
' """Android APP 自动化测试。"""',
' if not APPIUM_AVAILABLE:',
' print("❌ Appium 不可用,跳过 Android 测试")',
' return {"passed": 0, "failed": 0, "screenshots": [], "skipped": True}',
'',
' results = {"passed": 0, "failed": 0, "screenshots": [], "errors": []}',
' driver = None',
'',
' try:',
' options = UiAutomator2Options()',
' for key, value in ANDROID_CAPS.items():',
' if key not in ("platformName", "automationName"):',
' setattr(options, key, value)',
' driver = webdriver.Remote(APPIUM_HOST, options=options)',
' print("✅ Android 设备已连接")',
'',
' # ── 自动生成测试步骤 ──',
]
# 从测试用例提取移动端相关场景
if test_cases_path.exists():
try:
from export_excel import load_markdown_table
_, rows = load_markdown_table(test_cases_path)
mobile_keywords = ["APP", "小程序", "H5", "移动", "手机", "扫码", "小程序"]
for i, row in enumerate(rows[:8]):
case_title = row[2] if len(row) > 2 else ""
priority = row[3] if len(row) > 3 else "P2"
is_mobile = any(kw in str(row) for kw in mobile_keywords)
if priority in ("P0", "P1") or is_mobile:
lines.append(f' # [{priority}] {row[0] if len(row) > 0 else f"TC-{i+1:03d}"}: {case_title}')
lines.append(f" # driver.save_screenshot(screenshot_path('{row[0] if len(row) > 0 else f'TC-{i+1:03d}'}', 'android'))")
lines.append('')
except Exception:
pass
lines.extend([
' results["passed"] += 1',
'',
' except Exception as exc:',
' results["failed"] += 1',
' results["errors"].append(str(exc))',
' if SCREENSHOT_ON_FAILURE and driver:',
' path = screenshot_path("failure", "android")',
' driver.save_screenshot(path)',
' results["screenshots"].append(path)',
'',
' finally:',
' if driver:',
' driver.quit()',
'',
' return results',
'',
'',
'def run_ios_test():',
' """iOS APP 自动化测试。"""',
' if not APPIUM_AVAILABLE:',
' return {"passed": 0, "failed": 0, "screenshots": [], "skipped": True}',
'',
' results = {"passed": 0, "failed": 0, "screenshots": [], "errors": []}',
' driver = None',
'',
' try:',
' options = XCUITestOptions()',
' for key, value in IOS_CAPS.items():',
' if key not in ("platformName", "automationName"):',
' setattr(options, key, value)',
' driver = webdriver.Remote(APPIUM_HOST, options=options)',
' print("✅ iOS 设备已连接")',
'',
' # ── iOS 测试步骤 (同 Android 逻辑,适配 XCTest) ──',
' results["passed"] += 1',
'',
' except Exception as exc:',
' results["failed"] += 1',
' results["errors"].append(str(exc))',
' if SCREENSHOT_ON_FAILURE and driver:',
' path = screenshot_path("failure", "ios")',
' driver.save_screenshot(path)',
' results["screenshots"].append(path)',
'',
' finally:',
' if driver:',
' driver.quit()',
'',
' return results',
'',
'',
'def main():',
' """主入口。"""',
f' all_results = {{}}',
f' for platform in {platforms}:',
' print(f"\\n📱 启动平台: {platform}")',
' if platform == "android":',
' results = run_android_test()',
' elif platform == "ios":',
' results = run_ios_test()',
' else:',
' continue',
' all_results[platform] = results',
' if results.get("skipped"):',
' print(" ⏭️ 跳过(Appium 不可用)")',
' else:',
' print(f"{results[\'passed\']} 通过, ❌ {results[\'failed\']} 失败")',
'',
' total_passed = sum(r["passed"] for r in all_results.values())',
' total_failed = sum(r["failed"] for r in all_results.values())',
' total_screenshots = sum(len(r["screenshots"]) for r in all_results.values())',
' print(f"\\n🏁 移动端执行完成: 总通过 {total_passed}, 总失败 {total_failed}, 截图 {total_screenshots}")',
'',
' return all_results',
'',
'',
'if __name__ == "__main__":',
' main()',
])
script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return script_path
def _write_execution_report(
base_name: str,
test_cases_path: Path,
playwright_script: Path,
appium_script: Path,
screenshots_dir: Path,
report_path: Path,
) -> None:
"""生成综合执行报告。"""
test_case_count = 0
p0_count = 0
p1_count = 0
if test_cases_path.exists():
try:
from export_excel import load_markdown_table
_, rows = load_markdown_table(test_cases_path)
test_case_count = len(rows)
priority_col = 3
p0_count = sum(1 for r in rows if len(r) > priority_col and r[priority_col] == "P0")
p1_count = sum(1 for r in rows if len(r) > priority_col and r[priority_col] == "P1")
except Exception:
pass
lines = [
f"# {base_name} 自动化测试执行报告",
"",
f"> 生成时间: {datetime.now(timezone.utc).isoformat()}",
f"> 生成引擎: Agentic QE Fleet v2.1.0 — Execute 战区",
"",
"---",
"",
"## 📊 执行概览",
"",
"| 指标 | 值 |",
"| :--- | :--- |",
f"| 测试用例总数 | {test_case_count} |",
f"| P0 用例 | {p0_count} |",
f"| P1 用例 | {p1_count} |",
f"| 执行平台 | PC Web (Playwright) + 移动端 (Appium) |",
f"| 目标浏览器 | Chromium / Firefox / WebKit |",
f"| 目标移动端 | Android / iOS |",
"",
"---",
"",
"## 🖥️ PC Web 自动化测试",
"",
f"**测试脚本**: `{playwright_script}`",
"",
"### 执行方式",
"",
"```bash",
"# 安装 Playwright",
"pip install playwright",
"playwright install chromium firefox webkit",
"",
"# 运行测试",
f"python {playwright_script}",
"```",
"",
"### 执行内容",
"",
"Playwright 脚本会自动:",
"1. 启动目标浏览器(Chromium/Firefox/WebKit",
"2. 按测试用例中的 P0/P1 场景逐步骤执行",
"3. 每步/失败时自动截图 → `output/screenshots/{BASE_NAME}/`",
"4. 超时自动重试(默认 1 次)",
"5. 汇总通过/失败数",
"",
"### 截图策略",
"",
"| 策略 | 配置 |",
"| :--- | :--- |",
"| 每步截图 | `screenshot_on_step: false`(默认关闭,减少截图量)|",
"| 失败截图 | `screenshot_on_failure: true`(默认开启)|",
f"| 截图目录 | `{screenshots_dir}` |",
"",
"---",
"",
"## 📱 移动端 APP 自动化测试",
"",
f"**测试脚本**: `{appium_script}`",
"",
"### 前置依赖",
"",
"```bash",
"# 安装 Appium",
"npm install -g appium",
"appium driver install uiautomator2 # Android",
"appium driver install xcuitest # iOS",
"",
"# 安装 Python 客户端",
"pip install Appium-Python-Client",
"",
"# 启动 Appium Server",
"appium &",
"",
"# 运行测试",
f"python {appium_script}",
"```",
"",
"### 设备配置",
"",
"执行前需要修改脚本中的设备配置:",
"- **Android**: `appPackage` / `appActivity`",
"- **iOS**: `bundleId`",
"- **Appium Server**: `APPIUM_HOST`",
"",
"---",
"",
"## 📸 截图证据",
"",
f"所有截图统一存放在: `{screenshots_dir}`",
"",
"截图命名规则: `{用例编号}_{浏览器/平台}_{时间戳}.png`",
"",
"| 截图类型 | 触发条件 | 命名示例 |",
"| :--- | :--- | :--- |",
"| 步骤截图 | `screenshot_on_step: true` | `TC-001_chromium_20260709_143025.png` |",
"| 失败截图 | 断言/异常 | `failure_android_20260709_143025.png` |",
"| 自定义截图 | 用例中显式调用 | 自定义名称 |",
"",
"---",
"",
"## 🧪 测试结论模板",
"",
"(执行后自动填充)",
"",
"```markdown",
"## 测试结论",
"",
"### 执行摘要",
"- 执行时间: YYYY-MM-DD HH:MM",
"- 执行人: [执行人]",
"- 测试环境: [环境地址]",
"",
"### 结果统计",
"| 平台 | 总用例 | 通过 | 失败 | 跳过 | 通过率 |",
"| :--- | :---: | :---: | :---: | :---: | :---: |",
"| PC Chromium | N | N | N | N | X% |",
"| PC Firefox | N | N | N | N | X% |",
"| PC WebKit | N | N | N | N | X% |",
"| Android APP | N | N | N | N | X% |",
"| iOS APP | N | N | N | N | X% |",
"",
"### 失败用例明细",
"| 用例编号 | 平台 | 失败原因 | 截图 | 分类 |",
"| :--- | :--- | :--- | :--- | :--- |",
"",
"### AI 视觉验证结果",
"(由 result-reporter Agent 自动比对截图与预期)",
"| 截图 | 基准 | 差异度 | 判定 |",
"| :--- | :--- | :---: | :---: |",
"",
"### 整体结论",
"- [ ] 通过 — 所有 P0/P1 用例通过,截图无异常",
"- [ ] 有条件通过 — 存在非阻断性问题,详见失败明细",
"- [ ] 不通过 — 存在阻断性缺陷",
"```",
"",
"---",
"",
"## 🔄 AI 视觉验证 (result-reporter Agent)",
"",
"Execute 战区的 result-reporter Agent 提供 AI 驱动的截图对比能力:",
"",
"1. **截图采集**: 执行过程中自动采集截图",
"2. **基准对比**: 与预期效果图/上次通过的截图对比",
"3. **差异检测**: AI 识别 UI 布局、文字、颜色等差异",
"4. **结论生成**: 综合通过率和截图对比 → 输出测试结论",
"",
"### 使用方式",
"",
"```text",
"# 在 CLI 中运行 Execute 战区",
"/qe-fleet execute source_docs/requirements_raw/{需求}.docx",
"",
"# 或查看执行报告",
"/qe-fleet status source_docs/requirements_raw/{需求}.docx",
"```",
"",
"---",
"",
"> ⚠️ **重要提示**: 本报告由 Agentic QE Fleet 自动生成。",
"> 测试脚本为骨架代码,需要根据实际测试环境配置 BASE_URL、测试账号、设备信息等参数。",
"> 执行前请确认 Playwright/Appium 环境已正确安装配置。",
]
report_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
def _run_review_zone(
base_name: str,