feat: 多轮运行 + GCOV 合并 + JSON 出力 + DesignDataGenerator
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# 出力目录规则
|
||||
|
||||
## 单轮(runs: [] 或 YAML 不存在)
|
||||
|
||||
```text
|
||||
runtime/{program_id}/
|
||||
input/KIN08R01, KIN08S01 ← COBOL 入力平面文件
|
||||
json/{program_id}.json ← JSON(input/working_storage/termination)
|
||||
output/KIN08W01 ← COBOL 出力平面文件
|
||||
gcov/ ← .gcda/.gcno(直下)
|
||||
```
|
||||
|
||||
## 多轮(runs: 定义在 config/programs/{program_id}.yaml 中)
|
||||
|
||||
```text
|
||||
runtime/{program_id}/
|
||||
run_{scenario_id}/
|
||||
input/KIN08R01, KIN08S01 ← 該当场景の入力
|
||||
json/{program_id}.json ← 該当场景の JSON
|
||||
output/KIN08W01 ← 該当场景の COBOL 出力
|
||||
gcov/
|
||||
run_{scenario_id}/ ← 該当场景の .gcda/.gcno
|
||||
```
|
||||
|
||||
## 单轮/多轮共有
|
||||
|
||||
- 编译: `work_dir/bin/{program_id}.exe`(全场景共享)
|
||||
- .gcno: `work_dir/bin/{program_id}_pp.gcno`(编译时生成,COPY 到各场景)
|
||||
- DB: `C:/Temp/gix/{program_id}_{scenario_id}.db`(多轮场景)或 `C:/Temp/gix/{program_id}.db`(单轮)
|
||||
- 运行前 COPY 到 `C:/Temp/gix/{program_id}.db`(COBOL 的 gixsql 硬编码连接此路径)
|
||||
- SYSIN 文件: 由 `write_sysin_file(run_cfg=scenario.sysin)` 按场景生成
|
||||
|
||||
## 覆盖率合并(多轮)
|
||||
|
||||
- 各场景独立跑 gcov → 解析 `.cbl.gcov` → `{line: count}` 字典
|
||||
- Python 级合并: `merged[line] = max(各场景的 count)`
|
||||
- 合并后数据传给 `run_coverage()`
|
||||
|
||||
## YAML 场景定义格式
|
||||
|
||||
`config/programs/{program_id}.yaml` 追加 `runs:` 字段:
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
- id: normal
|
||||
sysin:
|
||||
period: "202607" # P 卡値(null=无 P 卡)
|
||||
include_invalid_period: true # 是否追加无效 P 卡
|
||||
modes: ["NORMAL", "RESET"] # M 卡列表
|
||||
inject_duplicate_pk: false
|
||||
```
|
||||
|
||||
每个场景的 `sysin` 字段由该程序的 `write_sysin_file()` 解析,字段含义不跨程序通用。
|
||||
@@ -0,0 +1,332 @@
|
||||
# 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`
|
||||
|
||||
```python
|
||||
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() 兼容。
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
### 调用侧入口
|
||||
|
||||
```python
|
||||
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. 架构与数据流
|
||||
|
||||
```text
|
||||
[式样书 .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
|
||||
```
|
||||
|
||||
### 映射算法
|
||||
|
||||
```python
|
||||
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 规则:
|
||||
|
||||
```python
|
||||
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 行计数失真
|
||||
|
||||
### 解决方案
|
||||
|
||||
```python
|
||||
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. 多轮融合策略
|
||||
|
||||
### 策略选项
|
||||
|
||||
```yaml
|
||||
# 在 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()`)
|
||||
|
||||
```python
|
||||
# 原本
|
||||
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()`)
|
||||
|
||||
```python
|
||||
# 原本
|
||||
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 |
|
||||
Reference in New Issue
Block a user