From 2ed36c001398698734b82ac2bdf0f3f0d378530a Mon Sep 17 00:00:00 2001 From: Peter Zhang <18501667167@qq.com> Date: Sun, 31 May 2026 17:01:23 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E5=AE=9E=E7=8E=B0=E7=AB=AF=E5=88=B0?= =?UTF-8?q?=E7=AB=AF=E9=AA=8C=E6=94=B6=E6=B5=8B=E8=AF=95=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=20(run=5Fpipeline.py=20+=20acceptance.yml)=20-=20Closes=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/run_pipeline.py: 完整管道运行器 (docx → IR → acceptance tests) - acceptance.yml: 更新为 workflow_dispatch,支持 --input/--parsed/--test 三种模式 - 失败时自动创建 acceptance-failure issue --- .gitea/workflows/acceptance.yml | 60 ++++++----- scripts/run_pipeline.py | 178 ++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 scripts/run_pipeline.py diff --git a/.gitea/workflows/acceptance.yml b/.gitea/workflows/acceptance.yml index 15da188..fc12d2b 100644 --- a/.gitea/workflows/acceptance.yml +++ b/.gitea/workflows/acceptance.yml @@ -3,23 +3,23 @@ name: QE Acceptance Tests on: workflow_dispatch: inputs: + prd_path: + description: 'Path to .docx PRD file (absolute)' + required: false + default: '' + parsed_path: + description: 'Path to pre-parsed _updated.json (skip doc_parser if set)' + required: false + default: '' acceptance_runs: - description: 'Layer B stability runs (1 = skip stability testing)' + description: 'Layer B stability runs (1 = skip)' required: false default: '1' - ir_path: - description: 'Path to IR JSON file (relative to workspace)' - required: false - default: 'output/ir_final.json' - parsed_path: - description: 'Path to _parsed.json or _updated.json (relative to workspace)' - required: false - default: 'output/车机娱乐系统禁止功能文档_精简_updated.json' jobs: acceptance: runs-on: shell - timeout-minutes: 30 + timeout-minutes: 60 steps: - name: Checkout main branch run: | @@ -29,26 +29,34 @@ jobs: - name: Install dependencies run: pip install -r requirements.txt - - name: Run QE Acceptance Tests - run: >- - python -m pytest tests/acceptance/ -v - --run-acceptance - --acceptance-runs=${{ github.event.inputs.acceptance_runs }} - --ir-path=${{ github.event.inputs.ir_path }} - --parsed-path=${{ github.event.inputs.parsed_path }} - --tb=long + - name: Run pipeline + acceptance tests + run: | + if [ -n "${{ github.event.inputs.prd_path }}" ]; then + python scripts/run_pipeline.py --input "${{ github.event.inputs.prd_path }}" --test + elif [ -n "${{ github.event.inputs.parsed_path }}" ]; then + python scripts/run_pipeline.py --parsed "${{ github.event.inputs.parsed_path }}" --test + else + # No input provided — run acceptance on existing output if present + python -m pytest tests/acceptance/ -v --run-acceptance \ + --acceptance-runs=${{ github.event.inputs.acceptance_runs }} --tb=short + fi env: DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} - name: Create issue on failure if: failure() env: GITEA_API_TOKEN: ${{ secrets.GITEA_TOKEN }} - run: >- - python scripts/create_failure_issue.py - --sha "${{ github.sha }}" - --branch "main" - --run "${{ github.run_number }}" - --message "QE Acceptance Tests Failed" - --workflow "QE Acceptance" - --labels "acceptance-failure,agent-task" + run: | + # Read acceptance report summary if it exists + if [ -f acceptance-report.json ]; then + SUMMARY=$(python -c "import json; r=json.load(open('acceptance-report.json')); print(r.get('final_verdict','?'))") + DETAILS=$(python -c "import json; r=json.load(open('acceptance-report.json')); fd=r.get('failure_details',[]); print('\\n'.join(f'- {d}' for d in fd) if fd else '')") + fi + python scripts/create_failure_issue.py \ + --sha "${{ github.sha }}" --branch "main" \ + --run "${{ github.run_number }}" \ + --message "QE Acceptance: ${SUMMARY:-pipeline failed}" \ + --workflow "QE Acceptance" \ + --labels "acceptance-failure,agent-task" diff --git a/scripts/run_pipeline.py b/scripts/run_pipeline.py new file mode 100644 index 0000000..8886477 --- /dev/null +++ b/scripts/run_pipeline.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""End-to-end pipeline runner for QE acceptance testing. + +Runs the complete document_analyzer pipeline: + 1. doc_parser (docx → _parsed.json, if .docx provided) + 2. ir_generation steps (parsed JSON → ir_final.json + audit report) + 3. QE acceptance tests (optional, if --test flag) + +Usage: + python scripts/run_pipeline.py --input # full pipeline + python scripts/run_pipeline.py --parsed <_updated.json> # skip doc_parser + python scripts/run_pipeline.py --parsed <_updated.json> --test # pipeline + acceptance tests + +Outputs are placed in output/ matching the project config.py structure: + output/final/ir_final.json + output/final/ir_audit_report.md + acceptance-report.json (if --test) +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +import json +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(PROJECT_ROOT / "skills" / "ir_generation_skill")) +sys.path.insert(0, str(PROJECT_ROOT / "skills" / "doc_parser_skill" / "scripts")) + +import config + + +# ── Stage 1: Document Parsing ──────────────────────────────────────────────── + + +def run_doc_parser(docx_path: str, output_dir: str) -> str | None: + """Run doc_parser on a .docx file. Returns path to _parsed.json or None.""" + from doc_parser import parse_document + + print(f"[1/3] Parsing document: {docx_path}") + result = parse_document(docx_path, output_dir, dry_run=False) + parsed_path = result.get("output") + if parsed_path and os.path.isfile(parsed_path): + print(f" → {parsed_path}") + return parsed_path + print(" ✗ doc_parser failed to produce output", file=sys.stderr) + return None + + +# ── Stage 2: IR Generation ─────────────────────────────────────────────────── + + +def run_ir_pipeline(parsed_path: str) -> str | None: + """Run the ir_generation steps. Returns path to ir_final.json or None.""" + config.set_input_file(parsed_path) + os.makedirs(config.PROJECT_OUTPUT, exist_ok=True) + os.makedirs(config.IR_OUTPUT, exist_ok=True) + os.makedirs(config.FINAL_OUTPUT, exist_ok=True) + + steps = [ + ("step1_semantic_index.py", "Semantic Index"), + ("step2_ir_extraction.py", "IR Extraction"), + ("step2_5_branch_coverage.py", "Branch Coverage"), + ("step3_merge_and_audit.py", "Merge & Audit"), + ] + + print(f"[2/3] Generating IR from: {parsed_path}") + + for script, label in steps: + script_path = PROJECT_ROOT / "skills" / "ir_generation_skill" / script + if not script_path.exists(): + print(f" ✗ Missing: {script}", file=sys.stderr) + continue + + print(f" Running {script} ({label})...") + result = subprocess.run( + [sys.executable, str(script_path)], + cwd=str(PROJECT_ROOT), + capture_output=True, text=True, + ) + if result.returncode != 0: + print(f" ✗ {script} failed (exit {result.returncode})", file=sys.stderr) + print(result.stderr[-500:], file=sys.stderr) + else: + # Print last line of stdout for brief progress + lines = result.stdout.strip().split("\n") + last = lines[-1] if lines else "done" + print(f" ✓ {label}: {last[:120]}") + + if os.path.isfile(config.IR_FINAL_JSON): + print(f" → {config.IR_FINAL_JSON}") + return config.IR_FINAL_JSON + + print(" ✗ IR generation did not produce ir_final.json", file=sys.stderr) + return None + + +# ── Stage 3: Acceptance Tests ──────────────────────────────────────────────── + + +def run_acceptance_tests() -> int: + """Run QE acceptance tests. Returns pytest exit code.""" + print("[3/3] Running QE acceptance tests...") + + test_dir = PROJECT_ROOT / "tests" / "acceptance" + result = subprocess.run( + [ + sys.executable, "-m", "pytest", str(test_dir), + "-v", "--run-acceptance", + "--ir-path", config.IR_FINAL_JSON, + "--parsed-path", config.INPUT_JSON, + "--tb=short", + ], + cwd=str(PROJECT_ROOT), + ) + return result.returncode + + +# ── Main ───────────────────────────────────────────────────────────────────── + + +def main(): + parser = argparse.ArgumentParser(description="Run the full document_analyzer pipeline") + parser.add_argument("--input", help="Path to .docx PRD file") + parser.add_argument("--parsed", help="Path to pre-parsed _updated.json (skip doc_parser)") + parser.add_argument("--test", action="store_true", help="Run acceptance tests after pipeline") + parser.add_argument("--output-dir", default=None, help="Output directory (default: output/)") + args = parser.parse_args() + + parsed_path = args.parsed + + # Stage 1: doc_parser + if args.input: + docx = args.input + if not os.path.isfile(docx): + print(f"Error: Input file not found: {docx}", file=sys.stderr) + sys.exit(1) + out_dir = args.output_dir or str(PROJECT_ROOT / "output") + parsed_path = run_doc_parser(docx, out_dir) + if not parsed_path: + print("\n✗ Pipeline blocked at Stage 1 (doc_parser)", file=sys.stderr) + # Create tracking issue for dev-agent + _maybe_create_blocking_issue("doc_parser", f"Input: {docx}") + sys.exit(1) + + if not parsed_path: + print("Error: Either --input or --parsed is required", file=sys.stderr) + sys.exit(1) + + if not os.path.isfile(parsed_path): + print(f"Error: Parsed JSON not found: {parsed_path}", file=sys.stderr) + sys.exit(1) + + # Stage 2: IR generation + ir_path = run_ir_pipeline(parsed_path) + if not ir_path: + print("\n✗ Pipeline blocked at Stage 2 (ir_generation)", file=sys.stderr) + _maybe_create_blocking_issue("ir_generation", f"Parsed: {parsed_path}") + sys.exit(1) + + print(f"\n✓ Pipeline complete: {ir_path}") + + # Stage 3: Acceptance tests + if args.test: + exit_code = run_acceptance_tests() + sys.exit(exit_code) + + +def _maybe_create_blocking_issue(stage: str, detail: str): + """Notify about a pipeline blockage. The acceptance CI will create the issue.""" + print(f"\n⚠ Stage '{stage}' failed. CI will create an acceptance-failure issue.", file=sys.stderr) + + +if __name__ == "__main__": + main()