11 KiB
11 KiB
DesignDataGenerator — 式样书驱动测试数据生成
1. 概要
目的
V3 现有两条数据生成路径:
- 白盒数据(
cobol_testgen.generate_data()):基于 MC/DC 路径枚举,覆盖决策点 - LLM 测试数据(
Agent2Data.design()):基于 FieldTree + DeepSeek API
缺少一条能读取日文详细设计书(.md)并生成机能测试数据的路径。
DesignDataGenerator 填补这个空缺。它解析式样书中的业务规则、输入输出定义、DB 结构,通过 LLM 生成有业务意义的测试数据。与白盒数据合并后,覆盖可以同时命中决策点分支和业务场景路径。
与其他 Agent 的关系
| Agent | 输入 | 输出 | 位置 |
|---|---|---|---|
Agent1Parser |
COPYBOOK | FieldTree | agents/agent1_parser.py |
DesignDataGenerator |
式样书 .md + 源码 + COPYBOOK | list[dict](机能数据) |
agents/design_data.py ← 新增 |
Agent2Data |
FieldTree | TestSuite(LLM 数据) | agents/agent2_data.py |
Agent3Diagnostic |
FieldResult | 诊断文本 | agents/agent3_diagnostic.py |
2. 接口定义
DesignDataGenerator
class DesignDataGenerator:
def __init__(
self,
llm_client: LLMClient,
cpy_dirs: list[str | Path],
rules_dir: str | Path = "rules",
):
...
def generate(
self,
design_md_text: str, # 式样书 .md 全文
source_text: str, # COBOL 源码全文
file_db_md_text: str | None = None, # COPY句定义书 .md(可选)
db_md_text: str | None = None, # DB 定义书 .md(可选)
replacing_rules: dict[str, str] | None = None, # REPLACING 展开规则
v3_field_names: list[str] | None = None, # V3 字段名参考列表
) -> list[dict]:
"""生成机能测试数据。
Returns:
list[dict]: 每条记录为 {field_name: value} 格式。
字段名已被 REPLACING 展开,与 generate_data() 兼容。
"""
...
调用侧入口
def generate_all_data(
program_id: str,
src_text: str,
copybook_dirs: list[str | Path],
st: dict | None = None, # extract_structure 结果(传入可跳过重复解析)
config: Config | None = None, # 用于读取 LLM / 设计书路径配置
design_doc_dir: str | Path | None = None, # 式样书目录
file_db_md_path: str | Path | None = None, # COPY句定义书路径
db_md_path: str | Path | None = None, # DB定义书路径
merge_strategy: str = "merge_to_normal", # merge_to_normal / as_separate_scenes / auto
) -> list[dict]:
"""白盒数据 + 机能数据 + 策略补充 全量生成与合并。
合并策略:
- merge_to_normal: 全部合到一条记录集中
- as_separate_scenes: 返回 (main_records, func_records),由调用方决定如何分场景
- auto: DEPRECATED
"""
...
3. 架构与数据流
[式样书 .md] ──┐
[源码 .cbl] ───┤
[COPYBOOK] ────┤
[DB定义书 .md] ┘
│
▼
DesignDataGenerator.generate()
│
├── InputParser 解析式样书 → ProgramMeta
├── RuleLoader 模式匹配 → 规则文本
├── PromptBuilder LLM 提示词
├── LLMClient DeepSeek API
├── 字段名映射 REPLACING 展开 + V3 字段名参考
└── 返回 list[dict]
│
▼
generate_all_data()
├── ① generate_data() → 白盒数据
├── ② DesignDataGenerator → 机能数据(式样书存在时)
├── ③ strategy_supplement() → 策略补充
├── ④ 去重(基于 hash)
└── ⑤ 返回 merged_records
4. 字段名兼容性处理
问题
外部 Agent 从 COPYBOOK 解析字段名,含有未展开的 (A) 占位符:
(A)EMP-ID → V3 是 R01EMP-ID(REPLACING ==(A)== BY ==R01== 后)
(A)DATE → V3 是 R01DATE
映射算法
def _resolve_field_names(
records: list[dict],
replacing_rules: dict[str, str] | None,
v3_field_names: set[str] | None,
) -> list[dict]:
"""将外部 Agent 输出的字段名映射为 V3 兼容名称。"""
if not (replacing_rules or v3_field_names):
return records # 无需映射
result = []
for rec in records:
mapped = {}
for key, val in rec.items():
new_key = key
# 第一步:REPLACING 展开
if replacing_rules:
for old, new in replacing_rules.items():
if old in new_key:
new_key = new_key.replace(old, new)
# 第二步:如果 V3 字段名参考列表存在,精确匹配
if v3_field_names and new_key not in v3_field_names:
# 尝试去掉连字符
no_hyphen = new_key.replace("-", "")
if no_hyphen in v3_field_names:
new_key = no_hyphen
# 尝试下划线转空
no_underscore = new_key.replace("_", "")
if no_underscore in v3_field_names:
new_key = no_underscore
else:
continue # 无法映射 → 丢弃
mapped[new_key] = val
result.append(mapped)
return result
REPLACING 规则提取
从 COBOL 源码中提取 REPLACING 规则:
def _extract_replacing_rules(source_text: str) -> dict[str, str]:
"""从 COPY ... REPLACING ... 语句中提取替换规则。"""
rules = {}
for m in re.finditer(
r'COPY\s+(\w+)\s+REPLACING\s+(?:==(\w+)==\s+BY\s+==(\w+)==\s*)*\.',
source_text, re.IGNORECASE
):
for i in range(0, len(m.groups()) - 1, 2):
old = m.group(i + 2)
new = m.group(i + 3)
if old and new:
rules[old] = new
return rules
5. 去重策略
问题
白盒数据和机能数据可能包含重复记录(完全相同或高度相似)。重复会导致:
- COBOL 程序处理重复
- INSERT PK 冲突
- gcov 行计数失真
解决方案
def _dedup(
main_records: list[dict],
additional_records: list[dict],
key_fields: list[str] | None = None,
) -> list[dict]:
"""合并+去重,additional 优先保留。"""
seen = set()
result = []
# 先处理 additional(机能数据优先)
for rec in additional_records:
h = _hash(rec, key_fields)
if h not in seen:
seen.add(h)
result.append(rec)
# 后处理 main
for rec in main_records:
h = _hash(rec, key_fields)
if h not in seen:
seen.add(h)
result.append(rec)
return result
def _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()))
6. 多轮融合策略
策略选项
# 在 config/programs/{pid}.yaml 或 CLI 参数中控制
merge_strategy: merge_to_normal # 默认值
| 策略 | 行为 | 适用场景 |
|---|---|---|
merge_to_normal |
机能数据合并到 normal 场景,与白盒数据一起执行 | 简单程序,一轮覆盖所有 |
as_separate_scenes |
机能数据作为独立场景 run_func/ 执行 |
复杂程序,业务场景与白盒路径不兼容 |
auto |
有设计书时用 as_separate_scenes,否则 merge_to_normal |
通用场景 |
as_separate_scenes 时的目录
runtime/{pid}/
run_normal/ ← 白盒数据(MC/DC 路径)
input/ json/ output/
run_func/ ← 机能数据(式样书驱动)
input/ json/ output/
gcov/run_normal/ ← 各场景独立 .gcda
gcov/run_func/
7. 调用链路
DB 管道(orchestrator_db.py:step2_generate_inputs())
# 原本
recs = generate_data(src_text, st, copybook_dirs=cbd)
# 改为
recs = generate_all_data(
program_id=self.program_id,
src_text=src_text,
copybook_dirs=cbd,
st=st,
config=self.config,
design_doc_dir=str(v3_root / "详细设计书"),
file_db_md_path=str(v3_root / "详细设计书" / "COPY句定义书.md"),
)
原生管道(cobol_testgen/__init__.py:main())
# 原本
recs = generate_data(src_text, st, copybook_dirs=cbd)
# 改为
recs = generate_all_data(
program_id=filepath.stem,
src_text=source,
copybook_dirs=[str(filepath.parent / '..' / 'cpy')],
st=st,
design_doc_dir=str(design_doc_base),
)
8. 外部 Agent 代码迁移清单
从 jcl-cobol-data-create/ 迁移
| 源文件 | 目标 | 变更 |
|---|---|---|
input_parser.py |
agents/design_data_input_parser.py |
接收文本而非路径 |
rule_loader.py |
内置到 design_data.py 方法 |
简化接口 |
prompt_builder.py |
内置到 design_data.py |
使用 LLMClient |
api_client.py |
不需要 | V3 已有 agents/llm.py |
output_writer.py |
不需要 | 不写文件 |
models.py |
部分并入 design_data.py |
按需移植数据类 |
rules/pgm_pattern/*.md |
rules/pgm_pattern/ |
直接复制 |
rules/special_feature/*.md |
rules/special_feature/ |
直接复制 |
layout/*.md |
layout/ |
参考用 |
新建文件清单
| 文件 | 说明 |
|---|---|
agents/design_data.py |
DesignDataGenerator 主类 |
agents/design_data_input_parser.py |
式样书解析器(从外部 Agent 移植) |
cobol_testgen/data_merger.py |
generate_all_data() + 去重 + 字段名映射 |
9. 错误处理
场景
| 场景 | 行为 |
|---|---|
| 式样书 .md 不存在 | 跳过机能数据,仅返回白盒数据 |
| LLM 调用超时 | 捕获异常,跳过机能数据,日志告警 |
| LLM 返回非法 JSON | try/except,返回空列表 |
| 字段名映射失败(无法匹配 V3 字段名) | 丢弃该字段,日志记录 |
| 式样书存在但无法解析(格式不标准) | try/except,跳过机能数据 |
10. 测试计划
| 测试 | 方法 |
|---|---|
| 式样书不存在时回退到仅白盒 | mock os.path.exists → False |
| LLM 超时/出错时回退 | mock LLMClient.call → raise TimeoutError |
| 字段名 REPLACING 映射 | Fixture 含 (A)EMP-ID → 期待 R01EMP-ID |
| 去重逻辑 | 白盒 10 条 + 机能 5 条(含 2 条重复)→ 期待 13 条 |
多轮融合策略 as_separate_scenes |
验证 run_func/ 目录被创建 |
| 实际 COBOL 执行 + gcov | KIN08DBU 白盒+机能合并后覆盖率是否 >= 原 52/60 |