feat(blackbox): add black-box JSON to internal record mapping
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
"""黑盒(black-box-data-create)测试数据接入:JSON → cobol_testgen 内部 record。
|
||||||
|
|
||||||
|
黑盒 JSON 结构(每组一个文件):
|
||||||
|
{ "program": "ZAN04MAT",
|
||||||
|
"records": [ { "input": { "<FD>": { "<字段>": "<值>" } } } ] }
|
||||||
|
|
||||||
|
FD 键与字段名由 LLM 生成,存在多种写法(FD名 / DD名 / 识别子,字段带或不带 '-'),
|
||||||
|
因此这里做归一化容错匹配。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlackBoxGroup:
|
||||||
|
label: str
|
||||||
|
records: list[dict]
|
||||||
|
term_types: list[str] = field(default_factory=list)
|
||||||
|
source_path: str = ''
|
||||||
|
|
||||||
|
|
||||||
|
def norm_name(name) -> str:
|
||||||
|
"""字段/FD 名归一化:大写 + 去除 '-' '_' 分隔符。"""
|
||||||
|
return str(name).upper().replace('-', '').replace('_', '')
|
||||||
|
|
||||||
|
|
||||||
|
def _fd_aliases(fd_name: str, select_info: dict) -> set:
|
||||||
|
aliases = {norm_name(fd_name)}
|
||||||
|
sel = select_info.get(fd_name, {})
|
||||||
|
if isinstance(sel, dict):
|
||||||
|
assign = sel.get('assign')
|
||||||
|
if assign:
|
||||||
|
aliases.add(norm_name(assign))
|
||||||
|
return aliases
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_fd(bb_key, fd_aliases: dict):
|
||||||
|
"""bb_key → 内部 fd_name;先精确别名,再前缀容错(R01 → R01INNFIL)。"""
|
||||||
|
nk = norm_name(bb_key)
|
||||||
|
for fd_name, aliases in fd_aliases.items():
|
||||||
|
if nk in aliases:
|
||||||
|
return fd_name
|
||||||
|
for fd_name, aliases in fd_aliases.items():
|
||||||
|
for a in aliases:
|
||||||
|
if a and (nk.startswith(a) or a.startswith(nk)):
|
||||||
|
return fd_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _field_alias_map(data_fields: list[dict]) -> dict:
|
||||||
|
m = {}
|
||||||
|
for f in data_fields or []:
|
||||||
|
if f.get('is_88'):
|
||||||
|
continue
|
||||||
|
m.setdefault(norm_name(f['name']), f['name'])
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_group_files(path: Path) -> list[Path]:
|
||||||
|
p = Path(path)
|
||||||
|
if p.is_file() and p.suffix.lower() == '.json':
|
||||||
|
return [p]
|
||||||
|
if not p.is_dir():
|
||||||
|
return []
|
||||||
|
files = sorted(p.glob('*_g*.json'))
|
||||||
|
if files:
|
||||||
|
return files
|
||||||
|
bb = p / 'black_box'
|
||||||
|
if bb.is_dir():
|
||||||
|
return sorted(bb.rglob('*_g*.json'))
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _group_label(json_path: Path) -> str:
|
||||||
|
for token in json_path.stem.split('_'):
|
||||||
|
if token.lower().startswith('g') and token[1:].isdigit():
|
||||||
|
return token
|
||||||
|
return json_path.stem
|
||||||
|
|
||||||
|
|
||||||
|
def _map_record(raw, fd_aliases, field_map, jf: Path) -> dict:
|
||||||
|
inp = raw.get('input')
|
||||||
|
if not isinstance(inp, dict):
|
||||||
|
return {}
|
||||||
|
rec = {}
|
||||||
|
for bb_fd, fields in inp.items():
|
||||||
|
fd_name = _resolve_fd(bb_fd, fd_aliases)
|
||||||
|
if fd_name is None:
|
||||||
|
logger.debug(f"黑盒 FD 未匹配: {bb_fd} ({jf.name})")
|
||||||
|
continue
|
||||||
|
if not isinstance(fields, dict):
|
||||||
|
continue
|
||||||
|
for bb_field, val in fields.items():
|
||||||
|
internal = field_map.get(norm_name(bb_field))
|
||||||
|
if internal is None:
|
||||||
|
logger.debug(f"黑盒字段未匹配: {bb_field} ({jf.name})")
|
||||||
|
continue
|
||||||
|
rec[internal] = '' if val is None else str(val)
|
||||||
|
return rec
|
||||||
|
|
||||||
|
|
||||||
|
def load_black_box_groups(path, fd_fields: dict, select_info: dict,
|
||||||
|
data_fields: list[dict]) -> list[BlackBoxGroup]:
|
||||||
|
"""读取黑盒 JSON,返回分组列表。
|
||||||
|
|
||||||
|
path 可以是:程序目录(自动找 black_box/)、black_box 目录、或单个 JSON 文件。
|
||||||
|
"""
|
||||||
|
fd_aliases = {fd: _fd_aliases(fd, select_info) for fd in fd_fields}
|
||||||
|
field_map = _field_alias_map(data_fields)
|
||||||
|
groups = []
|
||||||
|
for jf in _iter_group_files(Path(path)):
|
||||||
|
try:
|
||||||
|
obj = json.loads(jf.read_text(encoding='utf-8'))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning(f"黑盒 JSON 解析失败 {jf}: {e}")
|
||||||
|
continue
|
||||||
|
label = _group_label(jf)
|
||||||
|
records = []
|
||||||
|
for raw in obj.get('records', []) or []:
|
||||||
|
rec = _map_record(raw, fd_aliases, field_map, jf)
|
||||||
|
if rec:
|
||||||
|
records.append(rec)
|
||||||
|
if not records:
|
||||||
|
logger.warning(f"黑盒组 {label} 无有效记录,跳过 ({jf.name})")
|
||||||
|
continue
|
||||||
|
groups.append(BlackBoxGroup(
|
||||||
|
label=label,
|
||||||
|
records=records,
|
||||||
|
term_types=['normal'] * len(records),
|
||||||
|
source_path=str(jf),
|
||||||
|
))
|
||||||
|
logger.info(f" 黑盒组 {label}: {len(records)} 条记录 ({jf.name})")
|
||||||
|
return groups
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""黑盒 JSON → cobol_testgen 内部 record 映射测试"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
from cobol_testgen.blackbox import norm_name, load_black_box_groups
|
||||||
|
|
||||||
|
|
||||||
|
def _write_group(tmp, name, payload):
|
||||||
|
d = Path(tmp) / "black_box" / "g1"
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
p = d / f"{name}.json"
|
||||||
|
p.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def test_norm_name():
|
||||||
|
assert norm_name("R01-APPL-ID") == "R01APPLID"
|
||||||
|
assert norm_name("R01APPL-ID") == "R01APPLID"
|
||||||
|
assert norm_name("r01_appl_id") == "R01APPLID"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_groups_fd_assign_and_field_hyphen():
|
||||||
|
fd_fields = {"R01INNFIL": ["R01APPL-ID", "R01EMP-ID"]}
|
||||||
|
select_info = {"R01INNFIL": {"assign": "ZAN04R01"}}
|
||||||
|
data_fields = [
|
||||||
|
{"name": "R01APPL-ID", "pic": "X(8)"},
|
||||||
|
{"name": "R01EMP-ID", "pic": "9(8)"},
|
||||||
|
]
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
_write_group(tmp, "ZAN04MAT_g1", {
|
||||||
|
"program": "ZAN04MAT",
|
||||||
|
"records": [{
|
||||||
|
"input": {"ZAN04R01": {
|
||||||
|
"R01-APPL-ID": "A0000001",
|
||||||
|
"R01-EMP-ID": "00000001",
|
||||||
|
}}
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
groups = load_black_box_groups(tmp, fd_fields, select_info, data_fields)
|
||||||
|
assert len(groups) == 1
|
||||||
|
g = groups[0]
|
||||||
|
assert g.label == "g1"
|
||||||
|
assert g.records[0]["R01APPL-ID"] == "A0000001"
|
||||||
|
assert g.records[0]["R01EMP-ID"] == "00000001"
|
||||||
|
assert g.term_types == ["normal"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_groups_fd_name_variant():
|
||||||
|
fd_fields = {"R01INNFIL": ["R01APPL-ID"]}
|
||||||
|
select_info = {"R01INNFIL": {"assign": "ZAN04R01"}}
|
||||||
|
data_fields = [{"name": "R01APPL-ID", "pic": "X(8)"}]
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
_write_group(tmp, "ZAN04MAT_g1", {
|
||||||
|
"program": "ZAN04MAT",
|
||||||
|
"records": [{"input": {"R01INNFIL": {"R01-APPL-ID": "B0000002"}}}],
|
||||||
|
})
|
||||||
|
groups = load_black_box_groups(tmp, fd_fields, select_info, data_fields)
|
||||||
|
assert groups[0].records[0]["R01APPL-ID"] == "B0000002"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_groups_skips_unmatched_and_empty():
|
||||||
|
fd_fields = {"R01INNFIL": ["R01APPL-ID"]}
|
||||||
|
select_info = {"R01INNFIL": {"assign": "ZAN04R01"}}
|
||||||
|
data_fields = [{"name": "R01APPL-ID", "pic": "X(8)"}]
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
_write_group(tmp, "ZAN04MAT_g1", {
|
||||||
|
"program": "ZAN04MAT",
|
||||||
|
"records": [{"input": {"UNKNOWN-FD": {"X": "1"}}}],
|
||||||
|
})
|
||||||
|
groups = load_black_box_groups(tmp, fd_fields, select_info, data_fields)
|
||||||
|
assert groups == []
|
||||||
Reference in New Issue
Block a user