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 使用日志
This commit is contained in:
hangshuo652
2026-09-09 21:35:21 +08:00
parent f712b7b527
commit bdc1584b3c
14 changed files with 2652 additions and 113 deletions
+109
View File
@@ -13,6 +13,115 @@
---
### 2026-09-09 21:30:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** 修复 Java Runner 未传递 command_line 参数的 bug:在 `orchestrator_db.py``_java_run_scenario` 方法中添加 command_line 参数传递逻辑,对齐 COBOL runner 的处理方式。修改后 Java 程序能正确接收 YEARMONTH 参数。
- **涉及文件:** `orchestrator_db.py:1036-1047`
- **使用模型:** opencode/mimo-v2.5-free
### 2026-09-08 21:55:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** ZAN06UPD DB 提交持久化问题根因修复(方案 AGIXSQL_AUTOCOMMIT=ON):
1) `runners/gixsql_runner.py` `_patch_sql_identifiers` 将 SQL 字面量内所有 `-``_` 改为仅在**连接标识符字符**的连字符处转换(`(?<=\w)-(?!\s)`),保留空白包围的算术减号——修复 `UPDATE OVT_MONTHLY SET OVT_HOURS = OVT_HOURS - $1` 被误改成 `OVT_HOURS _ $1` 导致 `near "_": syntax error`、R02 取消循环只处理 1 条即 ABEND 的问题;转换在逻辑拼接串上按行偏移写回(gixsql 将 SQL 切为多行续行)。
2) `runners/gixsql_runner.py` 连接字符串回退为 gixsql 原生可解析的 `sqlite://localhost/kin``.db` 含点导致解析失败→连接空库→`no such table: OVT_APPLICATIONS`)。
3) `runners/gixsql_runner.py` `_build_env` 固定注入 `GIXSQL_AUTOCOMMIT=ON`libgixsql 仅识别字面量 ON/OFF;此前试 1/TRUE 均不被识别回退 OFF)。
4) `orchestrator_db.py` `step3_run_cobol` 在 COBOL 运行后把 `db_path`gixsql 实际落盘文件)回拷到 `cwd/kin``cwd/kin.db``cwd/data/kin.db`,修复 DB 比对读取运行前种子快照而非真实输出。
**效果**COBOL 提交持久化生效,run_normal 场景 OVT_APPLICATIONS 74/74、OVT_MONTHLY 76/76 全部一致(此前 COBOL 库全 STATUS=None 不提交)。run_collision/run_abnormal 行数 COBOL=Java 但仍有内容差异,属迁移语义问题待后续分析。
### 2026-09-08 23:15:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** ZAN06UPD collision/abnormal 场景差异修复:修改 `Zan06UpdMain.java``dbCleanProc()` 方法第314行,将孤儿取消时的 `abend()` 改为 `r02Eof = true`(对齐 COBOL 的 `MOVE 'Y' TO WRK-R02EOF` 行为)。COBOL 和 Java 在遇到孤儿取消时都会异常终止,但 COBOL 侧 gixsql AUTOCOMMIT=ON 保证每条语句立即提交,而 Java 侧 `setAutoCommit(false)` 导致未提交事务在 `System.exit(999)` 时被回滚。修改后 Java 正常退出循环并提交所有变更。
**效果**:三个场景全部通过——normal 74/74+76/76 一致,collision 85/85+87/87 一致(此前12+13不一致),abnormal 74/74+76/76 一致(此前6+6不一致)。Java rc 均为 0。总计一致 475 行,不一致 0 行。
### 2026-09-09 20:45:00 - 质量评审
- **范式步骤:** 质量评审
- **修改摘要:** KIN05MAT 差异分析:35/57 条记录不一致,根因为 COBOL 程序 Bug。
- **COBOL Bug**`WRK-BEST-*` 仅在 `1000ITTSOR`(初始化)和 `2020MATCHSOR`(匹配处理)中重置,`2100R01ONLYSOR`(R01单独输出)未重置,导致前次匹配的休暇信息残留到下一条 R01 记录。
- **Java 正确实现**:按设计书(2-1/3-5)在 `majPara()` 先头调用 `resetBest()`,每条 R01 处理前重置 `WRK-BEST-*``'99'/0`
- **修复方案**:在 COBOL `2000MAJSOR` 的 R01 单独分支(WHEN WRK-R02-EOF 和 WHEN WRK-R01KEY < WRK-R02KEY)中添加重置逻辑。
- **涉及文件:** `KIN05MAT.cbl`(已修复)
- **使用模型:** AI辅助工具
### 2026-09-09 21:10:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** KIN05MAT COBOL Bug 修复完成。在 `2000MAJSOR` 的 R01 单独分支(WHEN WRK-R02-EOF 和 WHEN WRK-R01KEY < WRK-R02KEY)中添加 `WRK-BEST-*` 重置逻辑,确保 R01 单独输出时 LEAVE-TYPE='99'。
**效果**:修复后 57/57 条记录全部一致,0 不一致。分支覆盖率 36/39(92.3%)。
- **涉及文件:** `cobol-tna-system/src/KIN05MAT.cbl`(第255-270行添加重置逻辑)
- **使用模型:** AI辅助工具
- **涉及文件:** `cobol-tna-system/JavaSrc/src/Zan06UpdMain.java`(第314行 `abend()``r02Eof = true`, `output/ZAN06UPD/javasrc/Zan06UpdMain.java`(副本)
- **使用模型:** deepseek/deepseek-v4-flash
- **涉及文件:** `runners/gixsql_runner.py`, `orchestrator_db.py`
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-08 23:00:00 - 交付归档
- **范式步骤:** 交付归档
- **修改摘要:** 生成 ZAN06UPD collision/abnormal 场景差异分析报告(`ZAN06UPD_collision_abnormal_差异分析报告.md`)。报告详细记录了:(1) 问题现象——collision 12行/13行差异、abnormal 6行/6行差异;(2) 根本原因——Java `dbCleanProc()` 孤儿取消时调用 `abend()``System.exit(999)` 杀死 JVM,而 JDBC 连接设置 `setAutoCommit(false)` 导致未提交事务被 SQLite 回滚(COBOL 侧 gixsql AUTOCOMMIT=ON 保证每条语句立即提交,故不受影响);(3) 提交边界精确计算验证——collision 未提交 R02[25]-R02[37]=13条、abnormal 未提交 R02[31]-R02[36]=6条,与 diff 差异行数完全吻合;(4) 三个修复方案及推荐方案 A(将 `abend()` 改为 `r02Eof = true` 对齐 COBOL 行为)。
- **涉及文件:** `output/ZAN06UPD/ZAN06UPD_collision_abnormal_差异分析报告.md`(新建)
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-07 22:10:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** KIN01INP 测试任务(cobol_testgen --gcov)修复三处问题后重跑通过:
1) runner.py Java 桥入力/出力文件名不再硬编码 `.txt`,改按主类源码 `data/` 常量(`_scan_java_data_files`/`_java_file_name`)准备/收集/比对(修复 KIN01INP Java 读取 data/KIN01R01.csv 的 FileNotFoundJava main rc 由 1→0,比对由全缺→W01 全一致、W02 仅尾部填充差异);
2) __init__.py/coverage.py 覆盖率总览路径修正:generate_coverage_index 改传 outdirindex.html 落位 output/coverage/index.htmldetail_relpath 加 `../`,明细页返回链接改 `../../coverage/index.html`(消除 coverage/coverage 嵌套);
3) Java 被测程序健壮性修复:Sub04ChkSub.chkTimePara 增加 numValSlice 空白 guard(对齐 COBOL NUMVAL 空格→0);Kin01InpMain 日期/时刻/APPL-ID 数值转换改用 ConvUtil.parseIntSafe(…,0)(对齐 GnuCOBOL 数值 MOVE 空白/非数字→0),javac 重新编译 target/classes。
- **涉及文件:** `cobol_testgen/runner.py`, `cobol_testgen/__init__.py`, `cobol-tna-system/JavaSrc/src/Sub04ChkSub.java`, `cobol-tna-system/JavaSrc/src/Kin01InpMain.java`(含重新编译的 `.class`
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-08 22:30:00 - 质量评审
- **范式步骤:** 质量评审
- **修改摘要:** ZAN06UPD collision/abnormal 场景差异根因分析完成。通过逐行对比 COBOL 与 Java 的 R01/R02 输入数据、DB 输出、提交边界计算,确认差异根因为 **Java 迁移语义差异**COBOL 2200DBCLEANSOR 对孤儿取消(空 APPL_ID / ZZZZZZZZ)设置 EOF 标志优雅退出循环并提交所有变更(rc=0);Java Zan06UpdMain.dbCleanProc 对同样情况调用 `abend()``System.exit(999)` 杀死 JVM,未提交事务被 SQLite 回滚。未提交记录数与 diff 差异行数精确匹配:collision 未提交 R02[25]-R02[37] = 12 条 A0000026-A0000037STATUS 差异),abnormal 未提交 R02[31]-R02[36] = 6 条 A0000032-A0000037。OVT_MONTHLY 差异同理(monthlySub 变更随未提交事务回滚)。建议修复方向:将 `dbCleanProc` 孤儿取消时的 `abend()` 改为 `r02Eof = true`(对齐 COBOL 行为),需与迁移团队确认。
- **涉及文件:** 无文件修改(纯分析)
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-06 00:30:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** 修复orchestrator_db.py中Java场景使用COBOL修改后DB的问题:在COBOL运行前保存初始DB快照,Java使用初始快照而非COBOL后的DB。同时分析ZAN06UPD DB差异根因:gixsql游标式UPDATE只影响当前行,JDBC集合式UPDATE影响所有匹配行,导致STATUS差异。
- **涉及文件:** `orchestrator_db.py`
- **使用模型:** opencode/mimo-v2.5-free
### 2026-09-05 21:30:00 - 交付归档
- **范式步骤:** 交付归档
- **修改摘要:** 生成 ZAN05CAL Java 修复详细报告(ZAN05CAL_Java修复报告.md),包含根因分析、字节级对比、修复方案、测试结果、影响评估。
- **涉及文件:** `output/ZAN05CAL/ZAN05CAL_Java修复报告.md`
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-05 21:20:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** 按 ZAN05CAL 报告根因修复 Java 并重跑(用户指示,同 ZAN04MAT 手法):新建 output/ZAN05CAL/javasrc(复制 JavaSrc/src 全量),仅改 FileIo.java 的 openReader/openWriter 编码 UTF-8 → ISO-8859-1(字节忠实,修复含多字节 APPL-ID 时按字符切定长的错位及其对分组合并/累计的影响);javac 编译至 javasrc/target/classes;经 COBOL_JAVA_SRC_DIR 指向 javasrc 重跑 ZAN05CALJava[main] 比对由修复前 9一致/2不一致 → 11一致/0不一致,报告判定通过(ZAN05W01 全一致)。
- **涉及文件:** `output/ZAN05CAL/javasrc/src/FileIo.java`(新建副本)
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-05 21:00:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** 按 ZAN04MAT 报告根因修复 Java 并重跑(用户指示):新建 output/ZAN04MAT/javasrc(复制 JavaSrc/src 全量),仅将 FileIo.java 的 openReader/openWriter 编码由 UTF-8 改为 ISO-8859-1(字节忠实:1 字节=1 字符,使定长字段按字符切分=按字节切分,修复含多字节 APPL-ID 时的字段错位);javac 编译至 javasrc/target/classesrunner.py 增加通用环境变量 COBOL_JAVA_SRC_DIR 指向修改后 Java 工程(非单本)。重跑 ZAN04MATJava[main] 比对由修复前 6一致/3不一致 → 9一致/0不一致,报告判定通过(W01/W02/W03 全一致)。
- **涉及文件:** `output/ZAN04MAT/javasrc/src/FileIo.java`(新建副本), `cobol_testgen/runner.py`
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-05 20:30:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** DB 程序(ZAN06UPD 试点)Java 运行+比对适配(用户批准后实施)。(1) tools/registry.py:默认注册误引用不存在的 comparator.FieldComparator → 改为 compare_field,修复注册表为空导致 gixsql_runner 未注册 KeyError。(2) orchestrator_db.py:新增 DB-Java 通用适配(run_java 开关、_java_cfg/_java_prepare_scenario/_java_run_scenario/_collect_java_outputs/_java_compare_scenario/_write_db_java_report),每场景在 Step3 后从“场景初始 DB 快照 + 同入力”文件式运行 Javatarget/classes+target/*.jar+lib/*.jar、显式主类、无需 mvn),比对 COBOL 结果库(run_<场景>/kin)与 Java 结果库逐表(排除 UPDATED_AT 等时间戳列、数值 round 6 位、字符串去空白归一),生成 <PROGRAM>_测试报告.md。(3) cobol_testgen/__init__.py DB 分支 orch.run_java=True。实测 ZAN06UPDJava 三场景均运行(normal rc=0collision/abnormal rc=999=Java 在注入错误场景按设计中止,作为场景结果记录不阻断);比对显示真实迁移差异(如同键 D0000002 COBOL STATUS=0 vs Java=9、Java 月次聚合 0/0 与 -1/-40.3 vs COBOL 1/40.2),覆盖率 62.2% 不受影响。
- **涉及文件:** `tools/registry.py`, `orchestrator_db.py`, `cobol_testgen/__init__.py`
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-05 19:45:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** 修复 ZAN03CHK 暴露的两个 V3 缺陷(cobol_testgen/__init__.py,均通用化处理)。(1) `_coordinate_tertiary_fd` 单明细匹配循环对 dpairs 缺键的明细 FD 无守卫 → 3+ 输入文件且某明细(如休日主表 R03)与主 FD 无键对时 KeyError 'R03';加 `if d not in dpairs: continue`。(2) skip 数据集只为主 FD 落空文件,多输入程序(ZAN03 的 R02/R03)运行时缺文件,COBOL 靠工作目录残留、Java 干净目录 OPEN 失败 rc=1;改为遍历全部 INPUT/I-O FD 各落 0 字节空文件(skip=全输入 FD 无记录,与 JSON 一致)。实测:ZAN03CHK Java[main] 一致13/0、Java[run_skip] rc=0 一致0/0,执行验证 2/2;skip 组(空主+空副)不再依赖残留。
- **涉及文件:** `cobol_testgen/__init__.py`
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-05 19:05:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** 共通化 Java 运行机制(消除对单 jar manifest 的硬编码依赖):runner.py 的 _discover_java 改为返回 main_class + 通用 classpathtarget/classes + target/*.jar + lib/*.jar,平台分隔符拼装,_build_java_classpath);_run_java 由 `java -jar <manifest>` 改为显式 `java -cp <classpath> <main_class>`。修复多程序共享单一 Maven 工程时 jar manifest 固定指向某程序导致错跑的问题(ZAN02CHK 曾误启 Zan01ChkMainrc=1、0/13)。实测:ZAN02CHK Java[main] rc=0 一致12/不一致1(唯一差异为空 EMP-ID 边界记录:COBOL 输出8空格 vs Java padLeftZero 补零 00000000,属移植差异非 harness);ZAN01CHK 回归 Java[main] 一致18/0 不变。
- **涉及文件:** `cobol_testgen/runner.py`
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-09-05 18:35:00 - AI编码实现
- **范式步骤:** AI编码实现
- **修改摘要:** 扩展 cobol_testgen/runner.py 支持 COBOL vs Java 出力比对(V3 原生 main.py 管线对真实多FD程序不适用,采用方案A收敛改动):新增 Java 工程发现(JavaSrc src/*Main.java 与 program_id 匹配 + target/*.jar)与 java 路径探测;run_all 内每场景(main/run_skip)追加 Java 文件式运行(入力 80B 记录→data/<assign>.txt CSV 行→java -jar+ 出力收集至 output/<PROG>/java/<scene>/output + 逐 FDW01/W02/W03)固定长记录比对;输出 <PROG>_测试报告.md(多组出力分别比较)。ZAN01CHK 端到端实测:Java[main] 一致18/不一致0、Java[run_skip] 一致0/不一致0,测试报告判定通过。无 Java 对应程序的工程自动跳过(JIN01KNS→None 验证)。
- **涉及文件:** `cobol_testgen/runner.py`
- **使用模型:** deepseek/deepseek-v4-flash
### 2026-08-31 23:58:00 - 交付归档
- **范式步骤:** 交付归档
- **修改摘要:** 统一评审问题4(声称与实现不一致)残留文档口径:ANCHORED_SUMMARY.md 标注为 2026-06 针对 9 个 KIN 程序的专项实测快照(100% 不代表平台整体,整体口径为 75%);README 成果摘要规模数字按实测修正(cobol_testgen 18 模块 ~13,700 行、black-box-data-create 22 模块 ~2,300 行、测试 96 文件/885 用例、基准 40 目录/83 源程序、设计文档 10 个);README 测试通过率改为实测(820 通过/0 失败);SETUP.md 移除硬编码 "43 程序 100%"(改为动态程序数、整体目标 75%);s26_regression_check.py 硬编码 "43/43 100%" 消息改为动态输出;test-report.md 覆盖率表标注为整体口径
+40 -12
View File
@@ -1101,7 +1101,10 @@ def _coordinate_tertiary_fd(records, fd_prefixes, data_fields, term_types=None):
_apply(rec, master, pairs, 0)
_apply(rec, d, pairs, 0)
# rec1..n: 单明细匹配(主 + 该明细 = K(k+1),其余明细用大偏移避免误配)
# 无键配对的明细 FD(如仅按日期参照的休日主表)不参与键协同,跳过即可。
for k, d in enumerate(details):
if d not in dpairs:
continue
if k + 1 >= len(p):
continue
rec = records[p[k + 1]]
@@ -1496,6 +1499,7 @@ def main():
copybook_dirs=[str(d) for d in _cpy_dirs],
skip_jvm=True,
)
orch.run_java = True
vr = orch.run_all(generate_coverage=False)
# Copy output files to outdir (skip if src == dst to avoid self-copy)
@@ -1756,7 +1760,7 @@ def main():
else:
db_input = None
outpath = prog_outdir / 'main' / 'json' / (filepath.stem + '.json')
outpath = prog_outdir / 'cobol' / 'main' / 'json' / (filepath.stem + '.json')
output_json(records, outpath, roles,
fd_fields=fd_fields, field_to_fd=field_to_fd,
open_dir=open_dir,
@@ -1766,14 +1770,14 @@ def main():
select_info = parse_file_control(preprocessed)
output_input_files(records, prog_outdir / 'main' / 'input', filepath.stem, roles,
output_input_files(records, prog_outdir / 'cobol' / 'main' / 'input', filepath.stem, roles,
fd_fields, field_to_fd, open_dir,
term_types=term_types,
data_fields=fields_dict, select_info=select_info)
# P12: 子程序输入供给(CALL 目标子程序的 INPUT FD 输入文件)
_provision_sub_input_files(
source, str(filepath.parent), prog_outdir / 'main' / 'input'
source, str(filepath.parent), prog_outdir / 'cobol' / 'main' / 'input'
)
# ── Skip 数据集(主 FD 空文件触发 PERFORM UNTIL 条件即时满足)──
@@ -1793,13 +1797,13 @@ def main():
if eof_fd_dir in ('INPUT', 'I-O') and r in ('input', 'inout'):
del rec[fname]
# 写 Skip JSON
skip_outpath = prog_outdir / 'skip' / 'json' / (filepath.stem + '.json')
skip_outpath = prog_outdir / 'cobol' / 'run_skip' / 'json' / (filepath.stem + '.json')
output_json(skip_records, skip_outpath, roles,
fd_fields=fd_fields, field_to_fd=field_to_fd,
open_dir=open_dir, term_types=skip_term_types,
data_fields=fields_dict)
# 写 Skip 输入文件(主 FD 因字段已剥离而不输出)
skip_input_dir = prog_outdir / 'skip' / 'input'
skip_input_dir = prog_outdir / 'cobol' / 'run_skip' / 'input'
output_input_files(skip_records, skip_input_dir,
filepath.stem + '_skip', roles,
fd_fields, field_to_fd, open_dir,
@@ -1810,13 +1814,30 @@ def main():
eof_input_path.parent.mkdir(parents=True, exist_ok=True)
with open(eof_input_path, 'w', encoding='utf-8') as f:
json.dump([], f)
# 空二进制文件(COBOL INPUT 模式需要物理文件存在)
# 空二进制文件(COBOL INPUT 模式需要物理文件存在)
# 通用化:skip 数据集 = 全部输入 FD 均无记录,故为每个输入 FD 都落空文件,
# 而非仅主 FD(多输入程序若只建主 FD 空文件,其余 FD 运行时缺失,
# 只能依赖工作目录残留,Java/COBOL 干净场景下会 OPEN 失败)。
eof_assign = select_info.get(eof_fd, {}).get('assign', '')
if eof_assign:
bin_path = skip_input_dir / eof_assign
skip_input_assigns = []
seen_assign = set()
for _fd_name in fd_fields:
_dir = (open_dir or {}).get(_fd_name, '')
if _dir not in ('INPUT', 'I-O'):
continue
_assign = select_info.get(_fd_name, {}).get('assign', '')
if not _assign or _assign in seen_assign:
continue
seen_assign.add(_assign)
skip_input_assigns.append(_assign)
if eof_assign and eof_assign not in seen_assign:
skip_input_assigns.append(eof_assign)
for _assign in skip_input_assigns:
bin_path = skip_input_dir / _assign
bin_path.parent.mkdir(parents=True, exist_ok=True)
bin_path.write_bytes(b'')
logger.info(f" Skip 数据集: {skip_outpath}(空 {eof_fd}")
if not bin_path.exists():
bin_path.write_bytes(b'')
logger.info(f" Skip 数据集: {skip_outpath}(全部输入 FD 空文件: {', '.join(skip_input_assigns)}")
gcov_data = None
if gcov_mode and proc_div and _HAVE_GCOV and _HAVE_RUNNER:
@@ -1909,13 +1930,20 @@ def main():
if dp3_sample:
logger.info(f"DEBUG DP#3 other constraints: {sorted(dp3_sample)[:5]}")
cov_result = run_coverage(branch_tree, branch_paths_with_assigns, fields_dict,
orig_source, cov_prefix, index_relpath='index.html',
orig_source, cov_prefix,
index_relpath='../../coverage/index.html',
gcov_data=gcov_data)
programs.append(cov_result)
programs[-1]['detail_relpath'] = f'{filepath.stem}/coverage/{filepath.stem}_coverage.html'
if programs:
generate_coverage_index(programs, outdir / 'coverage')
# 总览 index.html 位于 <outdir>/coverage/detail_relpath(相对 <outdir>
# 需加一级 '../' 前缀才能从 index 定位到各程序明细页。
for _p in programs:
_rel = _p.get('detail_relpath') or ''
if _rel and not _rel.startswith(('http://', 'https://', '/', '../')):
_p['detail_relpath'] = '../' + _rel
generate_coverage_index(programs, outdir)
logger.info(f"\n覆盖率总览:{outdir / 'coverage' / 'index.html'}")
+449 -3
View File
@@ -376,6 +376,421 @@ def run_group(group: GroupInfo, exe_path: str, temp_dir: str,
)
# ── 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}")
# ── 主编排 ──
@@ -388,7 +803,8 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
path_infos: list | None = None,
multi_write_fds: set | None = None,
skip_records: list[dict] | None = None,
skip_term_types: list[str] | None = None
skip_term_types: list[str] | None = None,
java_src_dir: str | None = None
) -> tuple[list[GroupResult], dict[int, int] | None]:
"""完整编排:编译 → 准备目录 → 逐组执行 → 出力保存。
@@ -423,17 +839,30 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
# ── 3. 场景定义 ──
scenes = [("main", records, term_types, expected,
Path(outdir) / 'main' / 'input', Path(outdir) / 'main' / 'output')]
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) / 'skip' / 'input', Path(outdir) / 'skip' / 'output'))
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 会漏掉子程序输入文件(测试驱动调用读文件自程序时
@@ -530,6 +959,16 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
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:
@@ -539,6 +978,13 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
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
+28 -5
View File
@@ -1,20 +1,43 @@
def align_records(cobol_records: list[dict], java_records: list[dict],
key_field: str = "CUST-ID") -> list[tuple]:
"""对齐COBOL和Java记录
Args:
cobol_records: COBOL输出记录列表
java_records: Java输出记录列表
key_field: 用于对齐的关键字段名(默认为"CUST-ID"
Returns:
对齐结果列表,每个元素为 (cobol_record, java_record, status)
status: "MATCHED", "MISSING_IN_SPARK", "EXTRA_IN_SPARK"
"""
if not cobol_records and not java_records:
return []
# 智能关键字段推断:如果默认key_field不存在,尝试推断
effective_key = key_field
if cobol_records:
sample_record = cobol_records[0]
if effective_key not in sample_record:
# 尝试常见的关键字段名
common_keys = ["ID", "CUST-ID", "EMP-ID", "KEY", "CODE", "NO"]
for k in common_keys:
if k in sample_record:
effective_key = k
break
def _by(records, kf):
d = {}
for r in records:
key = str(r.get(kf, "__NONE__"))
d.setdefault(key, []).append(r)
return d
c_by = _by(cobol_records, key_field)
j_by = _by(java_records, key_field)
c_by = _by(cobol_records, effective_key)
j_by = _by(java_records, effective_key)
pairs = []
all_keys = set(c_by) | set(j_by)
for k in sorted(all_keys):
c_items = c_by.get(k, [])
j_items = j_by.get(k, [])
+400
View File
@@ -0,0 +1,400 @@
# V3系统目录结构设计文档
## 一、目录结构概述
V3系统采用统一的目录结构来组织COBOL和Java的输出结果,确保所有程序的输出文件都位于同一个根目录下,便于管理和比较。
## 二、目录结构规范
### 2.1 标准目录结构
```
output/<PROGRAM_ID>/
├── cobol/ # COBOL输出(所有组)
│ ├── main/ # 单轮/默认场景
│ │ ├── input/ # COBOL输入flat文件
│ │ ├── output/ # COBOL输出flat文件
│ │ └── json/ # Java验证用JSON
│ ├── run_skip/ # 跳过场景
│ │ ├── input/
│ │ ├── output/
│ │ └── json/
│ ├── run_<scenario_id>/ # 多轮场景
│ │ ├── input/
│ │ ├── output/
│ │ └── json/
│ └── pre_src/ # 预处理源码
├── java/ # Java输出(所有组)
│ ├── main/ # 对应cobol/main的Java输出
│ │ └── output/
│ ├── run_skip/ # 对应cobol/run_skip的Java输出
│ │ └── output/
│ └── run_<scenario_id>/ # 对应cobol/run_*的Java输出
│ └── output/
├── coverage/ # 覆盖率报告(全局)
├── logs/ # 日志(全局)
│ ├── main.log
│ ├── run_skip.log
│ └── run_<scenario_id>.log
├── gcov/ # gcov数据(按场景分离)
│ ├── run_main/
│ ├── run_skip/
│ └── run_<scenario_id>/
├── data/ # SQLite数据库(按场景分离)
│ ├── kin.db # 单轮场景
│ └── kin_<scenario_id>.db # 多轮场景
└── reports/ # 测试报告
└── <timestamp>/
├── result.json
├── report.html
└── machine.json
```
### 2.2 目录命名规则
| 目录 | 命名规则 | 说明 |
|------|----------|------|
| `cobol/main/` | 固定名称 | 单轮/默认场景 |
| `cobol/run_skip/` | 固定名称 | 跳过场景(原 `skip/` |
| `cobol/run_<id>/` | `run_` + 场景ID | 多轮场景 |
| `java/<对应cobol目录>/` | 与cobol目录对应 | Java输出 |
| `coverage/` | 固定名称 | 覆盖率报告 |
| `logs/` | 固定名称 | 日志文件 |
| `gcov/run_<id>/` | `run_` + 场景ID | gcov数据 |
| `data/` | 固定名称 | SQLite数据库 |
### 2.3 文件命名规则
| 文件类型 | 命名规则 | 示例 |
|----------|----------|------|
| COBOL输入文件 | `<PROGRAM_ID>R<NN>` | `KIN01R01`, `ZAN01R01` |
| COBOL输出文件 | `<PREFIX>W<NN>` | `KIN01W01`, `ZAN01W01` |
| JSON中介文件 | `<PROGRAM_ID>.json` | `KIN01INP.json` |
| 覆盖率报告 | `<PROGRAM_ID>_coverage.html` | `KIN01INP_coverage.html` |
| 运行日志 | `<场景ID>.log` | `main.log`, `run_normal.log` |
| gcov数据 | 按场景ID分目录 | `gcov/run_main/`, `gcov/run_skip/` |
| SQLite数据库 | `kin_<场景ID>.db` | `kin.db`, `kin_normal.db` |
## 三、场景映射规则
### 3.1 单轮场景
**定义**:程序只有一次执行,无多场景配置。
**目录结构**
```
output/<PROGRAM_ID>/
├── cobol/
│ └── main/
│ ├── input/
│ ├── output/
│ └── json/
├── java/
│ └── main/
│ └── output/
├── coverage/
├── logs/
│ └── main.log
├── gcov/
│ └── run_main/
└── data/
└── kin.db
```
### 3.2 多轮场景
**定义**:程序有多次执行,通过YAML配置文件定义场景。
**目录结构**
```
output/<PROGRAM_ID>/
├── cobol/
│ ├── run_normal/
│ │ ├── input/
│ │ ├── output/
│ │ └── json/
│ ├── run_insert_error/
│ │ ├── input/
│ │ ├── output/
│ │ └── json/
│ └── run_sql_delete_error/
│ ├── input/
│ ├── output/
│ └── json/
├── java/
│ ├── run_normal/
│ │ └── output/
│ ├── run_insert_error/
│ │ └── output/
│ └── run_sql_delete_error/
│ └── output/
├── coverage/
├── logs/
│ ├── run_normal.log
│ ├── run_insert_error.log
│ └── run_sql_delete_error.log
├── gcov/
│ ├── run_normal/
│ ├── run_insert_error/
│ └── run_sql_delete_error/
└── data/
├── kin_normal.db
├── kin_insert_error.db
└── kin_sql_delete_error.db
```
### 3.3 跳过场景
**定义**:程序有跳过主FD输入的场景(旧版 `skip/` 目录)。
**目录结构**
```
output/<PROGRAM_ID>/
├── cobol/
│ ├── main/
│ │ ├── input/
│ │ ├── output/
│ │ └── json/
│ └── run_skip/
│ ├── input/
│ ├── output/
│ └── json/
├── java/
│ ├── main/
│ │ └── output/
│ └── run_skip/
│ └── output/
├── coverage/
├── logs/
│ ├── main.log
│ └── run_skip.log
├── gcov/
│ ├── run_main/
│ └── run_skip/
└── data/
├── kin.db
└── kin_skip.db
```
## 四、路径依赖说明
### 4.1 env_overrides 路径
COBOL运行时通过环境变量映射文件路径:
```python
# 输入文件
env_overrides[fname] = os.path.join("input", fname)
# 输出文件
env_overrides[fname] = os.path.join("output", fname)
```
**注意**:这些是相对于CWD的路径,CWD为 `cobol/main/``cobol/run_<id>/`
### 4.2 SQLite数据库路径
```python
# 单轮场景
db_path = data/kin.db
# 多轮场景
db_path = data/kin_<scenario_id>.db
```
### 4.3 gcov数据路径
```python
# 单轮场景
gcov_dir = gcov/run_main/
# 多轮场景
gcov_dir = gcov/run_<scenario_id>/
```
## 五、向后兼容性
### 5.1 旧版目录迁移
| 旧版目录 | 新版目录 | 迁移方式 |
|----------|----------|----------|
| `main/input/` | `cobol/main/input/` | 移动文件 |
| `main/output/` | `cobol/main/output/` | 移动文件 |
| `main/json/` | `cobol/main/json/` | 移动文件 |
| `skip/input/` | `cobol/run_skip/input/` | 重命名+移动 |
| `skip/output/` | `cobol/run_skip/output/` | 重命名+移动 |
| `skip/json/` | `cobol/run_skip/json/` | 重命名+移动 |
| `pre_src/` | `cobol/pre_src/` | 移动文件 |
| `coverage/` | `coverage/` | 保持不变 |
| `logs/` | `logs/` | 保持不变 |
| `gcov/` | `gcov/` | 保持不变 |
| `data/` | `data/` | 保持不变 |
### 5.2 兼容性处理
```python
def _ensure_cobol_dir_structure(runtime_dir):
"""确保cobol目录结构存在,兼容旧版"""
cobol_dir = runtime_dir / "cobol"
if not cobol_dir.exists():
# 检查是否是旧版结构(main/直接在runtime_dir下)
old_main = runtime_dir / "main"
if old_main.exists():
# 迁移到新结构
shutil.move(str(old_main), str(cobol_dir / "main"))
```
## 六、黑盒测试映射
### 6.1 YAML配置示例
```yaml
# config/programs/KIN08DBU.yaml
runs:
- id: normal
sysin:
- { dd: KIN08S01, content: "..." }
- id: no_period
sysin:
- { dd: KIN08S01, content: "..." }
- id: insert_error
inject_duplicate_pk: true
sysin:
- { dd: KIN08S01, content: "..." }
- id: sql_delete_error
sysin:
- { dd: KIN08S01, content: "..." }
- id: sql_select_error
sysin:
- { dd: KIN08S01, content: "..." }
```
### 6.2 目录映射
| 场景ID | COBOL目录 | Java目录 | 日志文件 | gcov目录 | 数据库文件 |
|--------|-----------|----------|----------|----------|------------|
| normal | `cobol/run_normal/` | `java/run_normal/output/` | `logs/run_normal.log` | `gcov/run_normal/` | `data/kin_normal.db` |
| no_period | `cobol/run_no_period/` | `java/run_no_period/output/` | `logs/run_no_period.log` | `gcov/run_no_period/` | `data/kin_no_period.db` |
| insert_error | `cobol/run_insert_error/` | `java/run_insert_error/output/` | `logs/run_insert_error.log` | `gcov/run_insert_error/` | `data/kin_insert_error.db` |
| sql_delete_error | `cobol/run_sql_delete_error/` | `java/run_sql_delete_error/output/` | `logs/run_sql_delete_error.log` | `gcov/run_sql_delete_error/` | `data/kin_sql_delete_error.db` |
| sql_select_error | `cobol/run_sql_select_error/` | `java/run_sql_select_error/output/` | `logs/run_sql_select_error.log` | `gcov/run_sql_select_error/` | `data/kin_sql_select_error.db` |
### 6.3 黑盒测试执行流程
1. **编译阶段**:编译一次,全场景共享 `.exe``.gcno`
2. **场景执行**:对每个场景独立执行
- 生成场景特定输入数据
- 初始化场景特定DB
- 运行COBOL程序
- 收集场景特定gcov数据
3. **Java执行**:使用最后一个场景的DB结果
4. **验证比对**:比较COBOL和Java输出
5. **覆盖率合并**:合并多轮gcov数据,生成覆盖率报告
### 6.4 测试报告生成
| 报告类型 | 生成方式 | 输出位置 |
|----------|----------|----------|
| 覆盖率HTML报告 | `orchestrator_db.generate_coverage_report()` | `coverage/<PROGRAM_ID>_coverage.html` |
| 测试结果JSON | `orchestrator.run_all()` | `reports/<timestamp>/result.json` |
| 测试报告HTML | `ReportGenerator.generate_html()` | `reports/<timestamp>/report.html` |
| 机器可读JSON | `ReportGenerator.generate_machine_json()` | `reports/<timestamp>/machine.json` |
## 七、代码修改清单
### 7.1 orchestrator_db.py
| 行号 | 修改内容 | 说明 |
|------|----------|------|
| 103 | `runtime_dir = v3_root / "output" / program_id / "cobol"` | 添加cobol子目录 |
| 594-596 | `run_label = "main" if not scenario else f"run_{scenario.id}"` | 统一场景目录命名 |
| 626-628 | `env_overrides` 路径改为 `"input"``"output"` | 移除main/层级 |
| 686 | `log_dir = self.runtime_dir.parent / "logs"` | 日志目录移到根目录 |
| 700 | `gcda_dst_dir = gcov_dir / run_label` | 统一gcov目录命名 |
| 769 | `output_dir = v3_root / "output" / self.program_id / "coverage"` | 覆盖率报告移到根目录 |
| 779 | `gcov_dir = self.runtime_dir.parent / "gcov"` | gcov目录移到根目录 |
| 975 | `java_out = self.runtime_dir.parent / "java" / run_label / "output"` | Java输出目录 |
| 1016 | `self.java_output_path = self.runtime_dir.parent / "java" / run_label / "output"` | Java输出路径 |
### 7.2 cobol_testgen/__init__.py
| 行号 | 修改内容 | 说明 |
|------|----------|------|
| 1759 | `outpath = prog_outdir / 'cobol' / 'main' / 'json'` | JSON输出路径 |
| 1769 | `prog_outdir / 'cobol' / 'main' / 'input'` | 输入文件路径 |
| 1776 | `prog_outdir / 'cobol' / 'main' / 'input'` | 子程序输入路径 |
| 1796 | `prog_outdir / 'cobol' / 'run_skip' / 'json'` | Skip JSON路径 |
| 1802 | `prog_outdir / 'cobol' / 'run_skip' / 'input'` | Skip输入路径 |
### 7.3 cobol_testgen/runner.py
| 行号 | 修改内容 | 说明 |
|------|----------|------|
| 425-426 | `Path(outdir) / 'cobol' / 'main' / 'input'` | 主场景输入路径 |
| 425-426 | `Path(outdir) / 'cobol' / 'main' / 'output'` | 主场景输出路径 |
| 430-431 | `Path(outdir) / 'cobol' / 'run_skip' / 'input'` | Skip场景输入路径 |
| 430-431 | `Path(outdir) / 'cobol' / 'run_skip' / 'output'` | Skip场景输出路径 |
### 7.4 runners/gixsql_runner.py
| 行号 | 修改内容 | 说明 |
|------|----------|------|
| 308 | `debug_dir = ... / "cobol" / "pre_src"` | 预处理源码路径 |
### 7.5 orchestrator.py
| 行号 | 修改内容 | 说明 |
|------|----------|------|
| 142 | `co = Path(f"output/{cfg.program}/cobol/main/output/cobol_out.bin")` | COBOL输出路径 |
| 159 | `java_out_dir = Path("output") / cfg.program / "java" / "main" / "output"` | Java输出路径 |
| 200 | `rd = Path(f"output/{vr.program}/reports") / vr.timestamp` | 报告输出路径 |
## 八、测试验证
### 8.1 单元测试
运行现有单元测试确保向后兼容:
```bash
python -m pytest tests/ -v
```
### 8.2 集成测试
运行黑盒测试验证目录结构:
```bash
cd test-data
python s15_coverage_verification.py
python s30_db_e2e.py
```
### 8.3 手动验证
检查目录结构是否正确:
```bash
# 检查单轮程序
ls -la output/KIN01INP/
ls -la output/KIN01INP/cobol/main/
ls -la output/KIN01INP/java/main/output/
# 检查多轮程序
ls -la output/KIN08DBU/
ls -la output/KIN08DBU/cobol/run_normal/
ls -la output/KIN08DBU/java/run_normal/output/
```
## 九、注意事项
1. **env_overrides 路径**:修改后需要确保COBOL运行时能找到正确的文件
2. **SQLite数据库路径**:多轮场景的DB文件需要按场景命名
3. **gcov数据合并**:多轮场景的gcov数据需要正确合并
4. **向后兼容**:需要处理旧版目录结构的迁移
5. **测试覆盖**:修改后需要运行所有测试确保功能正常
---
**文档版本**v1.0
**创建日期**2026-09-05
**最后更新**2026-09-05
+5 -3
View File
@@ -139,7 +139,7 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
vr.debug["cobol_build"] = {"ok": build.success, "log": build.log[-300:]}
if not build.success:
return _done(vr, t0, "BLOCKED", 2)
co = Path("cobol_out.bin")
co = Path(f"output/{cfg.program}/cobol/main/output/cobol_out.bin")
if not cob.run(build.artifact_path, str(bundle.cobol_input()), str(co)).success:
return _done(vr, t0, "ERROR", 3)
@@ -156,7 +156,9 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
if not jb.success:
return _done(vr, t0, "BLOCKED", 2)
inp = str(bundle.spark_input_dir() if cfg.runner_mode == "spark" else bundle.native_input())
jr = runner.run(jb.artifact_path, inp, "java_out")
java_out_dir = Path("output") / cfg.program / "java" / "main" / "output"
java_out_dir.mkdir(parents=True, exist_ok=True)
jr = runner.run(jb.artifact_path, inp, str(java_out_dir))
reader = CobolBinaryReader()
cr = reader.read(str(co), tree)
@@ -195,7 +197,7 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
except:
pass
rd = Path(f"reports/{vr.program}") / vr.timestamp
rd = Path(f"output/{vr.program}/reports") / vr.timestamp
rd.mkdir(parents=True, exist_ok=True)
g = ReportGenerator()
g.generate_json(vr, rd / "result.json")
+561 -31
View File
@@ -90,6 +90,10 @@ class GixsqlOrchestrator:
self.cobol_src_dir = Path(cobol_src_dir)
self.copybook_dirs = copybook_dirs or []
self.skip_jvm = skip_jvm
# DB-Java 文件式运行 + DB 表比对(run_java=True 时在每场景 Step3 后触发)
self.run_java: bool = False
self._java_cfg_cache = None
self._db_java_results: list[dict] = []
v3_root = Path(__file__).parent # cobol-java-v3/
# Build artifacts in temp (ASCII-only, gixpp can't handle Chinese paths)
@@ -100,7 +104,8 @@ class GixsqlOrchestrator:
self.work_dir = Path(work_dir)
# Runtime data under V3 (DB, flat files, CWD)
self.runtime_dir = v3_root / "output" / program_id
# 新目录结构:output/<PROGRAM_ID>/cobol/ 存放COBOL输出
self.runtime_dir = v3_root / "output" / program_id / "cobol"
self.schema: ProgramSchema = load_schema(program_id)
@@ -122,6 +127,7 @@ class GixsqlOrchestrator:
self.exe_path: Optional[Path] = None
self.java_input_path: Optional[Path] = None
self._current_db_path: Optional[Path] = None # scenario-specific DB path
self._java_initial_db_snapshots: dict[str, Path] = {} # scenario label -> initial DB snapshot (before COBOL)
self._multi_run_gcov_data: dict[int, int] | None = None # merged multi-run gcov data
self._sub_gcov_data: dict[str, dict[int, int]] = {} # per-subprogram gcov (kept separate from main)
self.java_output_path: Optional[Path] = None
@@ -590,11 +596,11 @@ class GixsqlOrchestrator:
"exe not found (run step1 first)")
# シナリオ毎の出力先
run_label = f"run_{scenario.id}" if scenario else ""
run_dir = self.runtime_dir / run_label if scenario else self.runtime_dir
input_dir = run_dir / "main" / "input"
output_dir = run_dir / "main" / "output"
gcov_dir = self.runtime_dir / "gcov"
run_label = f"run_{scenario.id}" if scenario else "main"
run_dir = self.runtime_dir / run_label
input_dir = run_dir / "input"
output_dir = run_dir / "output"
gcov_dir = self.runtime_dir.parent / "gcov"
input_dir.mkdir(parents=True, exist_ok=True)
output_dir.mkdir(parents=True, exist_ok=True)
gcov_dir.mkdir(parents=True, exist_ok=True)
@@ -623,9 +629,9 @@ class GixsqlOrchestrator:
env_overrides = {}
for fname, direction in assign_map.items():
if direction == "INPUT":
env_overrides[fname] = os.path.join("main", "input", fname)
env_overrides[fname] = os.path.join("input", fname)
else:
env_overrides[fname] = os.path.join("main", "output", fname)
env_overrides[fname] = os.path.join("output", fname)
# シナリオ毎の DB パス
db_path = self._current_db_path or self.db_path
@@ -645,10 +651,15 @@ class GixsqlOrchestrator:
shutil.copy2(str(db_path), str(cwd_db))
# gixsql regex requires sqlite://host/path (single segment, no dots).
# Copy to CWD/kin (no extension) for sqlite://localhost/kin.
# 同时复制到 kin.db,确保 gixsql 打开的是有 PRIMARY KEY 的版本
cwd_kin = cwd / "kin"
if cwd_kin.exists():
cwd_kin.unlink()
shutil.copy2(str(db_path), str(cwd_kin))
cwd_kin_db = cwd / "kin.db"
if cwd_kin_db.exists():
cwd_kin_db.unlink()
shutil.copy2(str(db_path), str(cwd_kin_db))
# .gcda は CWD= run_dir)に書き出されるので、実行後に gcov/run_{id}/ に移動する
# 各シナリオ実行前に前回の .gcda を削除(GnuCOBOL は累積書込みを行うため)
@@ -682,7 +693,7 @@ class GixsqlOrchestrator:
command_args=command_args,
)
log_dir = self.runtime_dir / "logs"
log_dir = self.runtime_dir.parent / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
log_dir.joinpath(f"{run_label or self.program_id}.log").write_text(
result.log, encoding='utf-8')
@@ -696,7 +707,7 @@ class GixsqlOrchestrator:
gcda_src_dirs.append(exe_dir_for_gcda)
if scenario is None:
gcda_src_dirs.append(self.runtime_dir) # 従来互換
gcda_dst_dir = gcov_dir / run_label if scenario else gcov_dir
gcda_dst_dir = gcov_dir / run_label
gcda_dst_dir.mkdir(parents=True, exist_ok=True)
for sd in gcda_src_dirs:
for f in sd.glob("*.gcda"):
@@ -715,6 +726,17 @@ class GixsqlOrchestrator:
except PermissionError:
pass
# gixsql 通过 GIXSQL_DB_PATH 把 COBOL 结果写回 db_path(运行时唯一真实落盘文件)。
# CWD 下的 kin/kin.db 是运行前从种子库复制的快照;这里运行后回拷,
# 保证 _java_compare_scenario 读取的是 COBOL 的真实输出而不是过期的种子快照。
if db_path and db_path.exists():
for _dst in (cwd / 'kin', cwd / 'kin.db', cwd / 'data' / 'kin.db'):
try:
_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(db_path), str(_dst))
except OSError as _e:
logger.warning(f" 运行后 DB 回拷失败 {_dst}: {_e}")
return DbPipelineResult(
self.program_id, 3, result.success,
data={"returncode": result.returncode, "log": result.log[:500],
@@ -765,7 +787,7 @@ class GixsqlOrchestrator:
f"exe not found at {self.exe_path} or {fallback}")
if output_dir is None:
v3_root = Path(__file__).parent
output_dir = v3_root / "reports" / self.program_id / "coverage"
output_dir = v3_root / "output" / self.program_id / "coverage"
output_dir = Path(output_dir)
# 1. Use pre-merged multi-run gcov data if available (skip gcov re-run)
@@ -775,7 +797,7 @@ class GixsqlOrchestrator:
# plain integers that collide with the main program's (e.g.
# SUB04CHK line 167=0 would overwrite main line 167=25 and
# wipe real coverage). Stored per-subprogram for reference.
gcov_dir = self.runtime_dir / "gcov"
gcov_dir = self.runtime_dir.parent / "gcov"
self._sub_gcov_data = {}
for sub in self.schema.subprograms:
sub_merged = _merge_run_dirs_gcov(gcov_dir, sub)
@@ -913,6 +935,344 @@ class GixsqlOrchestrator:
logger.exception("generate_coverage_report failed")
return DbPipelineResult(self.program_id, 0, False, str(e))
# ── DB-Java 通用适配(文件式 Java 运行 + DB 表比对) ──
# 依赖 JavaSrc 工程(target/classes + target/*.jar + lib/*.jar,显式主类),
# 无需 mvn。Java 从“场景初始 DB 快照 + 同入力”出发,最终 DB/W01 与 COBOL 结果比对。
def _java_cfg(self):
if self._java_cfg_cache is None:
self._java_cfg_cache = None
try:
from cobol_testgen.runner import _discover_java
root = Path(self.cobol_src_dir).parent / 'JavaSrc'
cfg = _discover_java(str(root), self.program_id)
self._java_cfg_cache = cfg
if cfg:
logger.info(f" DB-Java: 发现主类 {cfg['main_class']}")
except Exception as e: # noqa: BLE001
logger.warning(f" DB-Java 发现失败: {e}")
return self._java_cfg_cache
def _java_db_rel_path(self) -> Optional[str]:
"""扫描 Java 主类源码中 jdbc:sqlite:<相对路径>,返回 DB 相对路径。"""
cfg = self._java_cfg()
if not cfg:
return None
try:
main = Path(cfg['root']) / 'src' / f"{cfg['main_class']}.java"
txt = main.read_text(encoding='utf-8', errors='replace')
m = re.search(r'jdbc:sqlite:([^\s"\'\)]+)', txt)
if m:
return m.group(1).replace('\\', os.sep)
except Exception: # noqa: BLE001
pass
return None
def _java_scenario_label(self, scenario: ScenarioDef | None) -> str:
return f"run_{scenario.id}" if scenario else "main"
def _java_prepare_scenario(self, scenario: ScenarioDef | None) -> Optional[dict]:
"""每场景准备 Java 运行目录:初始 DB 快照 + 入力 txt。"""
cfg = self._java_cfg()
if not cfg:
return None
label = self._java_scenario_label(scenario)
java_dir = self.runtime_dir.parent / 'java' / label
data_dir = java_dir / 'data'
if java_dir.exists():
shutil.rmtree(str(java_dir))
data_dir.mkdir(parents=True, exist_ok=True)
# 1) 初始 DB 快照(Step2 初始化后、COBOL 运行前的场景 DB)
# db_rel 为相对 Java CWD(=java_dir) 的路径(如 data/OVERTIME.DB),直接落到 java_dir 下
db_rel = self._java_db_rel_path()
# 优先使用 COBOL 运行前保存的初始快照,避免 Java 在 COBOL 修改后的 DB 上重复处理
label = self._java_scenario_label(scenario)
db_src = self._java_initial_db_snapshots.get(label) or self._current_db_path or self.db_path
if db_rel and db_src and Path(db_src).exists():
dst = java_dir / db_rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(db_src), str(dst))
logger.info(f" DB-Java[{label}]: 初始 DB 快照 -> {dst}")
# 2) 入力 txt80B 定长 → 行)
gen_input_dir = (self.work_dir / f"run_{scenario.id}" / "main" / "input") if scenario \
else (self.work_dir / "main" / "input")
written: list[str] = []
layouts: dict = {}
try:
from cobol_testgen.flatfile import analyze_fd_layout
if self.src_path and self.src_path.exists():
layouts = analyze_fd_layout(
self.src_path.read_text(encoding='utf-8-sig'),
[str(d) for d in self.copybook_dirs])
except Exception as e: # noqa: BLE001
logger.warning(f" DB-Java 布局解析失败: {e}")
for assign, lay in layouts.items():
direction = str(lay.get('direction', 'INPUT')).upper()
if direction not in ('INPUT', 'I-O'):
continue
recs = lay.get('records') or []
rlen = recs[0].get('record_length', 0) if recs else 0
src = gen_input_dir / assign
if not src.exists() or rlen <= 0:
continue
data = src.read_bytes()
lines = []
i = 0
while i + rlen <= len(data):
lines.append(data[i:i + rlen].decode('utf-8', 'replace'))
i += rlen
txt = '\n'.join(lines) + ('\n' if lines else '')
(data_dir / f'{assign}.txt').write_text(txt, encoding='utf-8')
written.append(assign)
logger.info(f" DB-Java[{label}]: 入力 {assign}.txt {len(lines)}")
if not written and not db_rel:
return None
return {'label': label, 'java_dir': str(java_dir), 'data_dir': str(data_dir),
'db_rel': db_rel, 'inputs': written, 'cfg': cfg}
def _java_run_scenario(self, scenario: ScenarioDef | None, info: dict) -> dict:
"""运行 Java 主类(文件式),返回 rc/日志。异常与 rc!=0 不抛出,仅记录。"""
cfg = info['cfg']
from cobol_testgen.runner import _find_java_exe
java_exe = _find_java_exe()
cmd = [java_exe, '-cp', cfg['classpath'], cfg['main_class']]
# command_line: scenario-level (if set) overrides program-level default
cmd_line = self.schema.command_line
if scenario and scenario.command_line is not None:
cmd_line = scenario.command_line
if cmd_line:
cmd.extend(cmd_line.split())
log_path = self.runtime_dir.parent / 'logs' / f"{self.program_id}_java_{info['label']}.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
orig = os.getcwd()
p = None
try:
os.chdir(info['java_dir'])
p = subprocess.run(cmd, capture_output=True, text=True,
encoding='utf-8', errors='replace', timeout=180)
except subprocess.TimeoutExpired:
log_path.write_text(f"COMMAND: {' '.join(cmd)}\nTIMEOUT\n", encoding='utf-8')
return {'rc': -1, 'log': 'TIMEOUT'}
except Exception as e: # noqa: BLE001
log_path.write_text(f"COMMAND: {' '.join(cmd)}\nRUN ERROR: {e}\n", encoding='utf-8')
return {'rc': -2, 'log': str(e)}
finally:
os.chdir(orig)
log = f"COMMAND: {' '.join(cmd)}\nRETURNCODE: {p.returncode}\n\nSTDOUT:\n{p.stdout}\n\nSTDERR:\n{p.stderr}"
log_path.write_text(log, encoding='utf-8')
logger.info(f" DB-Java[{info['label']}]: rc={p.returncode}")
return {'rc': p.returncode, 'log': p.stdout + p.stderr}
def _collect_java_outputs(self, info: dict) -> None:
"""收集 Java 出力:txt 出力(排除入力)+ Java 运行后 DB 副本。"""
label = info['label']
java_out_dir = self.runtime_dir.parent / 'java' / label / 'output'
java_out_dir.mkdir(parents=True, exist_ok=True)
inputs = set(info.get('inputs') or [])
data_dir = Path(info['data_dir'])
for f in sorted(data_dir.glob('*.txt')):
if f.name in inputs:
continue
try:
shutil.copy2(str(f), str(java_out_dir / f.name))
except OSError:
pass
if info.get('db_rel'):
src = Path(info['java_dir']) / info['db_rel']
if src.exists():
try:
shutil.copy2(str(src), str(java_out_dir / f"{self.program_id}_java.db"))
except OSError:
pass
@staticmethod
def _db_table_snap(db_path) -> dict[str, list[dict]]:
snap: dict[str, list[dict]] = {}
if not db_path or not Path(db_path).exists():
return snap
try:
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
cur = conn.cursor()
tables = [r[0] for r in cur.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")]
for t in tables:
rows = []
try:
for row in cur.execute('SELECT * FROM "%s"' % t):
rows.append(dict(row))
except sqlite3.OperationalError:
continue
snap[t] = rows
conn.close()
except Exception as e: # noqa: BLE001
logger.warning(f" 读 DB 失败 {db_path}: {e}")
return snap
_IGNORE_DB_COLS = re.compile(
r'(updated_at|created_at|inserted_at|deleted_at|^.*timestamp.*$)', re.IGNORECASE)
@staticmethod
def _norm_db_value(v):
"""通用归一化:数值四舍五入到 6 位消除浮点噪声;字符串去两端空白。"""
s = str(v)
t = s.strip()
try:
f = float(t)
except ValueError:
return ('s', t)
return ('n', round(f, 6))
def _java_compare_scenario(self, scenario: ScenarioDef | None, info: dict, jrep: dict) -> dict:
"""COBOL 结果 DBrun_<id>/kin vs Java 结果 DB 逐表比对。"""
from collections import Counter
label = info['label']
run_dir = self.runtime_dir / label
cobol_db = run_dir / 'kin'
if not cobol_db.exists():
cobol_db = run_dir / 'data' / 'kin.db'
java_db = None
if info.get('db_rel'):
jp = Path(info['java_dir']) / info['db_rel']
if jp.exists():
java_db = jp
if not java_db:
return {'label': label, 'rc': jrep.get('rc', -1),
'error': 'Java DB 未生成(无 db_rel 或运行失败)', 'summaries': []}
c_tab = self._db_table_snap(cobol_db)
j_tab = self._db_table_snap(java_db)
keys = sorted(set(c_tab) | set(j_tab))
def _clean(row: dict) -> dict:
return {k: v for k, v in row.items() if not self._IGNORE_DB_COLS.search(k)}
def _canon(rows):
c = Counter()
for r in rows:
row = _clean(r)
# 忽略全为空的键(仅剩时间戳被剔除后的空行)
items = sorted((str(k),) + (self._norm_db_value(v),) for k, v in row.items())
c[tuple(items)] += 1
return c
summaries = []
for t in keys:
cc = _canon(c_tab.get(t, []))
jc = _canon(j_tab.get(t, []))
allk = set(cc) | set(jc)
matched = sum(min(cc[k], jc[k]) for k in allk)
# 不一致行数 = 两侧行数较小者中无法配成完全一致的行数(避免同一差异行重复计数)
min_rows = min(len(c_tab.get(t, [])), len(j_tab.get(t, [])))
mismatched = max(0, min_rows - matched)
samples = []
for k in sorted(allk, key=lambda x: str(x)):
if cc[k] != jc[k]:
side = 'COBOL' if cc[k] > jc[k] else 'Java'
disp = {}
for fld, typed in k:
if typed[0] == 'n':
fv = typed[1]
disp[fld] = str(int(fv)) if float(fv).is_integer() else str(fv)
else:
disp[fld] = typed[1]
samples.append({'side': side, 'diff': abs(cc[k] - jc[k]),
'row': {kk: str(vv)[:30] for kk, vv in disp.items()}})
if len(samples) >= 3:
break
summaries.append({'table': t, 'cobol_count': len(c_tab.get(t, [])),
'java_count': len(j_tab.get(t, [])),
'matched': matched, 'mismatched': mismatched,
'samples': samples})
return {'label': label, 'rc': jrep.get('rc', -1), 'summaries': summaries}
def _write_db_java_report(self, cobol_results: list) -> None:
lines = []
lines.append(f"# {self.program_id} COBOL vs Java 出力比对测试报告(DB 管道)")
lines.append('')
lines.append(f"- 程序ID: {self.program_id}")
lines.append('- 比对基准: COBOL 运行后 DB 表(run_<场景>/kin vs Java 从同初始 DB 运行后 DB 表')
lines.append('')
lines.append('## 1. COBOL 执行结果(DB 管道 run_all')
lines.append('')
lines.append('| 组 | returncode | 判定 |')
lines.append('|----|-----------|------|')
for label, res in cobol_results:
rc = (res.data or {}).get('returncode', '-')
mark = '通过' if res.success else '差异'
lines.append(f"| {label} | {rc} | {mark} |")
lines.append('')
lines.append('## 2. Java 执行与 DB 表比对(逐场景逐表)')
lines.append('')
for rep in self._db_java_results:
lines.append(f'### 2.{self._db_java_results.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.get('summaries'):
lines.append('(无可比对 DB 表)')
lines.append('')
continue
lines.append('| 表 | COBOL行数 | Java行数 | 一致 | 不一致 | 判定 |')
lines.append('|----|----------|----------|------|--------|------|')
for s in rep['summaries']:
count_ok = (s['cobol_count'] == s['java_count'])
verdict = '一致' if (count_ok and s['mismatched'] == 0) else '不一致'
lines.append(f"| {s['table']} | {s['cobol_count']} | {s['java_count']} "
f"| {s['matched']} | {s['mismatched']} | {verdict} |")
diff = [s for s in rep['summaries']
if s['mismatched'] > 0 or s['cobol_count'] != s['java_count']]
if diff:
lines.append('')
lines.append('**差异示例(最多 3 行/表,COBOL/Java 各侧存在但内容不同)**:')
for s in diff:
for smp in s['samples']:
lines.append(f"- {s['table']} [{smp['side']}侧 x{smp['diff']}]: "
f"{smp['row']}")
lines.append('')
all_fd = [s for rep in self._db_java_results for s in rep.get('summaries', [])]
tot_m = sum(s['matched'] for s in all_fd)
tot_mis = sum(s['mismatched'] for s in all_fd)
any_count_diff = any(s['cobol_count'] != s['java_count'] for s in all_fd)
lines.append('## 3. 总结')
lines.append('')
if not all_fd:
lines.append('- 无可比对的 DB 记录(Java 未产出或发现失败)。')
elif tot_mis == 0 and not any_count_diff:
lines.append(f'- COBOL 与 Java 全部 DB 记录一致(一致 {tot_m} 行,不一致 0 行)。')
lines.append('- 判定: **通过**')
else:
lines.append(f'- COBOL 与 Java DB 记录存在差异:一致 {tot_m} 行,内容不一致 {tot_mis}'
f"{'(另有行数不一致)' if any_count_diff else ''}")
lines.append('- 判定: **存在差异**,详见上文逐场景逐表明细。')
lines.append('')
lines.append('> 说明: Java 从各场景“初始 DB 快照 + 同入力”运行;Java 异常/rc!=0 已作为该场景结果记录(不阻断管道)。')
lines.append('')
report_path = self.runtime_dir.parent / f'{self.program_id}_测试报告.md'
report_path.write_text('\n'.join(lines), encoding='utf-8')
logger.info(f" DB-Java 比对报告: {report_path}")
def _run_db_java_scenario(self, scenario: ScenarioDef | None) -> None:
"""每场景执行:准备(初始DB快照+入力) -> 运行 Java -> 收集出力 -> 比对记录。"""
try:
info = self._java_prepare_scenario(scenario)
if not info:
logger.info(" DB-Java: 未发现 Java 对应程序或入力/DB 不可用,跳过")
return
jrep = self._java_run_scenario(scenario, info)
self._collect_java_outputs(info)
cres = self._java_compare_scenario(scenario, info, jrep)
self._db_java_results.append(cres)
except Exception as e: # noqa: BLE001
logger.warning(f" DB-Java 场景 {scenario.id if scenario else 'main'} 失败: {e}")
# ── Step 4: DB → Java 中介データ ──
def step4_extract_intermediate(self) -> DbPipelineResult:
@@ -958,21 +1318,55 @@ class GixsqlOrchestrator:
# ── Step 5: Java 実行 ──
def step5_run_java(self, java_cmd: str = "java",
java_jar: str | Path | None = None) -> DbPipelineResult:
"""Java プログラム実行"""
java_jar: str | Path | None = None,
java_src_dir: str | Path | None = None) -> DbPipelineResult:
"""Java プログラム実行
Args:
java_cmd: Java可执行文件路径
java_jar: Java JAR文件路径(如果提供)
java_src_dir: Java源代码目录(如果提供,将先编译)
"""
if not self.java_input_path or not self.java_input_path.exists():
return DbPipelineResult(self.program_id, 5, False,
"intermediate data not found (run step4 first)")
java_out = self.work_dir / "java_output"
run_label = "main" # 默认场景
java_out = self.runtime_dir.parent / "java" / run_label / "output"
java_out.mkdir(parents=True, exist_ok=True)
# 如果提供了Java源代码目录,先编译
if java_src_dir and not java_jar:
try:
from runners import NativeJavaRunner
runner = NativeJavaRunner()
build_result = runner.compile(str(java_src_dir))
if not build_result.success:
return DbPipelineResult(
self.program_id, 5, False,
message=f"Java compilation failed: {build_result.log[:200]}",
data={"log": build_result.log[:500]}
)
java_jar = build_result.artifact_path
except Exception as e:
return DbPipelineResult(
self.program_id, 5, False,
message=f"Java compilation error: {str(e)}",
data={"error": str(e)}
)
if java_jar:
# 使用JAR文件执行
cmd = [java_cmd, "-jar", str(java_jar),
"-i", str(self.java_input_path),
"-o", str(java_out)]
else:
cmd = [java_cmd, "-version"]
# 没有提供Java JAR或源代码
return DbPipelineResult(
self.program_id, 5, False,
message="No Java JAR or source directory provided",
data={"error": "No Java JAR or source"}
)
try:
r = subprocess.run(cmd, capture_output=True, timeout=60)
@@ -986,6 +1380,8 @@ class GixsqlOrchestrator:
)
except subprocess.TimeoutExpired:
return DbPipelineResult(self.program_id, 5, False, "Java timeout")
except Exception as e:
return DbPipelineResult(self.program_id, 5, False, f"Java execution error: {str(e)}")
# ── Step 6: 検証 ──
@@ -999,23 +1395,130 @@ class GixsqlOrchestrator:
sqlite_path=str(db_path) if db_path else "",
step_reached=6,
)
# 1. 读取COBOL输出(从DB表)
cobol_records = []
if db_path and db_path.exists():
after_tables = self.runner.read_db_tables(
db_path,
[t.name for t in self.schema.db_tables],
)
for table_data in after_tables:
vr.debug[f"table_{table_data.table_name}_rows"] = len(table_data.rows)
try:
after_tables = self.runner.read_db_tables(
db_path,
[t.name for t in self.schema.db_tables],
)
for table_data in after_tables:
vr.debug[f"table_{table_data.table_name}_rows"] = len(table_data.rows)
# 将表数据转换为记录格式
for row in table_data.rows:
record = {}
for i, col in enumerate(table_data.columns):
record[col] = row[i] if i < len(row) else ""
cobol_records.append(record)
except Exception as e:
vr.debug["cobol_read_error"] = str(e)
# 2. 读取Java输出(从文件)
java_records = []
if self.java_output_path and self.java_output_path.exists():
java_files = list(self.java_output_path.glob("*.txt")) + \
list(self.java_output_path.glob("*.json"))
vr.debug["java_output_files"] = [str(f) for f in java_files]
vr.fields_matched = len(java_files)
vr.exit_code = 0 if vr.fields_mismatched == 0 else 1
vr.status = "PASS" if vr.exit_code == 0 else "MISMATCH"
try:
java_files = list(self.java_output_path.glob("*.txt")) + \
list(self.java_output_path.glob("*.json"))
vr.debug["java_output_files"] = [str(f) for f in java_files]
for java_file in java_files:
if java_file.suffix == ".json":
with open(java_file, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, list):
java_records.extend(data)
else:
java_records.append(data)
elif java_file.suffix == ".txt":
with open(java_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line:
java_records.append({"raw": line})
except Exception as e:
vr.debug["java_read_error"] = str(e)
# 3. 比较COBOL和Java输出
if not cobol_records and not java_records:
vr.status = "PASS"
vr.exit_code = 0
return vr
if not cobol_records:
vr.status = "MISMATCH"
vr.exit_code = 1
vr.debug["error"] = "No COBOL records found"
return vr
if not java_records:
vr.status = "MISMATCH"
vr.exit_code = 1
vr.debug["error"] = "No Java records found"
return vr
# 4. 使用comparator模块进行比较
try:
from comparator import align_records, compare_field
# 智能关键字段推断:尝试常见字段名
key_field = "ID" # 默认值
if cobol_records:
sample_record = cobol_records[0]
common_keys = ["ID", "CUST-ID", "EMP-ID", "KEY", "CODE", "NO"]
for k in common_keys:
if k in sample_record:
key_field = k
break
aligned = align_records(cobol_records, java_records, key_field=key_field)
field_results = []
matched_count = 0
mismatched_count = 0
for cobol_rec, java_rec, status in aligned:
if status == "MATCHED":
# 比较每个字段
for field_name in cobol_rec:
if field_name == key_field:
continue # 跳过关键字段
cobol_value = str(cobol_rec.get(field_name, ""))
java_value = str(java_rec.get(field_name, ""))
# 确定字段类型
field_name_upper = field_name.upper()
if any(c in field_name_upper for c in ["AMT", "AMOUNT", "PRICE", "COST", "TOTAL", "QTY", "QUANTITY", "NUM", "NUMBER", "COUNT"]):
field_type = "decimal"
elif any(c in field_name_upper for c in ["DATE", "TIME", "TIMESTAMP", "DT", "TM"]):
field_type = "date"
else:
field_type = "string"
result = compare_field(field_name, cobol_value, java_value, field_type)
field_results.append(result)
if result.status == "PASS" or result.status == "TOLERATED":
matched_count += 1
else:
mismatched_count += 1
else:
# 记录不匹配
mismatched_count += 1
vr.fields_matched = matched_count
vr.fields_mismatched = mismatched_count
vr.field_results = field_results
vr.status = "PASS" if mismatched_count == 0 else "MISMATCH"
vr.exit_code = 0 if mismatched_count == 0 else 1
except Exception as e:
vr.status = "ERROR"
vr.exit_code = 1
vr.debug["comparison_error"] = str(e)
return vr
# ── 全Step一括実行 ──
@@ -1046,6 +1549,7 @@ class GixsqlOrchestrator:
)
# Each scenario: generate inputs + run COBOL
cobol_runs = []
for scenario in scenarios:
label = f" [{scenario.id}]" if is_multi else ""
logger.info(f" Step 2 (generate inputs){label}...")
@@ -1056,6 +1560,16 @@ class GixsqlOrchestrator:
status="BLOCKED", exit_code=2,
step_reached=2,
)
# 保存初始 DB 快照(Step2 初始化后、COBOL 运行前)供 Java 使用
initial_db = self._current_db_path or self.db_path
scenario_label = self._java_scenario_label(scenario if is_multi else None)
if initial_db and Path(initial_db).exists():
import tempfile, shutil as _shutil
snap = Path(tempfile.gettempdir()) / f"v3_initial_db_{self.program_id}_{scenario.id}.db"
_shutil.copy2(str(initial_db), str(snap))
self._java_initial_db_snapshots[scenario_label] = snap
logger.info(f" 初始 DB 快照 -> {snap}")
logger.info(f" Step 3 (run COBOL){label}...")
r3 = self.step3_run_cobol(scenario if is_multi else None)
if not r3.success:
@@ -1064,6 +1578,11 @@ class GixsqlOrchestrator:
status="BLOCKED", exit_code=2,
step_reached=3,
)
cobol_runs.append((scenario_label, r3))
# DB-Java(每场景):初始 DB 快照 + 同入力 → Java 运行 → DB 表比对
if getattr(self, 'run_java', False):
self._run_db_java_scenario(scenario if is_multi else None)
# Step 4: extract intermediate (last scenario wins for DB path)
if 4 not in skip:
@@ -1090,6 +1609,13 @@ class GixsqlOrchestrator:
if '--coverage' in cv_flags and generate_coverage:
self.generate_coverage_report()
# DB-Java 比对报告(多场景逐表)
if getattr(self, 'run_java', False) and self._db_java_results:
try:
self._write_db_java_report(cobol_runs)
except Exception as e: # noqa: BLE001
logger.warning(f" DB-Java 报告生成失败: {e}")
vr = VerificationRun(
program=self.program_id, runner="gixsql",
status="PASS", exit_code=0,
@@ -1166,6 +1692,10 @@ class GixsqlOrchestrator:
pk_cols.append(f"[{col.name}]")
if pk_cols:
col_defs.append(f"PRIMARY KEY ({', '.join(pk_cols)})")
# DROP 旧表确保 schema 与 YAML 定义一致(含 PRIMARY KEY
conn.execute(f"DROP TABLE IF EXISTS [{table.name}]")
if table.sql_name and table.sql_name != table.name:
conn.execute(f"DROP TABLE IF EXISTS [{table.sql_name}]")
ddl = f"CREATE TABLE IF NOT EXISTS [{table.name}] (\n " + \
",\n ".join(col_defs) + "\n)"
conn.execute(ddl)
+56 -16
View File
@@ -103,6 +103,10 @@ class GixsqlCobolRunner:
env["PATH"] = str(self.lib_path) + ";" + env["PATH"]
else:
env["PATH"] = str(self.lib_path)
# gixsql SQLite driver: without this, autocommit=OFF + COMMIT WORK never
# persists (whole transaction rolls back on disconnect). Value must be the
# literal "ON" (libgixsql only recognises ON/OFF).
env["GIXSQL_AUTOCOMMIT"] = "ON"
return env
def _expand_copy_replacing(self, text: str, search_dirs: list[Path]) -> str:
@@ -211,8 +215,11 @@ class GixsqlCobolRunner:
inner,
flags=re.IGNORECASE | re.DOTALL
)
# Orchestrator copies DB to CWD/kin.
return (f"MOVE 'sqlite://kin' TO {conn_var}\n"
# Orchestrator copies seeded DB to CWD/kin and CWD/kin.db.
# gixsql native canonical form is 'sqlite://localhost/kin' (host + dot-less path),
# matching gixpp's own conversion of CONNECT TO 'data/kin.db'.
# A dotted path segment (e.g. kin.db) breaks gixsql parsing -> empty connection.
return (f"MOVE 'sqlite://localhost/kin' TO {conn_var}\n"
f" MOVE 'gix' TO {usr_var}\n"
f" EXEC SQL\n"
f" {new_inner.strip()}\n"
@@ -305,7 +312,7 @@ class GixsqlCobolRunner:
norm_path.write_text(text, encoding="utf-8")
# Save a copy in runtime for diagnosis
try:
debug_dir = Path(__file__).parent.parent / "output" / src_path.stem / "pre_src"
debug_dir = Path(__file__).parent.parent / "output" / src_path.stem / "cobol" / "pre_src"
debug_dir.mkdir(parents=True, exist_ok=True)
(debug_dir / f"{src_path.stem}_norm.cbl").write_text(text, encoding="utf-8")
(debug_dir / f"{src_path.stem}_pre.cbl").write_text(
@@ -350,19 +357,52 @@ class GixsqlCobolRunner:
"""
text = pp_path.read_text(encoding="utf-8")
def _fix_line(m: re.Match) -> str:
return m.group(1) + m.group(2).replace('-', '_') + m.group(3)
# SQL start lines: GIXSQL ... VALUE "SQL TEXT"
text = re.sub(
r'^(GIXSQL.*?VALUE\s+")([^"]*)(")',
_fix_line, text, flags=re.MULTILINE
)
# SQL continuation lines: GIXSQL & "SQL TEXT"
text = re.sub(
r'^(GIXSQL\s*&\s*")([^"]*)(")',
_fix_line, text, flags=re.MULTILINE
)
# gixsql emits the SQL text verbatim from the COBOL source (e.g.
# "INSERT INTO EMP-MASTER (EMP-ID, ...)"), but SQLite cannot parse bare
# hyphenated identifiers, so '-' -> '_' is needed for identifiers such as
# EMP-ID -> EMP_ID. However a hyphen surrounded by whitespace is an
# arithmetic operator (e.g. "OVT_HOURS - $1", "OVT_COUNT - 1") and MUST
# be preserved, otherwise the generated SQL is syntactically invalid
# (near "_"). Conversion is therefore restricted to hyphens that join
# two identifier characters.
#
# Each SQL string is physically split across a VALUE "..." line and
# several & "..." continuation lines, so the conversion is performed on
# the logical concatenation (length-preserving: '-' -> '_') and written
# back at the original per-line offsets.
start_re = re.compile(r'^(GIXSQL.*?VALUE\s+")(.*)(")\s*$')
cont_re = re.compile(r'^(GIXSQL\s*&\s*")(.*)(")\s*$')
lines = text.split("\n")
i = 0
while i < len(lines):
m = start_re.match(lines[i])
if not m:
i += 1
continue
group = [(m.group(1), m.group(2), m.group(3))]
j = i + 1
while j < len(lines):
c = cont_re.match(lines[j])
if not c:
break
group.append((c.group(1), c.group(2), c.group(3)))
j += 1
contents = [g[1] for g in group]
logical = "".join(contents)
if '-' in logical:
new_logical = re.sub(r'(?<=\w)-(?!\s)', '_', logical)
if new_logical != logical:
# rebuild each physical line char-by-char (offset preserved)
base = 0
for gi, (prefix, content, suffix) in enumerate(group):
seg = new_logical[base:base + len(content)]
if seg != content:
group[gi] = (prefix, seg, suffix)
base += len(content)
for gi, (prefix, content, suffix) in enumerate(group):
lines[i + gi] = prefix + content + suffix
i = j
text = "\n".join(lines)
# SQLite accepts CURRENT_TIMESTAMP (no space); gixsql emits CURRENT TIMESTAMP
text = re.sub(r'\bCURRENT\s+TIMESTAMP\b', 'CURRENT_TIMESTAMP', text, flags=re.IGNORECASE)
pp_path.write_text(text, encoding="utf-8")
+208 -17
View File
@@ -1,30 +1,221 @@
import os
import subprocess, json, shutil
from pathlib import Path
from runners.runner import Runner, BuildResult, RunResult, CoverageReport
class NativeJavaRunner(Runner):
"""Java 本地运行器(mvn + java -jar
支持:
- 自动查找Java/Maven可执行文件
- 编译错误处理
- 执行超时处理
- JSON解析容错
"""
def __init__(self):
self.java = "java"
self.mvn = "mvn"
def _find_java_executable(self) -> str:
"""查找Java可执行文件路径"""
# 首先尝试PATH中的java
java_path = shutil.which("java")
if java_path:
return java_path
# 尝试常见安装路径(Windows
common_paths = [
"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",
]
for path in common_paths:
if Path(path).exists():
return path
# 尝试JAVA_HOME环境变量
java_home = os.environ.get("JAVA_HOME")
if java_home:
java_exe = Path(java_home) / "bin" / "java.exe"
if java_exe.exists():
return str(java_exe)
return "java"
def _find_mvn_executable(self) -> str:
"""查找Maven可执行文件路径"""
# 首先尝试PATH中的mvn
mvn_path = shutil.which("mvn")
if mvn_path:
return mvn_path
# 尝试常见安装路径(Windows
common_paths = [
"C:/apache-maven-3.9.6/bin/mvn.cmd",
"C:/Program Files/apache-maven-3.9.6/bin/mvn.cmd",
]
for path in common_paths:
if Path(path).exists():
return path
# 尝试MAVEN_HOME环境变量
maven_home = os.environ.get("MAVEN_HOME")
if maven_home:
mvn_cmd = Path(maven_home) / "bin" / "mvn.cmd"
if mvn_cmd.exists():
return str(mvn_cmd)
return "mvn"
def compile(self, source_dir: str) -> BuildResult:
p = subprocess.run([self.mvn, "-B", "package", "-f", str(Path(source_dir) / "pom.xml")],
cwd=source_dir, capture_output=True, text=True, timeout=120)
return BuildResult(success=p.returncode == 0,
artifact_path=str(Path(source_dir) / "target" / "program.jar"),
log=p.stdout + p.stderr)
"""编译Java项目
Args:
source_dir: Java源代码目录(包含pom.xml
Returns:
BuildResult: 编译结果
"""
source_path = Path(source_dir)
if not source_path.exists():
return BuildResult(
success=False,
artifact_path="",
log=f"Source directory not found: {source_dir}"
)
pom_path = source_path / "pom.xml"
if not pom_path.exists():
return BuildResult(
success=False,
artifact_path="",
log=f"pom.xml not found in {source_dir}"
)
try:
mvn = self._find_mvn_executable()
p = subprocess.run(
[mvn, "-B", "package", "-f", str(pom_path)],
cwd=source_dir,
capture_output=True,
text=True,
timeout=120
)
# 动态获取JAR文件路径
artifact_path = str(source_path / "target" / "program.jar")
if not Path(artifact_path).exists():
# 尝试查找target目录下的其他JAR文件
target_dir = source_path / "target"
if target_dir.exists():
jar_files = list(target_dir.glob("*.jar"))
if jar_files:
artifact_path = str(jar_files[0])
return BuildResult(
success=p.returncode == 0,
artifact_path=artifact_path,
log=p.stdout + p.stderr
)
except subprocess.TimeoutExpired:
return BuildResult(
success=False,
artifact_path="",
log="Maven build timed out after 120 seconds"
)
except Exception as e:
return BuildResult(
success=False,
artifact_path="",
log=f"Build error: {str(e)}"
)
def run(self, artifact: str, input_path: str, output_path: str) -> RunResult:
with open(input_path) as f:
data = f.read()
p = subprocess.run([self.java, "-jar", artifact], input=data,
capture_output=True, text=True, timeout=60)
records = []
if p.stdout.strip():
records = [json.loads(line) for line in p.stdout.strip().split("\n") if line.strip()]
return RunResult(success=p.returncode == 0, records=records, log=p.stdout + p.stderr)
"""执行Java程序
Args:
artifact: JAR文件路径
input_path: 输入文件路径
output_path: 输出目录路径
Returns:
RunResult: 执行结果
"""
artifact_path = Path(artifact)
if not artifact_path.exists():
return RunResult(
success=False,
records=[],
log=f"Artifact not found: {artifact}"
)
input_file = Path(input_path)
if not input_file.exists():
return RunResult(
success=False,
records=[],
log=f"Input file not found: {input_path}"
)
try:
java = self._find_java_executable()
with open(input_path, 'r', encoding='utf-8') as f:
data = f.read()
p = subprocess.run(
[java, "-jar", artifact],
input=data,
capture_output=True,
text=True,
timeout=60
)
# 解析输出,支持JSON和文本格式
records = []
if p.stdout.strip():
for line in p.stdout.strip().split("\n"):
line = line.strip()
if line:
try:
record = json.loads(line)
records.append(record)
except json.JSONDecodeError:
# 如果不是JSON格式,作为文本记录处理
records.append({"raw": line})
return RunResult(
success=p.returncode == 0,
records=records,
log=p.stdout + p.stderr
)
except subprocess.TimeoutExpired:
return RunResult(
success=False,
records=[],
log="Java execution timed out after 60 seconds"
)
except Exception as e:
return RunResult(
success=False,
records=[],
log=f"Execution error: {str(e)}"
)
def get_coverage(self, artifact: str, run_id: str) -> CoverageReport:
"""获取Java代码覆盖率
Args:
artifact: JAR文件路径
run_id: 运行ID
Returns:
CoverageReport: 覆盖率报告
"""
exec_path = Path(artifact).parent / "jacoco.exec"
return CoverageReport(branch_rate=0.85, verdict="PASS") if exec_path.exists() else CoverageReport(verdict="FAIL")
if exec_path.exists():
# TODO: 解析JaCoCo覆盖率报告
return CoverageReport(branch_rate=0.85, verdict="PASS")
return CoverageReport(verdict="FAIL")
+212 -23
View File
@@ -1,36 +1,225 @@
import os
import subprocess, json, shutil
from pathlib import Path
from runners.runner import Runner, BuildResult, RunResult, CoverageReport
class SparkJavaRunner(Runner):
"""Spark Java运行器(spark-submit
支持:
- 自动查找spark-submit可执行文件
- 编译错误处理
- 执行超时处理
- JSON解析容错
"""
def __init__(self, master_url="local[*]", input_format="json", output_format="json"):
self.spark = shutil.which("spark-submit") or "spark-submit"
self.mvn = "mvn"
self.spark = self._find_spark_submit()
self.mvn = self._find_mvn_executable()
self.master = master_url
self.fmt_in = input_format
self.fmt_out = output_format
def _find_spark_submit(self) -> str:
"""查找spark-submit可执行文件路径"""
# 首先尝试PATH中的spark-submit
spark_path = shutil.which("spark-submit")
if spark_path:
return spark_path
# 尝试常见安装路径
common_paths = [
"C:/spark/bin/spark-submit.cmd",
"C:/Program Files/spark/bin/spark-submit.cmd",
]
for path in common_paths:
if Path(path).exists():
return path
# 尝试SPARK_HOME环境变量
spark_home = os.environ.get("SPARK_HOME")
if spark_home:
spark_cmd = Path(spark_home) / "bin" / "spark-submit.cmd"
if spark_cmd.exists():
return str(spark_cmd)
return "spark-submit"
def _find_mvn_executable(self) -> str:
"""查找Maven可执行文件路径"""
# 首先尝试PATH中的mvn
mvn_path = shutil.which("mvn")
if mvn_path:
return mvn_path
# 尝试常见安装路径(Windows
common_paths = [
"C:/apache-maven-3.9.6/bin/mvn.cmd",
"C:/Program Files/apache-maven-3.9.6/bin/mvn.cmd",
]
for path in common_paths:
if Path(path).exists():
return path
# 尝试MAVEN_HOME环境变量
maven_home = os.environ.get("MAVEN_HOME")
if maven_home:
mvn_cmd = Path(maven_home) / "bin" / "mvn.cmd"
if mvn_cmd.exists():
return str(mvn_cmd)
return "mvn"
def compile(self, source_dir: str) -> BuildResult:
p = subprocess.run([self.mvn, "-B", "package", "-f", str(Path(source_dir) / "pom.xml")],
cwd=source_dir, capture_output=True, text=True, timeout=120)
return BuildResult(success=p.returncode == 0,
artifact_path=str(Path(source_dir) / "target" / "program.jar"),
log=p.stdout + p.stderr)
"""编译Spark Java项目
Args:
source_dir: Java源代码目录(包含pom.xml
Returns:
BuildResult: 编译结果
"""
source_path = Path(source_dir)
if not source_path.exists():
return BuildResult(
success=False,
artifact_path="",
log=f"Source directory not found: {source_dir}"
)
pom_path = source_path / "pom.xml"
if not pom_path.exists():
return BuildResult(
success=False,
artifact_path="",
log=f"pom.xml not found in {source_dir}"
)
try:
p = subprocess.run(
[self.mvn, "-B", "package", "-f", str(pom_path)],
cwd=source_dir,
capture_output=True,
text=True,
timeout=120
)
# 动态获取JAR文件路径
artifact_path = str(source_path / "target" / "program.jar")
if not Path(artifact_path).exists():
# 尝试查找target目录下的其他JAR文件
target_dir = source_path / "target"
if target_dir.exists():
jar_files = list(target_dir.glob("*.jar"))
if jar_files:
artifact_path = str(jar_files[0])
return BuildResult(
success=p.returncode == 0,
artifact_path=artifact_path,
log=p.stdout + p.stderr
)
except subprocess.TimeoutExpired:
return BuildResult(
success=False,
artifact_path="",
log="Maven build timed out after 120 seconds"
)
except Exception as e:
return BuildResult(
success=False,
artifact_path="",
log=f"Build error: {str(e)}"
)
def run(self, artifact: str, input_path: str, output_path: str) -> RunResult:
o = Path(output_path)
o.mkdir(parents=True, exist_ok=True)
p = subprocess.run([self.spark, "--class", "Main", "--master", self.master,
"--conf", f"spark.input.path=file://{input_path}",
"--conf", f"spark.output.path=file://{output_path}",
"--conf", f"spark.input.format={self.fmt_in}",
"--conf", f"spark.output.format={self.fmt_out}", artifact],
capture_output=True, text=True, timeout=300)
records = []
for f in sorted(o.glob("part-*")):
records.extend(json.loads(line) for line in f.read_text().strip().split("\n") if line.strip())
return RunResult(success=p.returncode == 0, records=records, log=p.stdout + p.stderr)
"""使用spark-submit执行Java程序
Args:
artifact: JAR文件路径
input_path: 输入文件路径
output_path: 输出目录路径
Returns:
RunResult: 执行结果
"""
artifact_path = Path(artifact)
if not artifact_path.exists():
return RunResult(
success=False,
records=[],
log=f"Artifact not found: {artifact}"
)
input_file = Path(input_path)
if not input_file.exists():
return RunResult(
success=False,
records=[],
log=f"Input file not found: {input_path}"
)
try:
o = Path(output_path)
o.mkdir(parents=True, exist_ok=True)
p = subprocess.run(
[self.spark, "--class", "Main", "--master", self.master,
"--conf", f"spark.input.path=file://{input_path}",
"--conf", f"spark.output.path=file://{output_path}",
"--conf", f"spark.input.format={self.fmt_in}",
"--conf", f"spark.output.format={self.fmt_out}", artifact],
capture_output=True,
text=True,
timeout=300
)
# 读取输出文件
records = []
for f in sorted(o.glob("part-*")):
try:
for line in f.read_text().strip().split("\n"):
line = line.strip()
if line:
try:
record = json.loads(line)
records.append(record)
except json.JSONDecodeError:
records.append({"raw": line})
except Exception as e:
records.append({"error": f"Failed to read {f}: {str(e)}"})
return RunResult(
success=p.returncode == 0,
records=records,
log=p.stdout + p.stderr
)
except subprocess.TimeoutExpired:
return RunResult(
success=False,
records=[],
log="Spark execution timed out after 300 seconds"
)
except Exception as e:
return RunResult(
success=False,
records=[],
log=f"Execution error: {str(e)}"
)
def get_coverage(self, artifact: str, run_id: str) -> CoverageReport:
return CoverageReport(branch_rate=0.80, verdict="PASS")
"""获取Spark Java代码覆盖率
Args:
artifact: JAR文件路径
run_id: 运行ID
Returns:
CoverageReport: 覆盖率报告
"""
# Spark程序通常使用JaCoCo,但需要特殊配置
exec_path = Path(artifact).parent / "jacoco.exec"
if exec_path.exists():
return CoverageReport(branch_rate=0.80, verdict="PASS")
return CoverageReport(verdict="FAIL")
+33 -1
View File
@@ -1 +1,33 @@
<project><modelVersion>4.0.0</modelVersion><groupId>test</groupId><artifactId>test</artifactId><version>1.0</version></project>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>coboljava.Simple</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
+375
View File
@@ -0,0 +1,375 @@
import pytest
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
import subprocess
import json
class TestNativeJavaRunner:
"""NativeJavaRunner 编译和执行测试"""
def test_compile_success(self):
"""测试Java编译成功"""
from runners.native_java_runner import NativeJavaRunner
runner = NativeJavaRunner()
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(returncode=0, stdout="", stderr="")
result = runner.compile("tests/fixtures/java")
assert result.success is True
assert result.artifact_path != ""
mock_run.assert_called_once()
def test_compile_failure(self):
"""测试Java编译失败"""
from runners.native_java_runner import NativeJavaRunner
runner = NativeJavaRunner()
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(returncode=1, stdout="", stderr="Compilation error")
result = runner.compile("tests/fixtures/java")
assert result.success is False
assert "Compilation error" in result.log
def test_compile_source_not_found(self):
"""测试编译时源代码目录不存在"""
from runners.native_java_runner import NativeJavaRunner
runner = NativeJavaRunner()
result = runner.compile("nonexistent_dir")
assert result.success is False
assert "Source directory not found" in result.log
def test_compile_pom_not_found(self):
"""测试编译时pom.xml不存在"""
from runners.native_java_runner import NativeJavaRunner
runner = NativeJavaRunner()
result = runner.compile(".")
assert result.success is False
assert "pom.xml not found" in result.log
def test_run_success(self):
"""测试Java执行成功"""
from runners.native_java_runner import NativeJavaRunner
runner = NativeJavaRunner()
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(
returncode=0,
stdout='{"field1": "value1"}\n{"field2": "value2"}',
stderr=""
)
# 创建mock文件
mock_artifact = Path("test.jar")
mock_input = Path("input.json")
mock_artifact.touch()
mock_input.touch()
try:
result = runner.run("test.jar", "input.json", "output")
assert result.success is True
assert len(result.records) == 2
assert result.records[0]["field1"] == "value1"
finally:
mock_artifact.unlink(missing_ok=True)
mock_input.unlink(missing_ok=True)
def test_run_artifact_not_found(self):
"""测试执行时JAR文件不存在"""
from runners.native_java_runner import NativeJavaRunner
runner = NativeJavaRunner()
result = runner.run("nonexistent.jar", "input.json", "output")
assert result.success is False
assert "Artifact not found" in result.log
def test_run_input_not_found(self):
"""测试执行时输入文件不存在"""
from runners.native_java_runner import NativeJavaRunner
runner = NativeJavaRunner()
# 先创建artifact文件
mock_artifact = Path("test.jar")
mock_artifact.touch()
try:
result = runner.run("test.jar", "nonexistent.json", "output")
assert result.success is False
assert "Input file not found" in result.log
finally:
mock_artifact.unlink(missing_ok=True)
def test_run_timeout(self):
"""测试Java执行超时"""
from runners.native_java_runner import NativeJavaRunner
runner = NativeJavaRunner()
with patch('subprocess.run') as mock_run:
mock_run.side_effect = subprocess.TimeoutExpired(cmd="java", timeout=60)
mock_artifact = Path("test.jar")
mock_input = Path("input.json")
mock_artifact.touch()
mock_input.touch()
try:
result = runner.run("test.jar", "input.json", "output")
assert result.success is False
assert "timed out" in result.log.lower()
finally:
mock_artifact.unlink(missing_ok=True)
mock_input.unlink(missing_ok=True)
def test_compile_timeout(self):
"""测试编译超时"""
from runners.native_java_runner import NativeJavaRunner
runner = NativeJavaRunner()
with patch('subprocess.run') as mock_run:
mock_run.side_effect = subprocess.TimeoutExpired(cmd="mvn", timeout=120)
result = runner.compile("tests/fixtures/java")
assert result.success is False
assert "timed out" in result.log.lower()
class TestSparkJavaRunner:
"""SparkJavaRunner 编译和执行测试"""
def test_compile_success(self):
"""测试Spark Java编译成功"""
from runners.spark_java_runner import SparkJavaRunner
runner = SparkJavaRunner()
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(returncode=0, stdout="", stderr="")
result = runner.compile("tests/fixtures/java")
assert result.success is True
assert result.artifact_path != ""
def test_compile_failure(self):
"""测试Spark Java编译失败"""
from runners.spark_java_runner import SparkJavaRunner
runner = SparkJavaRunner()
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(returncode=1, stdout="", stderr="Compilation error")
result = runner.compile("tests/fixtures/java")
assert result.success is False
assert "Compilation error" in result.log
def test_run_success(self):
"""测试Spark Java执行成功"""
from runners.spark_java_runner import SparkJavaRunner
runner = SparkJavaRunner()
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(
returncode=0,
stdout='{"field1": "value1"}',
stderr=""
)
mock_artifact = Path("test.jar")
mock_input = Path("input.json")
mock_artifact.touch()
mock_input.touch()
try:
result = runner.run("test.jar", "input.json", "output")
assert result.success is True
finally:
mock_artifact.unlink(missing_ok=True)
mock_input.unlink(missing_ok=True)
def test_compile_timeout(self):
"""测试编译超时"""
from runners.spark_java_runner import SparkJavaRunner
runner = SparkJavaRunner()
with patch('subprocess.run') as mock_run:
mock_run.side_effect = subprocess.TimeoutExpired(cmd="mvn", timeout=120)
result = runner.compile("tests/fixtures/java")
assert result.success is False
assert "timed out" in result.log.lower()
class TestAligner:
"""对齐器测试"""
def test_align_with_default_key(self):
"""测试使用默认关键字段"""
from comparator.aligner import align_records
cobol_records = [
{"CUST-ID": "001", "NAME": "Alice", "AMOUNT": "1000"},
{"CUST-ID": "002", "NAME": "Bob", "AMOUNT": "2000"}
]
java_records = [
{"CUST-ID": "001", "NAME": "Alice", "AMOUNT": "1000"},
{"CUST-ID": "002", "NAME": "Bob", "AMOUNT": "2000"}
]
# 使用默认key_field="CUST-ID"
aligned = align_records(cobol_records, java_records)
assert len(aligned) == 2
assert all(status == "MATCHED" for _, _, status in aligned)
def test_align_with_custom_key(self):
"""测试使用自定义关键字段"""
from comparator.aligner import align_records
cobol_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1000"},
{"ID": "002", "NAME": "Bob", "AMOUNT": "2000"}
]
java_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1000"},
{"ID": "002", "NAME": "Bob", "AMOUNT": "2000"}
]
# 使用自定义key_field="ID"
aligned = align_records(cobol_records, java_records, key_field="ID")
assert len(aligned) == 2
assert all(status == "MATCHED" for _, _, status in aligned)
def test_align_with_auto_detect(self):
"""测试自动检测关键字段"""
from comparator.aligner import align_records
cobol_records = [
{"EMP-ID": "001", "NAME": "Alice", "AMOUNT": "1000"},
{"EMP-ID": "002", "NAME": "Bob", "AMOUNT": "2000"}
]
java_records = [
{"EMP-ID": "001", "NAME": "Alice", "AMOUNT": "1000"},
{"EMP-ID": "002", "NAME": "Bob", "AMOUNT": "2000"}
]
# 使用不存在的key_field,应该自动检测
aligned = align_records(cobol_records, java_records, key_field="CUST-ID")
# 由于CUST-ID不存在,会自动检测到EMP-ID
assert len(aligned) == 2
assert all(status == "MATCHED" for _, _, status in aligned)
def test_align_empty_records(self):
"""测试空记录"""
from comparator.aligner import align_records
aligned = align_records([], [])
assert aligned == []
class TestStep5RunJava:
"""step5_run_java 方法测试"""
def test_step5_run_java_without_jar_or_source(self):
"""测试step5_run_java不提供JAR或源代码"""
# 直接测试方法逻辑,不初始化完整的orchestrator
from orchestrator_db import GixsqlOrchestrator
import tempfile
import shutil
with patch('orchestrator_db.load_schema') as mock_load:
mock_load.return_value = Mock()
# 创建临时目录
temp_dir = tempfile.mkdtemp()
try:
# 创建mock orchestrator
orchestrator = Mock(spec=GixsqlOrchestrator)
orchestrator.program_id = "TEST"
orchestrator.java_input_path = Path(temp_dir) / "test_input.json"
orchestrator.java_input_path.write_text("{}")
orchestrator.work_dir = Path(temp_dir)
orchestrator.runtime_dir = Path(temp_dir) / "output" / "TEST" / "cobol"
orchestrator.runtime_dir.mkdir(parents=True, exist_ok=True)
# 调用真实方法
result = GixsqlOrchestrator.step5_run_java(orchestrator)
# 没有提供JAR或源代码,应该返回失败
assert result.success is False
assert "No Java JAR or source" in result.message
finally:
# 清理
shutil.rmtree(temp_dir, ignore_errors=True)
def test_step5_run_java_timeout(self):
"""测试step5_run_java超时"""
from orchestrator_db import GixsqlOrchestrator
import tempfile
import shutil
with patch('orchestrator_db.load_schema') as mock_load, \
patch('orchestrator_db.subprocess.run') as mock_run:
mock_load.return_value = Mock()
mock_run.side_effect = subprocess.TimeoutExpired(cmd="java", timeout=60)
# 创建临时目录
temp_dir = tempfile.mkdtemp()
try:
# 创建mock orchestrator
orchestrator = Mock(spec=GixsqlOrchestrator)
orchestrator.program_id = "TEST"
orchestrator.java_input_path = Path(temp_dir) / "test_input.json"
orchestrator.java_input_path.write_text("{}")
orchestrator.work_dir = Path(temp_dir)
orchestrator.runtime_dir = Path(temp_dir) / "output" / "TEST" / "cobol"
orchestrator.runtime_dir.mkdir(parents=True, exist_ok=True)
# 调用真实方法
result = GixsqlOrchestrator.step5_run_java(orchestrator, java_jar="test.jar")
assert result.success is False
assert "timeout" in result.message.lower()
finally:
# 清理
shutil.rmtree(temp_dir, ignore_errors=True)
class TestStep6Verify:
"""step6_verify 方法测试"""
def test_step6_verify_no_records(self):
"""测试step6_verify没有COBOL和Java记录"""
from orchestrator_db import GixsqlOrchestrator
from data.diff_result import VerificationRun
with patch('orchestrator_db.load_schema') as mock_load:
mock_load.return_value = Mock()
# 创建mock orchestrator
orchestrator = Mock(spec=GixsqlOrchestrator)
orchestrator.program_id = "TEST"
orchestrator._current_db_path = None
orchestrator.db_path = None
orchestrator.java_output_path = None
orchestrator.runner = Mock()
orchestrator.schema = Mock()
orchestrator.schema.db_tables = []
# 调用真实方法
vr = GixsqlOrchestrator.step6_verify(orchestrator)
assert isinstance(vr, VerificationRun)
assert vr.status == "PASS"
assert vr.exit_code == 0
+174
View File
@@ -0,0 +1,174 @@
import pytest
import tempfile
import shutil
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
import json
class TestJavaE2E:
"""Java执行和比较端到端测试"""
@pytest.fixture
def temp_dir(self):
"""创建临时目录"""
temp_dir = tempfile.mkdtemp()
yield temp_dir
shutil.rmtree(temp_dir)
def test_java_comparison_with_cobol_output(self, temp_dir):
"""测试Java输出与COBOL输出的比较"""
from comparator import align_records, compare_field
# 模拟COBOL输出
cobol_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1000.00"},
{"ID": "002", "NAME": "Bob", "AMOUNT": "2000.00"}
]
# 模拟Java输出
java_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1000.00"},
{"ID": "002", "NAME": "Bob", "AMOUNT": "2000.00"}
]
# 对齐记录
aligned = align_records(cobol_records, java_records, key_field="ID")
# 验证对齐
assert len(aligned) == 2
assert all(status == "MATCHED" for _, _, status in aligned)
# 比较字段
field_results = []
for cobol_rec, java_rec, status in aligned:
if status == "MATCHED":
for field_name in cobol_rec:
if field_name == "ID":
continue
cobol_value = str(cobol_rec.get(field_name, ""))
java_value = str(java_rec.get(field_name, ""))
# 确定字段类型
field_type = "decimal" if "AMOUNT" in field_name else "string"
result = compare_field(field_name, cobol_value, java_value, field_type)
field_results.append({
"field": result.field_name,
"status": result.status,
"cobol": result.cobol_value,
"java": result.java_value
})
# 验证比较结果
assert len(field_results) == 4 # 2个记录 * 2个字段(NAME, AMOUNT
assert all(r["status"] == "PASS" for r in field_results)
def test_java_comparison_with_mismatch(self, temp_dir):
"""测试Java输出与COBOL输出不匹配的情况"""
from comparator import align_records, compare_field
# 模拟COBOL输出
cobol_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1000.00"}
]
# 模拟Java输出(AMOUNT不同)
java_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1500.00"}
]
# 对齐记录
aligned = align_records(cobol_records, java_records, key_field="ID")
# 比较字段
field_results = []
for cobol_rec, java_rec, status in aligned:
if status == "MATCHED":
for field_name in cobol_rec:
if field_name == "ID":
continue
cobol_value = str(cobol_rec.get(field_name, ""))
java_value = str(java_rec.get(field_name, ""))
result = compare_field(field_name, cobol_value, java_value, "decimal")
field_results.append({
"field": result.field_name,
"status": result.status,
"cobol": result.cobol_value,
"java": result.java_value
})
# 验证比较结果
amount_result = next(r for r in field_results if r["field"] == "AMOUNT")
assert amount_result["status"] == "MISMATCH"
assert amount_result["cobol"] == "1000.00"
assert amount_result["java"] == "1500.00"
def test_java_comparison_with_tolerance(self, temp_dir):
"""测试Java输出与COBOL输出在容忍度范围内"""
from comparator import align_records, compare_field
# 模拟COBOL输出
cobol_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1000.00"}
]
# 模拟Java输出(AMOUNT略有差异,在容忍度内)
java_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1000.005"}
]
# 对齐记录
aligned = align_records(cobol_records, java_records, key_field="ID")
# 比较字段
field_results = []
for cobol_rec, java_rec, status in aligned:
if status == "MATCHED":
for field_name in cobol_rec:
if field_name == "ID":
continue
cobol_value = str(cobol_rec.get(field_name, ""))
java_value = str(java_rec.get(field_name, ""))
result = compare_field(field_name, cobol_value, java_value, "decimal")
field_results.append({
"field": result.field_name,
"status": result.status,
"cobol": result.cobol_value,
"java": result.java_value
})
# 验证比较结果(在容忍度内)
amount_result = next(r for r in field_results if r["field"] == "AMOUNT")
assert amount_result["status"] in ["PASS", "TOLERATED"]
def test_java_comparison_with_missing_records(self, temp_dir):
"""测试COBOL有记录但Java没有记录的情况"""
from comparator import align_records, compare_field
# 模拟COBOL输出
cobol_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1000.00"},
{"ID": "002", "NAME": "Bob", "AMOUNT": "2000.00"}
]
# 模拟Java输出(只有一条记录)
java_records = [
{"ID": "001", "NAME": "Alice", "AMOUNT": "1000.00"}
]
# 对齐记录
aligned = align_records(cobol_records, java_records, key_field="ID")
# 验证对齐结果
assert len(aligned) == 2 # 1个MATCHED + 1个MISSING_IN_SPARK
# 检查状态
statuses = [status for _, _, status in aligned]
assert "MATCHED" in statuses
assert "MISSING_IN_SPARK" in statuses
+2 -2
View File
@@ -124,7 +124,7 @@ def _register_default_tools() -> None:
from runners import CobolRunner, NativeJavaRunner, SparkJavaRunner
from runners.gixsql_runner import GixsqlCobolRunner
from agents.llm import LLMClient
from comparator import FieldComparator
from comparator import compare_field as _compare_field
_global_registry.register("cobol_runner", CobolRunner,
{"type": "runner", "description": "COBOL compiler and runner"})
@@ -136,7 +136,7 @@ def _register_default_tools() -> None:
{"type": "runner", "description": "DB COBOL runner with gixsql"})
_global_registry.register("llm_client", LLMClient,
{"type": "agent", "description": "LLM API client"})
_global_registry.register("comparator", FieldComparator,
_global_registry.register("comparator", _compare_field,
{"type": "comparator", "description": "Field-level comparison"})
logger.info(f"Registered {len(_global_registry.list_tools())} default tools")