# 03 - runners 编译运行引擎 ## 1. 模块概述 `runners` 模块负责 COBOL 程序与 Java 程序的**编译、运行、覆盖率采集**全流程。模块采用策略模式,通过抽象基类 `Runner` 统一不同语言运行时的接口,对外暴露一致的 `compile -> run -> get_coverage` 三阶段管线。 核心职责: | 职责 | 说明 | |------|------| | COBOL 编译 | 调用 `cobc`(COBOL 编译器)将 `.cbl` 源码编译为可执行文件 | | Java 编译 | 调用 `mvn package` 将 Maven 项目打包为 `.jar` | | 运行执行 | 通过 `subprocess` 启动编译产物,捕获 stdout/stderr 和返回码 | | 测试数据写入 | 将 `TestCase` 列表序列化为 COBOL 二进制 / JSON 格式 | | 覆盖率报告 | 采集分支覆盖数据(gcov / JaCoCo)并返回量化报告 | --- ## 2. 文件清单 ``` runners/ __init__.py # 包导出:公开 API 声明 runner.py # 抽象基类 Runner + 数据类 BuildResult / RunResult / CoverageReport cobol_runner.py # COBOL 编译·执行器(cobc 管线) gixsql_runner.py # DB COBOL 程序编译·执行器(gixpp + cobc + gixsql 链接) native_java_runner.py # Java 本地运行器(mvn + java -jar) spark_java_runner.py # Spark 运行器(spark-submit) data_writer.py # 测试数据序列化(COBOL 二进制 / Spark JSON / Native JSON) ``` --- ## 3. 接口定义 ### 3.1 数据类(runner.py) ```python @dataclass class BuildResult: success: bool # 编译是否成功 artifact_path: str = "" # 编译产物路径(.exe / .jar) log: str = "" # 编译日志(stdout + stderr) @dataclass class RunResult: success: bool # 运行是否成功(returncode == 0) records: list[dict] # 运行输出记录(JSON 格式) log: str = "" # 运行日志 coverage_exec: str = "" # 覆盖率执行文件路径 @dataclass class CoverageReport: branch_rate: float = 0.0 # 分支覆盖率(0.0 ~ 1.0) covered_branches: int = 0 # 已覆盖分支数 total_branches: int = 0 # 总分支数 verdict: str = "PASS" # 判定结果(PASS / FAIL) ``` ### 3.2 抽象基类(runner.py) ```python class Runner(ABC): @abstractmethod def compile(self, source_dir: str) -> BuildResult: ... @abstractmethod def run(self, artifact: str, input_path: str, output_path: str) -> RunResult: ... @abstractmethod def get_coverage(self, artifact: str, run_id: str) -> CoverageReport: ... ``` ### 3.3 CobolRunner(cobol_runner.py) | 方法 | 签名 | 说明 | |------|------|------| | `compile` | `(src, dialect="ibm", gcov=False) -> BuildResult` | 旧式编译,`-std=ibm-strict`,供 orchestrator.py 使用 | | `run` | `(binary, input_path, output_path) -> RunResult` | 旧式执行,stdin 管道 stdout | | `compile_with_links` | `(src, work_dir, copybook_dirs, sub_objects, gcov) -> BuildResult` | 新式编译:主程序 + 链接 SUB.o,支持 COPYBOOK 搜索路径和 gcov | | `run_file_based` | `(binary, run_dir, input_files, timeout) -> RunResult` | 新式执行:基于文件的 I/O,将输入文件复制到运行目录后启动程序 | ### 3.4 GixsqlCobolRunner(gixsql_runner.py) | 方法 | 签名 | 说明 | |------|------|------| | `preprocess` | `(src_path, out_dir, copybook_dirs) -> str` | gixpp 预处理:COPY 展开、SQL 归一化、格式修正 | | `compile` | `(pp_path, exe_path, copybook_dirs, extra_srcs) -> GixsqlBuildResult` | cobc 编译,链接 gixsql 库 | | `run` | `(exe_path, work_dir, db_path, ...) -> GixsqlRunResult` | 执行 DB 程序,设置 SQLite 数据库路径 | | `read_db_tables` | `(db_path, table_names) -> list[GixsqlTableData]` | 读取 SQLite 数据库表内容 | ### 3.5 NativeJavaRunner(native_java_runner.py) | 方法 | 签名 | 说明 | |------|------|------| | `compile` | `(source_dir) -> BuildResult` | Maven 打包,输出 target/program.jar | | `run` | `(artifact, input_path, output_path) -> RunResult` | java -jar 执行,解析 stdout JSON 行 | | `get_coverage` | `(artifact, run_id) -> CoverageReport` | 检查 jacoco.exec 是否存在,返回覆盖率 | ### 3.6 SparkJavaRunner(spark_java_runner.py) | 方法 | 签名 | 说明 | |------|------|------| | `compile` | `(source_dir) -> BuildResult` | Maven 打包,输出 target/program.jar | | `run` | `(artifact, input_path, output_path) -> RunResult` | spark-submit 执行,读取 part-\* 输出文件 | | `get_coverage` | `(artifact, run_id) -> CoverageReport` | 返回固定 0.80 覆盖率(Spark 无原生覆盖率集成) | ### 3.7 DataWriter(data_writer.py) | 方法 | 签名 | 说明 | |------|------|------| | `write_cobol_binary` | `(cases, out)` | 将 TestCase 列表写为 COBOL 二进制格式(大端序 int64 / float64 / ASCII) | | `write_spark_json` | `(cases, cfg, d)` | 写 Spark 输入 JSON(part-00000.json),key 字段加序号后缀 | | `write_native_json` | `(cases, out)` | 写 Native JSON(每行一个 JSON 对象) | --- ## 4. 编译流程 ### 4.1 COBOL 编译(cobol_runner.py) #### 旧式编译流程 ``` .cbl 源文件 | v cobc -x -std=ibm-strict [-o 输出路径] [-g] [--coverage] 源文件 | v BuildResult(success, artifact_path, log) ``` - 默认超时 30 秒 - `gcov=True` 时追加 `--coverage` 参数生成 `.gcno` 文件 #### 新式编译流程(带链接) ``` .cbl 源文件 + SUB*.o 对象文件 | v cobc -x -g [--coverage] [-I COPYBOOK路径...] -o 输出.exe 源文件 SUB1.o SUB2.o ... | v BuildResult(success, exe_path, log) ``` - 默认超时 120 秒 - 工作目录切换至 `work_dir`(确保 `.gcno` 产出位置正确) - 不使用 `-std=ibm-strict`,依赖默认方言 ### 4.2 DB COBOL 编译(gixsql_runner.py) DB 程序编译采用 gixpp + cobc 两阶段管线: ``` 原始 .cbl 源文件 | v -- _normalize_source() | 1. 剥离注释行(避免日文注释中的 EXEC SQL 被误匹配) | 2. EXEC SQL INCLUDE SQLCA -> COPY SQLCA | 3. EXEC SQL CONNECT TO 'literal' -> CONNECT TO :WS-GIX-CONN USER :WS-GIX-USR | 4. COPY REPLACING 内联展开(Python 侧) | 5. ALL COPY 展开(替代 cobc -E) | 6. 关键字间多空格压缩 | 7. DIVISION/SECTION 头部列位置修正(Area A,列 8 起) | 8. SELECT ... FROM ... INTO -> SELECT ... INTO ... FROM(gixpp 要求 INTO 在前) | 9. CURRENT TIMESTAMP -> CURRENT_TIMESTAMP(SQLite 后端兼容) | 10. DB2 schema 限定表名去限定(SCHEMA.TABLE -> TABLE) v _norm.cbl(规范源文件) | v -- preprocess() | gixpp -i _norm.cbl -o _pp.cbl -e [-I COPYBOOK路径...] | 将 EXEC SQL 块转换为 GIXSQL 调用序列 v _pp.cbl(预处理后源文件) | v -- compile() | 1. _patch_sql_identifiers():DB2 连字符标识符 -> 下划线(SQLite 兼容) | 2. _patch_sqlcode_normalize():SQLite 约束错误码映射为 DB2 标准码 | 3. cobc -x -L gixsql库路径 -K GIXSQL*函数 -l gixsql [-I COPYBOOK...] -o exe pp.cbl v GixsqlBuildResult(success, exe_path, log) ``` ### 4.3 Java 编译(native_java_runner.py / spark_java_runner.py) 两个 Java Runner 共享相同的 Maven 编译流程: ``` source_dir/pom.xml | v mvn -B package -f pom.xml | v source_dir/target/program.jar | v BuildResult(success, artifact_path, log) ``` - 默认超时 120 秒 - 使用 `-B`(batch mode)避免交互式提示 - 产物固定为 `target/program.jar` --- ## 5. 运行流程 ### 5.1 COBOL 执行 #### 旧式执行(stdin 到 stdout) ``` input_path(二进制数据) | v subprocess.run([binary], input=data, capture_output=True) | v stdout 写入 output_path | v RunResult(success) ``` - 超时 30 秒 - 无输出记录解析(仅写入文件) #### 新式执行(基于文件) ``` input_files: {assign_name: source_path} | v 复制输入文件到 run_dir | v subprocess.run([binary], cwd=run_dir, capture_output=True) | v RunResult(success, log) ``` - 超时 60 秒(可配置) - 工作目录为 `run_dir`,程序通过 ASSIGN 名读取文件 - 日志截断至 2000 字符 ### 5.2 DB COBOL 执行(gixsql_runner.py) ``` exe_path + db_path(SQLite 数据库) | v 部署 DLL 到 exe_dir(libgixsql.dll, libgixsql-sqlite.dll 等) | v 复制输入文件到 work_dir | v subprocess.run([exe], cwd=work_dir, env={GIXSQL_DB_PATH: db_path}) | v GixsqlRunResult(success, returncode, db_path, log) | v read_db_tables(db_path, table_names) -> 读取数据库输出 ``` - 超时 30 秒(可配置) - 环境变量 `GIXSQL_DB_PATH` 指向 SQLite 数据库 - DLL 部署策略:优先 lib_path,回退到 gixpp bin 目录 - returncode 0 或 1 均视为成功(COBOL STOP RUN 返回码差异) ### 5.3 Java 执行 #### NativeJavaRunner ``` input_path(JSON 文件) | v java -jar artifact(stdin 输入) | v 解析 stdout JSON 行 -> records 列表 | v RunResult(success, records, log) ``` - 超时 60 秒 - 每行一个 JSON 对象 #### SparkJavaRunner ``` input_path(JSON 文件) | v spark-submit --class Main --master local[*] | --conf spark.input.path=... | --conf spark.output.path=... | --conf spark.input.format=json | --conf spark.output.format=json | artifact | v 读取 output_path/part-* 文件 -> records 列表 | v RunResult(success, records, log) ``` - 超时 300 秒(5 分钟) - 输入输出格式通过 spark conf 配置 - 输出文件自动 glob 匹配 `part-*` --- ## 6. 错误处理 ### 6.1 编译失败 | 场景 | 处理方式 | 返回值 | |------|----------|--------| | cobc 编译错误 | 捕获 returncode != 0 | `BuildResult(success=False, log=stdout+stderr)` | | cobc 超时 | 捕获 TimeoutExpired | `BuildResult(success=False, log="Compile timeout")` | | gixpp 预处理失败 | 捕获 returncode != 0,抛出 RuntimeError | `raise RuntimeError("gixpp failed")` | | Maven 编译错误 | 捕获 returncode != 0 | `BuildResult(success=False, log=stdout+stderr)` | ### 6.2 运行时异常 | 场景 | 处理方式 | 返回值 | |------|----------|--------| | COBOL 程序异常终止 | 捕获 returncode != 0 | `RunResult(success=False, log=stdout+stderr)` | | COBOL 程序超时 | 捕获 TimeoutExpired | `RunResult(success=False, log="Run timeout")` | | DB 程序 returncode 1 | 视为成功(COBOL 语义差异) | `GixsqlRunResult(success=True)` | | Java 程序异常 | 捕获 returncode != 0 | `RunResult(success=False, log=stdout+stderr)` | | Spark 程序超时 | 捕获 TimeoutExpired(300s) | `RunResult(success=False, log="Run timeout")` | | DLL 未找到 | 运行时加载失败,日志记录 | `GixsqlRunResult(success=False, log=...)` | ### 6.3 Gixsql 专用处理 | 机制 | 说明 | |------|------| | SQL 标识符归一化 | DB2 连字符标识符自动转换为下划线(`EMP-MASTER` -> `EMP_MASTER`) | | SQLCODE 映射 | SQLite 约束错误码(-1555/-2067/-19)映射为 DB2 标准码(-803) | | Schema 限定去限定 | `SCHEMA.TABLE` -> `TABLE`(SQLite 无 schema 概念) | | CURRENT TIMESTAMP | DB2 `CURRENT TIMESTAMP` -> SQLite `CURRENT_TIMESTAMP` | | DLL 自动部署 | 运行前将 gixsql DLL 复制到 exe_dir 确保加载器找到 | ### 6.4 日志截断 所有 Runner 的日志均截断至固定长度以避免内存溢出: | Runner | 日志截断长度 | |--------|-------------| | CobolRunner(旧式) | 无截断(完整 stdout+stderr) | | CobolRunner(新式) | 2000 字符 | | GixsqlCobolRunner | 500-1000 字符 | | NativeJavaRunner | 无截断(完整 stdout+stderr) | | SparkJavaRunner | 无截断(完整 stdout+stderr) | --- ## 7. 设计特点 ### 7.1 双轨架构 CobolRunner 维护两套接口: - **旧式接口**(`compile` + `run`):供 `orchestrator.py` 使用,兼容现有调用链 - **新式接口**(`compile_with_links` + `run_file_based`):支持非 DB 程序的文件 I/O 和 SUB.o 链接 两套接口互不干扰,通过不同方法名区分。 ### 7.2 Gixsql 预处理管线 GixsqlCobolRunner 的预处理是模块中最复杂的部分: 1. **Python 侧 COPY 展开**:替代 `cobc -E`,解决 gixpp ESQL 解析器对 COPY REPLACING 伪文本的兼容问题 2. **SQL 归一化**:处理 DB2 与 SQLite 的语法差异(标识符、时间函数、schema 限定符) 3. **SQLCODE 映射**:注入代码将 SQLite 特定错误码转换为 DB2 标准码,确保 `IF SQLCODE = -803` 分支可达 ### 7.3 DLL 部署策略 gixsql 运行时需要多个 DLL(libgixsql.dll, libgixsql-sqlite.dll 等)。部署策略: 1. 优先从 `lib_path` 复制 2. 若不存在,回退到 `gixpp` bin 目录 3. 检查文件大小避免重复复制 ### 7.4 DataWriter 格式 | 格式 | 字节序 | 数值类型 | 字符串处理 | |------|--------|----------|------------| | COBOL 二进制 | 大端序(Network Byte Order) | int64 (`>q`) / float64 (`>d`) | ASCII,右补空格至 10 字节 | | Spark JSON | N/A | JSON 数字 | UTF-8,key 字段加序号后缀 | | Native JSON | N/A | JSON 数字 | UTF-8,每行一个 JSON 对象 | --- ## 8. 依赖关系 ``` runners/ runner.py -> 无外部依赖 cobol_runner.py -> runners.runner, subprocess, pathlib gixsql_runner.py -> runners.runner(仅类型引用),subprocess, sqlite3, pathlib, logging native_java_runner.py -> runners.runner, subprocess, json, shutil, pathlib spark_java_runner.py -> runners.runner, subprocess, json, shutil, pathlib data_writer.py -> data.test_case.TestCase, struct, json, pathlib ``` 外部工具依赖: | 工具 | 用途 | 使用者 | |------|------|--------| | `cobc` | COBOL 编译器 | CobolRunner, GixsqlCobolRunner | | `gixpp` | COBOL EXEC SQL 预处理器 | GixsqlCobolRunner | | `mvn` | Java 构建工具 | NativeJavaRunner, SparkJavaRunner | | `java` | Java 运行时 | NativeJavaRunner | | `spark-submit` | Spark 提交工具 | SparkJavaRunner | | `libgixsql.dll` | gixsql 运行时库 | GixsqlCobolRunner(运行时链接) | | `libgixsql-sqlite.dll` | SQLite 后端驱动 | GixsqlCobolRunner(运行时加载) |