init the project

This commit is contained in:
evyzacq
2026-05-25 15:09:42 +08:00
commit 7fc0e7852e
122 changed files with 14557 additions and 0 deletions
@@ -0,0 +1,212 @@
# 测试用例生成器:基于 IR 生成测试用例集
import json
import logging
import uuid
from datetime import datetime, timezone
from server.config import settings
from server.core.llm_provider.router import router
from server.services.ir_engine.generator import ir_generator
logger = logging.getLogger("testflow")
DEFAULT_GEN_PROMPT = """你是一位资深的软件测试工程师。基于以下中间表示(IR),请生成完整的测试用例集。
测试设计原则:
1. 每个功能点至少覆盖一条正向用例和一条异常用例
2. 对于 P0 功能点,额外覆盖边界值测试
3. 用例步骤使用 Given-When-Then 结构
4. 预期结果必须明确可验证
输出格式:纯 JSON 数组,每个测试用例包含以下字段:
- id: 唯一标识符(字符串)
- module: 模块名
- feature: 功能点名称
- case_title: 用例标题
- preconditions: 前置条件
- steps: 测试步骤(Given-When-Then 格式)
- expected_result: 预期结果
- priority: 用例优先级 (P0/P1/P2)
- tags: 标签数组(如:["正向", "异常", "边界"]
## IR 内容:
{{ ir_content }}
请只输出 JSON 数组,不要用 ```json 代码块包裹。"""
class TestCaseGenerator:
"""Generate test case sets from IR YAML using LLM."""
def __init__(self):
self._tc_store: dict[str, dict] = {}
async def generate(self, ir_id: str, skill_name: str = "default") -> dict:
"""Generate test cases from an IR version."""
# Get IR content
ir_version = await ir_generator.get_ir(ir_id)
if not ir_version:
return {"error": f"IR version not found: {ir_id}", "cases": []}
# Handle both old YAML and new pipeline JSON formats
ir_content = ir_version.get("yaml_content", "")
ir_json = ir_version.get("ir_json")
# If we have the new pipeline format (rules array), convert to testcases directly
if ir_json and ir_json.get("rules"):
logger.info("[TC] 从 %d 条 IR 规则转换测试用例...", len(ir_json["rules"]))
cases = self._rules_to_cases(ir_json["rules"], ir_json.get("feature", ""))
tc_set_id = str(uuid.uuid4())[:8]
tc_set = self._store_tc(tc_set_id, ir_id, cases)
# Log distribution
p0 = sum(1 for c in cases if c.get("priority") == "P0")
p1 = sum(1 for c in cases if c.get("priority") == "P1")
logger.info("[TC] ✅ 生成 %d 条用例 (P0:%d P1:%d P2:%d)",
len(cases), p0, p1, len(cases) - p0 - p1)
return tc_set
# Fallback: use LLM with skill prompt
if not ir_content:
return {"error": "IR has no content", "cases": [], "tc_set_id": ""}
# Load skill prompt
skill = ir_generator.load_skill(skill_name)
prompt_template = skill.get("gen_cases_prompt", DEFAULT_GEN_PROMPT)
prompt = prompt_template.replace("{{ ir_content }}", ir_content)
logger.info("[TC] 调用 LLM 生成测试用例 for IR %s with skill %s", ir_id, skill_name)
client = router.get_text_client()
try:
raw = client.chat(
model=router.text_model,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
)
except Exception as e:
logger.error("Test case generation failed: %s", e)
return {"error": str(e), "cases": [], "tc_set_id": ""}
# Parse JSON response
cases = self._parse_json(raw)
# Create test case set
tc_set_id = str(uuid.uuid4())[:8]
tc_set = {
"tc_set_id": tc_set_id,
"ir_id": ir_id,
"cases": cases,
"case_count": len(cases),
"created_at": datetime.now(timezone.utc).isoformat(),
}
self._tc_store[tc_set_id] = tc_set
# Save to output
output_dir = settings.OUTPUT_DIR / ir_version.get("prd_id", "unknown") / "tc_sets"
output_dir.mkdir(parents=True, exist_ok=True)
tc_path = output_dir / f"tc_{tc_set_id}.json"
with open(tc_path, "w", encoding="utf-8") as f:
json.dump(tc_set, f, ensure_ascii=False, indent=2)
usg = client.usage
logger.info("Test cases generated: id=%s, count=%d, tokens: prompt=%d completion=%d total=%d",
tc_set_id, len(cases), usg["prompt_tokens"], usg["completion_tokens"], usg["total_tokens"])
return tc_set
async def get_tc_set(self, tc_set_id: str) -> dict | None:
"""Get a test case set by ID."""
return self._tc_store.get(tc_set_id)
@staticmethod
def _parse_json(raw: str) -> list[dict]:
"""Parse JSON from LLM response, handling code fences."""
text = raw.strip()
if text.startswith("```json") or text.startswith("```"):
first_nl = text.find("\n")
if first_nl != -1:
text = text[first_nl + 1:]
if text.endswith("```"):
text = text[:-3]
text = text.strip()
try:
parsed = json.loads(text)
if isinstance(parsed, list):
return parsed
if isinstance(parsed, dict) and "cases" in parsed:
return parsed["cases"]
return [parsed] if isinstance(parsed, dict) else []
except json.JSONDecodeError:
logger.warning("Failed to parse test case JSON, returning empty")
return []
def _rules_to_cases(self, rules: list[dict], feature: str) -> list[dict]:
"""Convert pipeline IR rules to test case format. TC IDs: TC-{feature_id}-{seq}"""
import re
feature_id = re.sub(r'[^A-Z]', '', feature.upper())[:6] or "FEAT"
if len(feature_id) < 2:
feature_id = (feature_id + "FEAT")[:4]
cases = []
for i, rule in enumerate(rules):
tc_id = f"TC-{feature_id}-{i+1:03d}"
desc = rule.get("description", "")
# Build steps string from trigger + actions
trigger = rule.get("trigger", {})
conditions = trigger.get("conditions", [])
cond_texts = []
for c in conditions:
unit = f" {c.get('unit', '')}" if c.get('unit') else ""
cond_texts.append(f"{c.get('signal','?')} {c.get('operator','?')} {c.get('value','?')}{unit}")
given = "Given " + (", ".join(cond_texts) if cond_texts else "触发条件满足")
actions = rule.get("actions", [])
action_texts = []
for a in actions:
if a.get("type") == "user_interaction":
action_texts.append(f"{a.get('description','')}('{a.get('content','')}')")
else:
action_texts.append(a.get("description", ""))
then = "Then " + ("; ".join(action_texts) if action_texts else "执行动作")
steps = f"{given}\nWhen 条件触发\n{then}"
# Determine tags
tags = ["正向"]
priority = rule.get("priority", "P2")
if priority == "P0":
tags.append("冒烟")
precond = rule.get("precondition", {})
if precond:
tags.append(f"{precond.get('app_type', '')} {precond.get('app_state', '')}".strip())
cases.append({
"id": tc_id,
"ir_rule_id": rule.get("rule_id", ""),
"module": feature,
"feature": rule.get("description", desc)[:40],
"case_title": desc[:60],
"preconditions": json.dumps(precond, ensure_ascii=False) if precond else "",
"steps": steps,
"expected_result": action_texts[-1] if action_texts else desc,
"priority": priority,
"tags": [t for t in tags if t],
})
return cases
def _store_tc(self, tc_set_id: str, ir_id: str, cases: list[dict]) -> dict:
"""Store a test case set and return its metadata."""
tc_set = {
"tc_set_id": tc_set_id,
"ir_id": ir_id,
"cases": cases,
"case_count": len(cases),
"created_at": datetime.now(timezone.utc).isoformat(),
}
self._tc_store[tc_set_id] = tc_set
return tc_set
tc_generator = TestCaseGenerator()