"""DesignDataGenerator — 式样书驱动测试数据生成器。 从详细设计书 .md + COBOL 源码 + COPYBOOK 中提取业务信息, 通过 LLM 生成有业务意义的機能テストデータ。 """ import json import logging import os import re from pathlib import Path from typing import Optional from agents.llm import LLMClient from agents.design_data_input_parser import ( DesignDataInputParser, ProgramMeta, ) logger = logging.getLogger(__name__) # LLM 提示词 _SYSTEM_PROMPT = """你是 COBOL 测试数据生成专家。根据详细设计书、COPYBOOK 结构和 DB 定义, 生成测试数据。数据必须覆盖正常路径和边界条件。 输出格式: {"records": [{"field_name": "value", ...}]} JSON only。""" def _resolve_field_names( records: list[dict], replacing_rules: dict[str, str] | None, v3_field_names: set[str] | None, ) -> list[dict]: """将外部 Agent 输出的字段名映射为 V3 兼容名称。 处理顺序: 1. REPLACING 展开((A) → R01) 2. 去掉前缀和字段名之间的多余连字符(R01-EMP-ID → R01EMP-ID) 3. 尝试直接匹配 V3 字段名 4. 尝试以 V3 字段名 prefix 截断匹配 5. 无法映射的字段丢弃 """ if not replacing_rules and not v3_field_names: return records # Build prefix map: from REPLACING rules, e.g. (A) → R01, then R01 is the prefix prefixes = set() prefix_from_replacing = {} if replacing_rules: for old, new in replacing_rules.items(): if new.strip(): prefixes.add(new) prefix_from_replacing[old] = new if not v3_field_names: # Just apply replacing, no V3 validation result = [] for rec in records: mapped = {} for key, val in rec.items(): new_key = key for old, new in prefix_from_replacing.items(): new_key = new_key.replace(old, new) mapped[new_key] = val result.append(mapped) return result result = [] for rec in records: mapped = {} for key, val in rec.items(): new_key = key # Step 1: REPLACING 展开 for old, new in prefix_from_replacing.items(): new_key = new_key.replace(old, new) # Step 2: 去掉前缀和字段名间的连字符 # Agent 输出: R01-EMP-ID, V3 期望: R01EMP-ID # 去掉 {prefix}- 前缀(如果前缀是 R01,去掉 R01-) for p in prefixes: if new_key.startswith(p + "-"): new_key = p + new_key[len(p) + 1 :] break # Step 3: 直接匹配 if new_key in v3_field_names: mapped[new_key] = val continue # Step 4: 去掉所有连字符尝试匹配 no_hyphen = new_key.replace("-", "") if no_hyphen in v3_field_names: mapped[no_hyphen] = val continue # Step 5: 去掉下划线尝试匹配 no_underscore = no_hyphen.replace("_", "") if no_underscore in v3_field_names: mapped[no_underscore] = val continue logger.debug(f" field '{key}' -> '{new_key}' not in V3 fields, dropped") result.append(mapped) return result def _extract_replacing_rules(source_text: str) -> dict[str, str]: """从 COBOL 源码的 COPY ... REPLACING 提取替换规则。""" rules = {} for m in re.finditer( r"COPY\s+(\w+)\s+REPLACING\s+==\(A\)==\s+BY\s+==(\w+)==", source_text, re.IGNORECASE, ): rules["(A)"] = m.group(2) return rules def _dedup( main_records: list[dict], additional_records: list[dict], key_fields: list[str] | None = None, ) -> list[dict]: """合并+去重,additional 优先保留。""" seen = set() result = [] for rec in additional_records: h = _record_hash(rec, key_fields) if h not in seen: seen.add(h) result.append(rec) for rec in main_records: h = _record_hash(rec, key_fields) if h not in seen: seen.add(h) result.append(rec) return result def _record_hash(rec: dict, key_fields: list[str] | None) -> tuple: if key_fields: return tuple(rec.get(k, "") for k in key_fields) return tuple(sorted(rec.items())) def _load_rules(rules_dir: str) -> str: """Load all rules from pgm_pattern/ and special_feature/ directories.""" texts = [] base = Path(rules_dir) for subdir in ["pgm_pattern", "special_feature"]: d = base / subdir if d.exists(): for f in sorted(d.glob("*.md")): texts.append(f"=== {subdir}/{f.name} ===\n{f.read_text(encoding='utf-8')}") return "\n\n".join(texts) class DesignDataGenerator: """式样书驱动测试数据生成器。 使用例: llm = LLMClient(model="deepseek-v4-flash") gen = DesignDataGenerator(llm, cpy_dirs=["cpy"]) records = gen.generate( design_md_text=design_text, source_text=source_text, ) """ def __init__( self, llm_client: LLMClient, cpy_dirs: list[str | Path], rules_dir: str | Path = "rules", ): self.llm = llm_client self.cpy_dirs = cpy_dirs self.rules_dir = Path(rules_dir) def generate( self, design_md_text: str, source_text: str, file_db_md_text: str | None = None, db_md_text: str | None = None, replacing_rules: dict[str, str] | None = None, v3_field_names: list[str] | None = None, ) -> list[dict]: """生成机能测试数据。 Args: design_md_text: 式样书 .md 全文 source_text: COBOL 源码全文 file_db_md_text: COPY句定义书 .md(可选) db_md_text: DB 定义书 .md(可选) replacing_rules: REPLACING 展开规则 v3_field_names: V3 字段名参考列表(用于映射验证) Returns: list[dict]: 每条记录为 {field_name: value} 格式 """ logger.info(" DesignDataGenerator: parsing design document...") try: parser = DesignDataInputParser() meta = parser.parse(design_md_text, source_text) except Exception as e: logger.warning(f" Design doc parsing failed: {e}") return [] if not meta.pgm_pattern: logger.info(" No PGM pattern found in design doc, skipping") return [] logger.info( f" Program: {meta.program_id}, pattern: {meta.pgm_pattern}, " f"type: {meta.input_type}" ) # 加载规则 rules_text = _load_rules(str(self.rules_dir)) # 构建描述信息 files_desc = "\n".join( f" {f.identifier}: {f.file_db_name} (I/O={f.io}, 媒体={f.medium})" for f in meta.files ) keys_desc = "\n".join( f" {k.file_name}: sort={k.sort_condition}, key={k.key_condition}" for k in meta.keys ) user_prompt = f"""## プログラム情報 - プログラムID: {meta.program_id} - PGMパターン: {meta.pgm_pattern} - 入力タイプ: {meta.input_type} ## 使用ファイル一覧 {files_desc or '(なし)'} ## キー項目一覧 {keys_desc or '(なし)'} ## 処理詳細 {meta.process_detail[:2000] if meta.process_detail else '(なし)'} ## 出力レコード定義 {meta.output_records[:1000] if meta.output_records else '(なし)'} ## データ生成ルール {rules_text[:2000] if rules_text else '(なし)'} 以下の JSON 形式でテストデータを生成してください: {{"records": [{{"field1": "value1", "field2": "value2", ...}}]}}""" try: response = self.llm.call( [ {"role": "system", "content": _SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ] ) logger.info(" LLM response received") except Exception as e: logger.warning(f" LLM call failed: {e}") return [] try: parsed = json.loads(response) raw_records = parsed.get("records", []) except (json.JSONDecodeError, KeyError) as e: logger.warning(f" LLM response parse failed: {e}") return [] if not raw_records: logger.info(" No records generated") return [] # 字段名映射 v3_names_set = set(v3_field_names) if v3_field_names else None mapped = _resolve_field_names(raw_records, replacing_rules, v3_names_set) logger.info(f" Generated {len(mapped)} records") return mapped