"""黑盒(black-box-data-create)测试数据接入:JSON → cobol_testgen 内部 record。 黑盒 JSON 结构(每组一个文件): { "program": "ZAN04MAT", "records": [ { "input": { "": { "<字段>": "<值>" } } } ] } 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 matches = [ fd_name for fd_name, aliases in fd_aliases.items() if any(a and (nk.startswith(a) or a.startswith(nk)) for a in aliases) ] if len(matches) == 1: return matches[0] if len(matches) > 1: logger.warning(f"黑盒 FD '{bb_key}' 匹配到多个: {matches},跳过") 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 root = p / 'black_box' if (p / 'black_box').is_dir() else p files = [] for sub in sorted(root.iterdir()): if sub.is_dir(): files.extend(sorted(sub.glob('*_g*.json'))) return files 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: if not isinstance(raw, dict): return {} 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 if not isinstance(obj, dict): logger.warning(f"黑盒 JSON 顶层非对象,跳过: {jf}") 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