Files
cobol-java-v3/cobol_testgen/runner.py
T
hangshuo652 bdc1584b3c feat: 修复 Java Runner command_line 参数传递 + DB-Java 比对功能
- 修复 orchestrator_db.py: Java Runner 未传递 command_line 参数导致 ABEND
- 新增 DB-Java 文件式运行 + DB 表比对功能
- 优化输出目录结构: output/<PROGRAM_ID>/cobol/
- 新增测试文件: test_java_comparison.py, test_java_e2e.py
- 更新 AI 使用日志
2026-09-09 21:35:21 +08:00

1063 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""非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 桥据此按程序实际文件名准备/
收集/比对入力与出力,而非一律假定 `<assign>.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/ 常量,否则回退 `<assign>.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
- classpathtarget/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 '<MISSING>'
j = java_lines[i] if i < len(java_lines) else '<MISSING>'
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