- 修复 orchestrator_db.py: Java Runner 未传递 command_line 参数导致 ABEND - 新增 DB-Java 文件式运行 + DB 表比对功能 - 优化输出目录结构: output/<PROGRAM_ID>/cobol/ - 新增测试文件: test_java_comparison.py, test_java_e2e.py - 更新 AI 使用日志
1063 lines
39 KiB
Python
1063 lines
39 KiB
Python
"""非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;
|
||
- 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 '<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
|