"""非DB COBOL程序的编译·执行·验证层(被 __init__.py 的 --gcov/--run 调用) V3 编译方式(Windows 本地 cobc)+ SOURCE 分组执行+字段对比+出力保存。 """ import logging import os import re import shutil import subprocess from dataclasses import dataclass, field from pathlib import Path from . import file_io logger = logging.getLogger(__name__) # ── 数据模型 ── @dataclass class GroupInfo: """一组执行用例(SOURCE 版)""" name: str records: list[dict] expected_outputs: list[dict] expected_returncode: int = 0 fd_field_dicts: dict = field(default_factory=dict) open_dir: dict = field(default_factory=dict) select_info: dict = field(default_factory=dict) overlap_mask: list[bool] = field(default_factory=list) multi_write_fds: set = field(default_factory=set) command_args: list = field(default_factory=list) @dataclass class CompareDetail: """单字段对比结果(兼容 __init__.py 的 DetailItem 接口)""" field: str expected: str actual: str match: bool @dataclass class GroupResult: """单组执行+对比结果""" name: str returncode: int passed: bool details: list[CompareDetail] = field(default_factory=list) error_message: str = '' # ── 路径解析(V3)── def _resolve_sub_dir(source_dir: str) -> str: p = Path(source_dir).resolve() for d in [p.parent / 'sub', p / 'sub']: if d.is_dir(): return str(d) return str(p.parent / 'sub') def _resolve_cpy_dir(source_dir: str) -> str: p = Path(source_dir).resolve() for d in [p.parent / 'cpy', p / 'cpy']: if d.is_dir(): return str(d) return str(p.parent / 'cpy') def _input_assign_names(select_info: dict, open_dir: dict, fd_fields: dict) -> list[str]: names = [] seen = set() for fd_name in fd_fields: direction = (open_dir or {}).get(fd_name, '') if direction not in ('INPUT', 'I-O'): continue assign = select_info.get(fd_name, {}).get('assign', '') if assign and assign not in seen: seen.add(assign) names.append(assign) return names def _output_assign_names(select_info: dict, open_dir: dict, fd_fields: dict) -> list[str]: names = [] seen = set() for fd_name in fd_fields: direction = (open_dir or {}).get(fd_name, '') if direction not in ('OUTPUT', 'I-O'): continue assign = select_info.get(fd_name, {}).get('assign', '') if assign and assign not in seen: seen.add(assign) names.append(assign) return names # ── SUB 模块编译(V3)── def compile_sub_modules(sub_dir: str, work_dir: str, cpy_dir: str | None = None, log_dir: str | None = None) -> list[str]: sub_path = Path(sub_dir) work_path = Path(work_dir) work_path.mkdir(parents=True, exist_ok=True) o_files = [] if not sub_path.is_dir(): logger.warning(f" SUB目录不存在: {sub_dir}") return o_files for cbl in sorted(sub_path.glob('*.cbl')): o_path = work_path / f'{cbl.stem}.o' gcno_path = work_path / f'{cbl.stem}.gcno' if o_path.exists() and gcno_path.exists(): o_files.append(str(o_path)) continue cmd = ['cobc', '-c', '-g', '--coverage', '-o', str(o_path)] if cpy_dir: cmd.extend(['-I', cpy_dir]) cmd.append(str(cbl)) logger.info(f" SUB: {cbl.name}") orig = os.getcwd() try: os.chdir(str(work_path)) r = subprocess.run(cmd, capture_output=True, text=True, timeout=120, errors='replace') finally: os.chdir(orig) if log_dir: log_path = Path(log_dir) / 'compile' / f"sub_{cbl.stem}.log" log_path.parent.mkdir(parents=True, exist_ok=True) log_path.write_text( f"COMMAND: {' '.join(cmd)}\n" f"RETURNCODE: {r.returncode}\n\n" f"STDOUT:\n{r.stdout}\n\n" f"STDERR:\n{r.stderr}", encoding='utf-8' ) if r.returncode != 0: logger.warning(f" SUB编译失败 {cbl.name}: {r.stderr.strip()[:200]}") continue o_files.append(str(o_path)) return o_files def compile_program(program_name: str, source_dir: str, work_dir: str, sub_objects: list[str], cpy_dir: str | None = None, log_dir: str | None = None) -> str: work_path = Path(work_dir) work_path.mkdir(parents=True, exist_ok=True) src = Path(source_dir) / f'{program_name}.cbl' if not src.exists(): raise FileNotFoundError(f"源文件不存在: {src}") exe = work_path / f'{program_name}.exe' cmd = ['cobc', '-x', '-g', '--coverage', '-o', str(exe)] if cpy_dir: cmd.extend(['-I', cpy_dir]) cmd.append(str(src)) cmd.extend(sub_objects) logger.info(f" LINK: {program_name}.exe") orig = os.getcwd() try: os.chdir(str(work_path)) r = subprocess.run(cmd, capture_output=True, text=True, timeout=120, errors='replace') finally: os.chdir(orig) if log_dir: log_path = Path(log_dir) / 'compile' / f"{program_name}.log" log_path.parent.mkdir(parents=True, exist_ok=True) log_path.write_text( f"COMMAND: {' '.join(cmd)}\n" f"RETURNCODE: {r.returncode}\n\n" f"STDOUT:\n{r.stdout}\n\n" f"STDERR:\n{r.stderr}", encoding='utf-8' ) if r.returncode != 0: raise RuntimeError(f"编译失败 {program_name}: {r.stderr.strip()[:500]}") return str(exe) # ── 目录管理(SOURCE)── def _clean_gcda(temp_dir: str): for f in Path(temp_dir).glob('*.gcda'): try: f.unlink() except OSError: pass # ── 工具函数(SOURCE)── def _build_fd_field_dicts(fd_fields: dict, fields_dict: list) -> dict: name_map = {f['name']: f for f in fields_dict} result = {} for fd_name, names in fd_fields.items(): result[fd_name] = [] for n in names: if n in name_map: pi = name_map[n].get('pic_info', {}) if pi.get('type') == 'unknown': continue result[fd_name].append(name_map[n]) return result def _name_to_field(fields_dict: list[dict]) -> dict: return {f['name']: f for f in fields_dict if not f.get('is_88') and not f.get('is_filler')} # ── 读取出力(SOURCE)── def read_outputs(fd_field_dicts: dict, open_dir: dict, select_info: dict, temp_dir: str) -> dict[str, list[dict]]: result = {} for fd_name, fds in fd_field_dicts.items(): direction = open_dir.get(fd_name, '') if direction not in ('OUTPUT', 'I-O'): continue sel = select_info.get(fd_name, {}) if isinstance(sel, dict): assign = sel.get('assign', fd_name) org = sel.get('organization', 'SEQUENTIAL') rec_mode = sel.get('recording_mode', 'F') else: assign = sel org = 'SEQUENTIAL' rec_mode = 'F' outpath = os.path.join(temp_dir, assign) if not os.path.exists(outpath): logger.warning(f" 出力文件不存在: {outpath}") continue line_seq = (org == 'LINE SEQUENTIAL') try: records = file_io.read_output_file( outpath, fds, line_sequential=line_seq, recording_mode=rec_mode ) result[fd_name] = records except Exception as e: logger.warning(f" 读取出力文件失败 {outpath}: {e}") continue return result # ── 对比(SOURCE)── def compare_outputs(actual: list[dict], expected: list[dict], fd_fields: list[dict], subset_match: bool = False) -> tuple[bool, list[CompareDetail]]: all_pass = True details = [] if subset_match: for exp in expected: for fd in fd_fields: fname = fd['name'] expected_val = exp.get(fname, '') if not expected_val: continue found = any( act.get(fname, '') == expected_val for act in actual ) if not found: all_pass = False details.append(CompareDetail( field=fname, expected=expected_val, actual='(not found in actual)', match=False, )) return all_pass, details if len(actual) != len(expected): logger.warning(f" compare_outputs: actual={len(actual)} vs expected={len(expected)}") for i, (act, exp) in enumerate(zip(actual, expected)): for fd in fd_fields: fname = fd['name'] actual_val = act.get(fname, '') expected_val = exp.get(fname, '') match = (actual_val == expected_val) if not match: all_pass = False details.append(CompareDetail( field=f'{fname}[{i}]', expected=expected_val, actual=actual_val, match=match, )) if len(actual) != len(expected): all_pass = False details.append(CompareDetail( field='record_count', expected=str(len(expected)), actual=str(len(actual)), match=False, )) return all_pass, details # ── 单组执行(SOURCE 改 - Native)── def run_group(group: GroupInfo, exe_path: str, temp_dir: str, log_dir: str | None = None) -> GroupResult: logger.info(f" 执行组: {group.name} ({len(group.records)} 条记录)") exe = Path(exe_path).resolve() work_dir = Path(temp_dir).resolve() orig = os.getcwd() try: os.chdir(str(work_dir)) result = subprocess.run( [str(exe)] + list(group.command_args or []), capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=60, ) except subprocess.TimeoutExpired: return GroupResult(name=group.name, returncode=-1, passed=False, error_message='timeout') finally: os.chdir(orig) if log_dir: log_path = Path(log_dir) / f"{group.name}.log" log_path.parent.mkdir(parents=True, exist_ok=True) log_path.write_text( f"COMMAND: {' '.join([str(exe)])}\n" f"RETURNCODE: {result.returncode}\n\n" f"STDOUT:\n{result.stdout}\n\n" f"STDERR:\n{result.stderr}", encoding='utf-8' ) rc = result.returncode all_pass = (rc == group.expected_returncode) all_details = [] if rc != group.expected_returncode: all_details.append(CompareDetail( field='returncode', expected=str(group.expected_returncode), actual=str(rc), match=False, )) return GroupResult( name=group.name, returncode=rc, passed=all_pass, details=all_details, ) # ── Java 文件式运行 + COBOL vs Java 出力比对(V3 扩展)── # 依赖 JavaSrc 工程:target/*.jar 已构建、src/*Main.java 主类可发现。 # 仅在满足发现条件时启用;其余程序不受影响。 def _find_java_exe() -> str: import shutil as _sh j = _sh.which('java') if j: return j for cand in ( 'C:/Program Files/Microsoft/jdk-11.0.32.101-hotspot/bin/java.exe', 'C:/Program Files/Java/jdk-11/bin/java.exe', 'C:/Program Files/Java/jdk-17/bin/java.exe', 'C:/Program Files/Eclipse Adoptium/jdk-11.0.21.9-hotspot/bin/java.exe', ): if os.path.exists(cand): return cand jh = os.environ.get('JAVA_HOME') if jh and os.path.exists(os.path.join(jh, 'bin', 'java.exe')): return os.path.join(jh, 'bin', 'java.exe') return 'java' def _build_java_classpath(java_src_dir: str) -> str: """通用化 classpath 拼装:target/classes + target/*.jar + lib/*.jar。 不依赖具体 artifact 名/manifest,任何含目标主类的编译产物都可通过 同一 classpath 命中(多程序共享一个 Maven 工程时也适用)。 """ import os as _os sep = ';' if _os.name == 'nt' else ':' parts = [] root = Path(java_src_dir) classes = root / 'target' / 'classes' if classes.is_dir(): parts.append(str(classes)) tgt = root / 'target' if tgt.is_dir(): parts.extend(str(p) for p in sorted(tgt.glob('*.jar'))) lib = root / 'lib' if lib.is_dir(): parts.extend(str(p) for p in sorted(lib.glob('*.jar'))) return sep.join(parts) def _scan_java_data_files(main_src: Path) -> dict: """扫描 Java 主类源码中引用的 `data/<文件名>` 常量。 返回 {文件基名大写: 完整文件名},例如 {'KIN01R01': 'KIN01R01.csv', 'KIN01W01': 'KIN01W01.txt'}。Java 桥据此按程序实际文件名准备/ 收集/比对入力与出力,而非一律假定 `.txt`(CSV 输入程序 如 KIN01INP 的入力文件为 `data/KIN01R01.csv`)。 """ file_map = {} if not main_src.is_file(): return file_map try: text = main_src.read_text(encoding='utf-8') except Exception: # noqa: BLE001 return file_map for m in re.finditer(r'["\'](?:\./)?data/([A-Za-z0-9_-]+)\.([A-Za-z0-9]+)["\']', text): base = m.group(1).upper() if base and base not in file_map: file_map[base] = f"{m.group(1)}.{m.group(2)}" return file_map def _java_file_name(java_cfg: dict, assign: str) -> str: """Java 侧实际使用的文件名:优先主类源码 data/ 常量,否则回退 `.txt`。""" fm = (java_cfg or {}).get('file_map') or {} return fm.get(assign.upper()) or f'{assign}.txt' def _discover_java(java_src_dir: str, program_name: str): """在 Java 工程中发现与 COBOL 程序对应的主类与可运行 classpath。 规则(对所有程序通用,非针对单本): - 主类:src 下 `XxxMain.java`,去 `Main` 后缀后大写 == 程序ID; - classpath:target/classes + target/*.jar + lib/*.jar(见 _build_java_classpath); - file_map:主类源码中 data/ 常量 → 实际入力/出力文件名; - 找不到主类或 classpath 为空时返回 None(调用方自动跳过)。 """ root = Path(java_src_dir) src = root / 'src' if not src.is_dir(): return None prog = str(program_name).upper() main_cls = None for f in src.glob('*Main.java'): stem = f.stem core = stem[:-4] if stem.endswith('Main') else stem if core.upper() == prog: main_cls = stem break if not main_cls: return None classpath = _build_java_classpath(java_src_dir) if not classpath: return None file_map = _scan_java_data_files(src / f'{main_cls}.java') return {'main_class': main_cls, 'classpath': classpath, 'root': str(root), 'file_map': file_map} def _fd_record_len(fd_field_dicts: dict, fd_name: str) -> int: """由 FD 叶子字段长度推算固定长记录字节数(unknown 组项跳过)。""" total = 0 for f in fd_field_dicts.get(fd_name, []) or []: pi = f.get('pic_info') or {} if pi.get('type') == 'unknown': continue total += pi.get('length') or (pi.get('digits', 0) + pi.get('decimal', 0)) or 0 return total def _slice_fixed_records(data: bytes, rlen: int) -> list[bytes]: if rlen <= 0: return [] out = [] i = 0 while i + rlen <= len(data): out.append(data[i:i + rlen]) i += rlen return out def _read_text_lines(path) -> list[str]: """读取 Java 侧每行一条记录的输出文件。""" p = Path(path) if not p.exists(): return [] text = p.read_text(encoding='utf-8') if text == '': return [] lines = text.split('\n') if lines and lines[-1] == '': lines.pop() return [ln.rstrip('\r') for ln in lines] def _output_fd_items(fd_field_dicts: dict, open_dir: dict, select_info: dict) -> list[tuple[str, str]]: """返回 [(fd_name, assign)],仅输出系 FD。""" items = [] seen = set() for fd_name in fd_field_dicts: direction = (open_dir or {}).get(fd_name, '') if direction not in ('OUTPUT', 'I-O'): continue sel = select_info.get(fd_name, {}) assign = sel.get('assign', fd_name) if isinstance(sel, dict) else fd_name if assign not in seen: seen.add(assign) items.append((fd_name, assign)) return items def _input_fd_assign_len(fd_field_dicts: dict, open_dir: dict, select_info: dict) -> list[tuple[str, int]]: """返回 [(assign, record_len)],仅输入系 FD(含 I-O)。""" out = [] seen = set() for fd_name in fd_field_dicts: direction = (open_dir or {}).get(fd_name, '') if direction not in ('INPUT', 'I-O'): continue sel = select_info.get(fd_name, {}) assign = sel.get('assign', fd_name) if isinstance(sel, dict) else fd_name if assign in seen: continue seen.add(assign) rlen = _fd_record_len(fd_field_dicts, fd_name) if rlen > 0: out.append((assign, rlen)) return out def _prepare_java_inputs(java_work: Path, src_in_dir: Path, input_items: list[tuple[str, int]], java_cfg: dict | None = None) -> None: data_dir = java_work / 'data' data_dir.mkdir(parents=True, exist_ok=True) for assign, rlen in input_items: src = Path(src_in_dir) / assign if not src.exists(): continue data = src.read_bytes() recs = _slice_fixed_records(data, rlen) lines = [r.decode('utf-8', 'replace') for r in recs] txt = '\n'.join(lines) if lines: txt += '\n' (data_dir / _java_file_name(java_cfg, assign)).write_text(txt, encoding='utf-8') def _run_java(java_cfg: dict, java_work: Path, log_path: Path) -> int: java_exe = _find_java_exe() cmd = [java_exe, '-cp', java_cfg['classpath'], java_cfg['main_class']] orig = os.getcwd() try: os.chdir(str(java_work)) p = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=90) except subprocess.TimeoutExpired: p = None finally: os.chdir(orig) if log_path: log_path.parent.mkdir(parents=True, exist_ok=True) if p is None: log_path.write_text('COMMAND: %s\nTIMEOUT' % ' '.join(cmd), encoding='utf-8') else: log_path.write_text( f"COMMAND: {' '.join(cmd)}\nRETURNCODE: {p.returncode}\n\n" f"STDOUT:\n{p.stdout}\n\nSTDERR:\n{p.stderr}", encoding='utf-8') return p.returncode if p is not None else -1 def _collect_java_outputs(java_work: Path, java_out_dir: Path, fd_field_dicts: dict, open_dir: dict, select_info: dict, java_cfg: dict | None = None) -> None: java_out_dir.mkdir(parents=True, exist_ok=True) for fd_name, assign in _output_fd_items(fd_field_dicts, open_dir, select_info): src = java_work / 'data' / _java_file_name(java_cfg, assign) if src.exists(): try: shutil.copy2(str(src), str(java_out_dir / _java_file_name(java_cfg, assign))) except OSError: pass def _compare_cobol_java(cobol_out_dir: Path, java_out_dir: Path, fd_field_dicts: dict, open_dir: dict, select_info: dict, java_cfg: dict | None = None) -> list[dict]: """逐 FD 比对 COBOL 出力(固定长字节)与 Java 出力(每行一条)。 返回每个输出 FD 的汇总 dict: {assign, cobol_count, java_count, matched, mismatched, samples} samples 为最多 3 条差异示例(含记录号与首个不同位置前后片段)。 """ summaries = [] for fd_name, assign in _output_fd_items(fd_field_dicts, open_dir, select_info): sel = select_info.get(fd_name, {}) org = sel.get('organization', 'SEQUENTIAL') if isinstance(sel, dict) else 'SEQUENTIAL' rlen = _fd_record_len(fd_field_dicts, fd_name) cobol_path = Path(cobol_out_dir) / assign if cobol_path.exists(): data = cobol_path.read_bytes() if str(org).upper() == 'LINE SEQUENTIAL': cobol_lines = [ln.rstrip('\n').rstrip('\r') for ln in data.decode('utf-8', 'replace').split('\n')] if cobol_lines and cobol_lines[-1] == '': cobol_lines.pop() else: cobol_lines = [r.decode('utf-8', 'replace') for r in _slice_fixed_records(data, rlen)] else: cobol_lines = [] java_lines = _read_text_lines( Path(java_out_dir) / _java_file_name(java_cfg, assign)) matched = 0 mismatched = 0 samples = [] n = max(len(cobol_lines), len(java_lines)) for i in range(n): c = cobol_lines[i] if i < len(cobol_lines) else '' j = java_lines[i] if i < len(java_lines) else '' if c == j: matched += 1 else: mismatched += 1 if len(samples) < 3: pos = next((k for k in range(min(len(c), len(j))) if c[k] != j[k]), min(len(c), len(j))) samples.append({ 'index': i, 'pos': pos, 'cobol': c[:60], 'java': j[:60], }) summaries.append({ 'assign': assign, 'cobol_count': len(cobol_lines), 'java_count': len(java_lines), 'matched': matched, 'mismatched': mismatched, 'samples': samples, }) return summaries def _java_scene_label(dst_out_dir: Path) -> str: """由 cobol 出力目录推导 java 场景目录名:cobol/main/output -> main。""" cobol_scene = Path(dst_out_dir).parent # .../cobol/main return cobol_scene.name def _run_java_scene(program_name: str, scene_id: str, outdir: str, temp_dir: str, src_in_dir: Path, dst_out_dir: Path, fd_field_dicts: dict, open_dir: dict, select_info: dict, java_cfg: dict, log_dir: str | None = None) -> dict: """单场景:准备 Java 入力 -> 运行 jar -> 收集出力 -> 与 COBOL 出力比对。""" label = _java_scene_label(dst_out_dir) java_out_dir = Path(outdir) / 'java' / label / 'output' java_work = Path(temp_dir) / f'java_{scene_id}' input_items = _input_fd_assign_len(fd_field_dicts, open_dir, select_info) if not input_items: logger.warning(f" Java[{label}]: 未发现输入 FD,跳过") return {'label': label, 'rc': -1, 'summaries': []} try: if java_work.exists(): shutil.rmtree(str(java_work)) _prepare_java_inputs(java_work, src_in_dir, input_items, java_cfg) log_path = Path(log_dir) / f'{program_name}_java_{label}.log' if log_dir else None rc = _run_java(java_cfg, java_work, log_path) _collect_java_outputs(java_work, java_out_dir, fd_field_dicts, open_dir, select_info, java_cfg) summaries = _compare_cobol_java(dst_out_dir, java_out_dir, fd_field_dicts, open_dir, select_info, java_cfg) except Exception as e: # noqa: BLE001 logger.warning(f" Java[{label}] 运行/比对失败: {e}") return {'label': label, 'rc': -1, 'summaries': [], 'error': str(e)} total_matched = sum(s['matched'] for s in summaries) total_mismatched = sum(s['mismatched'] for s in summaries) logger.info(f" Java[{label}]: rc={rc}, 出力比对 一致={total_matched} 不一致={total_mismatched}") return {'label': label, 'rc': rc, 'summaries': summaries} def _write_java_test_report(outdir: str, program_name: str, results: list, java_reports: list[dict]) -> None: """汇总多组(main/skip 等)COBOL vs Java 出力比对结果,生成中文测试报告。""" lines = [] lines.append(f"# {program_name} COBOL vs Java 出力比对测试报告") lines.append('') lines.append(f"- 程序ID: {program_name}") lines.append('- 报告类型: COBOL 出力 vs Java 出力 逐 FD 比对(多组出力分别比较)') lines.append('') lines.append('## 1. COBOL 执行结果(V3 cobol_testgen runner)') lines.append('') lines.append('| 组 | returncode | 判定 |') lines.append('|----|-----------|------|') for r in results: mark = '通过' if r.passed else '差异' lines.append(f"| {r.name} | {r.returncode} | {mark} |") lines.append('') lines.append('## 2. Java 执行与出力比对(逐组逐 FD)') lines.append('') for rep in java_reports: lines.append(f'### 2.{java_reports.index(rep) + 1} 组 `{rep["label"]}` (Java rc={rep.get("rc", "N/A")})') lines.append('') if rep.get('error'): lines.append(f'运行/比对异常: {rep["error"]}') lines.append('') continue if not rep['summaries']: lines.append('(无输出 FD 可比对)') lines.append('') continue lines.append('| 出力FD | COBOL记录数 | Java记录数 | 一致 | 不一致 | 判定 |') lines.append('|--------|------------|-----------|------|--------|------|') for s in rep['summaries']: verdict = '一致' if s['mismatched'] == 0 else '不一致' lines.append( f"| {s['assign']} | {s['cobol_count']} | {s['java_count']} " f"| {s['matched']} | {s['mismatched']} | {verdict} |") diff = [s for s in rep['summaries'] if s['mismatched'] > 0] if diff: lines.append('') lines.append('**差异示例(最多 3 条/FD)**:') for s in diff: for smp in s['samples']: lines.append(f"- {s['assign']}[记录{smp['index']}] 首异位置={smp['pos']}") lines.append(f" - COBOL: `{smp['cobol']}`") lines.append(f" - Java : `{smp['java']}`") lines.append('') all_fd = [s for rep in java_reports for s in rep.get('summaries', [])] total_match = sum(s['matched'] for s in all_fd) total_mis = sum(s['mismatched'] for s in all_fd) lines.append('## 3. 总结') lines.append('') if total_match + total_mis == 0: lines.append('- 无可比对的出力记录(或未发现 Java 对应程序)。') elif total_mis == 0: lines.append(f'- COBOL 与 Java 全部出力记录一致(一致 {total_match} 条,不一致 0 条)。') lines.append('- 判定: **通过**') else: lines.append(f'- COBOL 与 Java 出力存在差异:一致 {total_match} 条,不一致 {total_mis} 条。') lines.append('- 判定: **存在差异**,详见上文逐 FD 明细。') lines.append('') lines.append('> 覆盖率详情见 `coverage/` 目录 HTML 报告;Java 出力见 `java/<组>/output/`。') lines.append('') report_path = Path(outdir) / f'{program_name}_测试报告.md' report_path.write_text('\n'.join(lines), encoding='utf-8') logger.info(f" COBOL vs Java 测试报告: {report_path}") # ── 主编排 ── def run_all(program_name: str, outdir: str, temp_dir: str, fields_dict: list[dict], fd_fields: dict, select_info: dict, open_dir: dict, term_types: list[str], records: list[dict], expected_records: list[dict] | None = None, source_dir: str | None = None, path_infos: list | None = None, multi_write_fds: set | None = None, skip_records: list[dict] | None = None, skip_term_types: list[str] | None = None, java_src_dir: str | None = None ) -> tuple[list[GroupResult], dict[int, int] | None]: """完整编排:编译 → 准备目录 → 逐组执行 → 出力保存。 Returns: (results_list, merged_gcov_data) merged_gcov_data is None when no gcov runs. """ source_dir = source_dir or str(Path(outdir).parent) work_dir = Path(temp_dir).resolve() work_dir.mkdir(parents=True, exist_ok=True) expected = expected_records or [] path_infos = path_infos or [] multi_write_fds = multi_write_fds or set() fd_field_dicts = _build_fd_field_dicts(fd_fields, fields_dict) assign_names = _input_assign_names(select_info, open_dir, fd_fields) log_dir = os.path.join(outdir, 'logs') def _is_output_fd(fd_name: str) -> bool: dir_val = open_dir.get(fd_name, '') return dir_val in ('OUTPUT', 'I-O') # ── 1. SUB 编译(V3)── sub_dir = _resolve_sub_dir(source_dir) cpy_dir = _resolve_cpy_dir(source_dir) sub_o = compile_sub_modules(sub_dir, str(work_dir), cpy_dir, log_dir=log_dir) # ── 2. 主程序编译(V3)── exe_path = compile_program( program_name, source_dir, str(work_dir), sub_o, cpy_dir, log_dir=log_dir ) # ── 3. 场景定义 ── scenes = [("main", records, term_types, expected, Path(outdir) / 'cobol' / 'main' / 'input', Path(outdir) / 'cobol' / 'main' / 'output')] if skip_records: skip_expected = [{}] * len(skip_records) skip_term = skip_term_types or ['normal'] * len(skip_records) scenes.append(("skip", skip_records, skip_term, skip_expected, Path(outdir) / 'cobol' / 'run_skip' / 'input', Path(outdir) / 'cobol' / 'run_skip' / 'output')) results = [] gcov_data_sets = [] gcov_root = work_dir / "gcov" # ── 2.5 Java 工程发现(可选,无对应 Java 程序时整体跳过)── java_reports = [] java_cfg = None if str(os.environ.get('COBOL_JAVA_RUN', '1')).lower() not in ('0', 'off', 'false'): _jsrc = (java_src_dir or os.environ.get('COBOL_JAVA_SRC_DIR') or str(Path(source_dir).parent / 'JavaSrc')) java_cfg = _discover_java(_jsrc, program_name) if java_cfg: logger.info(f" 发现 Java 对应程序: {java_cfg['main_class']}") else: logger.info(f" 未发现 Java 对应程序({_jsrc}),跳过 Java 运行/比对") for scene_id, scene_recs, scene_terms, scene_expected, src_in_dir, dst_out_dir in scenes: # ── 3a. 入力ファイル配置(主程序 + 被调子程序的输入文件全部复制)── # 仅复制 assign_names 会漏掉子程序输入文件(测试驱动调用读文件自程序时 # 会 OPEN 失败 ABEND)。复制目录内全部非 JSON 文件(.json 为 V3 元数据)。 if src_in_dir.is_dir(): for src in sorted(src_in_dir.iterdir()): if not src.is_file() or src.suffix.lower() == '.json': continue shutil.copy2(str(src), str(work_dir / src.name)) # ── 3b. 清理旧 gcda ── _clean_gcda(str(work_dir)) # ── 3c. 过滤 non-abend ── filtered_exp = [] normal_recs = [] abend_pairs = [] # (rec, command_args) for i, rec in enumerate(scene_expected): term = scene_terms[i] if i < len(scene_terms) else 'normal' if term != 'abend': filtered_exp.append(rec) for i, rec in enumerate(scene_recs): term = scene_terms[i] if i < len(scene_terms) else 'normal' if term == 'abend': abend_pairs.append((rec, list((rec.get('__CLI_ARGS__') or {}).values()))) else: normal_recs.append(rec) group = GroupInfo( name=f"{program_name}_{scene_id}", records=normal_recs, expected_outputs=filtered_exp, expected_returncode=0, fd_field_dicts=fd_field_dicts, open_dir=open_dir, select_info=select_info, multi_write_fds=multi_write_fds, ) # ── 3d. 执行 ── r = run_group(group, exe_path, str(work_dir), log_dir=log_dir) results.append(r) status = '✓' if r.passed else '✗' logger.info(f" 组 '{group.name}': returncode={r.returncode}, {status}") # ── 3d-2. abend 记录单独执行(带命令行参数,触发 ABEND/异常返回)── for ab_idx, (ab_rec, ab_args) in enumerate(abend_pairs): ab_group = GroupInfo( name=f"{program_name}_{scene_id}_abend_{ab_idx + 1}", records=[ab_rec], expected_outputs=[], expected_returncode=1, fd_field_dicts=fd_field_dicts, open_dir=open_dir, select_info=select_info, multi_write_fds=multi_write_fds, command_args=ab_args, ) r2 = run_group(ab_group, exe_path, str(work_dir), log_dir=log_dir) results.append(r2) status2 = '✓' if r2.passed else '✗' logger.info(f" 组 '{ab_group.name}': returncode={r2.returncode}, {status2}") # ── 3e. 出力保存 ── dst_out_dir.mkdir(parents=True, exist_ok=True) for fd_name in fd_field_dicts: if not _is_output_fd(fd_name): continue sel = select_info.get(fd_name, {}) assign = sel.get('assign', fd_name) if isinstance(sel, dict) else fd_name src = os.path.join(str(work_dir), assign) if os.path.exists(src): shutil.copy2(src, str(dst_out_dir / assign)) # ── 3f. .gcda 隔离(.gcno 是共享的,COPY;.gcda 是每场景独立的,MOVE) scene_gcov_dir = gcov_root / f"run_{scene_id}" scene_gcov_dir.mkdir(parents=True, exist_ok=True) for f in work_dir.glob("*.gcda"): if f.is_file() and f.stat().st_size > 0: dst = scene_gcov_dir / f.name if dst.exists(): dst.unlink() shutil.move(str(f), str(dst)) for f in work_dir.glob("*.gcno"): if f.is_file() and f.stat().st_size > 0: dst = scene_gcov_dir / f.name shutil.copy2(str(f), str(dst)) # ── 3g. 收集该场景的 gcov 数据 ── from .gcov import run_gcov as _run_gcov scene_data = _run_gcov(program_name, str(scene_gcov_dir)) if scene_data: gcov_data_sets.append(scene_data) logger.info(f" {scene_id} 完了, output={dst_out_dir}") # ── 3h. Java 文件式运行 + COBOL vs Java 出力比对(仅在有 Java 对应程序时)── if java_cfg: jrep = _run_java_scene( program_name, scene_id, outdir, str(work_dir), src_in_dir, dst_out_dir, fd_field_dicts, open_dir, select_info, java_cfg, log_dir=log_dir, ) java_reports.append(jrep) # ── 4. 合并 gcov ── merged_gcov = None if gcov_data_sets: merged_gcov = {} for ds in gcov_data_sets: for line, count in ds.items(): merged_gcov[line] = max(merged_gcov.get(line, 0), count) logger.info(f" Merged gcov from {len(gcov_data_sets)} runs ({len(merged_gcov)} lines)") # ── 5. 生成 COBOL vs Java 出力比对测试报告 ── if java_cfg and java_reports: try: _write_java_test_report(outdir, program_name, results, java_reports) except Exception as e: # noqa: BLE001 logger.warning(f" 测试报告生成失败: {e}") return results, merged_gcov # ── run_and_compare(被 --run 调用,SOURCE 兼容)── def run_and_compare(program_name: str, outdir: str, fields_dict: list[dict], fd_fields: dict, select_info: dict, open_dir: dict, term_types: list[str], records: list[dict]) -> dict: """旧版接口兼容包装。返回 {normal_pass, normal_count, ...}。""" fd_field_dicts = _build_fd_field_dicts(fd_fields, fields_dict) normal_recs = [] abend_recs = [] for i, term in enumerate(term_types): if term == 'abend' and i < len(records): abend_recs.append(records[i]) elif i < len(records): normal_recs.append(records[i]) result = { 'normal_pass': False, 'normal_count': 0, 'abend_pass': 0, 'abend_total': len(abend_recs), 'output_summary': {}, } temp_dir = os.path.join(outdir, '.run_cache') source_dir = os.path.join(outdir, '..', 'input') log_dir = os.path.join(outdir, 'logs') work_dir = Path(temp_dir) work_dir.mkdir(parents=True, exist_ok=True) _clean_gcda(temp_dir) sub_dir = _resolve_sub_dir(source_dir) cpy_dir = _resolve_cpy_dir(source_dir) sub_o = compile_sub_modules(sub_dir, temp_dir, cpy_dir, log_dir=log_dir) exe_path = compile_program(program_name, source_dir, temp_dir, sub_o, cpy_dir, log_dir=log_dir) if normal_recs: group = GroupInfo( name='normal', records=normal_recs, expected_outputs=[], fd_field_dicts=fd_field_dicts, open_dir=open_dir, select_info=select_info, ) r = run_group(group, exe_path, temp_dir, log_dir=log_dir) result['normal_pass'] = (r.returncode == 0) result['normal_returncode'] = r.returncode for fd_name in fd_field_dicts: sel = select_info.get(fd_name, {}) assign = sel.get('assign', fd_name) if isinstance(sel, dict) else fd_name outpath = os.path.join(temp_dir, assign) fds = fd_field_dicts.get(fd_name, []) org = sel.get('organization', 'SEQUENTIAL') if isinstance(sel, dict) else 'SEQUENTIAL' line_seq = (org == 'LINE SEQUENTIAL') try: recs = file_io.read_output_file(outpath, fds, line_sequential=line_seq) result['output_summary'][fd_name] = len(recs) except Exception: result['output_summary'][fd_name] = -1 for i, rec in enumerate(abend_recs): group = GroupInfo( name=f'abend_{i+1}', records=[rec], expected_outputs=[], expected_returncode=1, fd_field_dicts=fd_field_dicts, open_dir=open_dir, select_info=select_info, command_args=list((rec.get('__CLI_ARGS__') or {}).values()), ) r = run_group(group, exe_path, temp_dir, log_dir=log_dir) if r.returncode != 0: result['abend_pass'] += 1 return result