# jtestlib **Repository Path**: chuanghou/jtestlib ## Basic Information - **Project Name**: jtestlib - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-17 - **Last Updated**: 2026-05-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # jtestlib > 用配置驱动的 Python 编排,把 Java 多模块本地联调变成「按需启动、快速验证、可重复执行」的工程化流程。 `jtestlib` 是 **Java 多模块项目** 的本地集成测试框架库:负责编译、依赖准备、进程启停、日志、HTTP/DB/SSH、数据库 schema 工具与 JaCoCo;业务项目只需在 `test/jtest.py` 写配置,在 `test//test.py` 写断言。 **设计目标** | 目标 | 做法 | |------|------| | 快 | 只启动当前套件需要的运行模块与节点 | | 稳 | `-a` 前自动 `-kd`;批量默认不写控制台 Java 日志,落盘到套件目录 | | 可复用 | 库与业务解耦,换项目主要改 `test/jtest.py` | | 可观测 | 按 `service-node` 分日志;`-a` 汇总 `test/jtest-last-run-all.txt` | --- ## 1. 在三方项目中的位置 ```text / pom.xml # Maven 父工程 dmatch-*-server/ # 运行模块(有 main) dmatch-*-api|common/ # 辅助模块(仅 classpath) test/ jtest.py # ★ 项目配置 + CLI 入口(调用 jtestlib) application-jtest.yml # 可选,全局 jtest profile application-.yml # 可选,-e 对应环境覆盖 / test.py # ★ 套件:run_suite + verify() application-server--0.yml server-0.log # 运行后生成 jtestlib/ # ★ 本库(可拷贝或 submodule) ``` **职责划分** - **`test/jtest.py`**:`ProjectConfig`、`infra_ops_by_env`、CLI 入口(`run_jtest_cli`) - **`jtestlib/`**:通用能力实现(本文档) - **`test//test.py`**:场景数据准备 + `verify(project) -> bool` --- ## 2. 源码模块一览 | 文件 | 职责 | |------|------| | `models.py` | `ProjectConfig`、`RuntimeModule`、`MysqlConfig`、`InfraOpsConfig`、`SuiteContext`;`resolve_infra_ops` | | `cli_runner.py` | 全局参数解析(`-k/-d/-a/-sc/-cs/...`)、分发到各子命令 | | `suite_runner.py` | `run_suite` / `run_all_suites`、套件发现、汇总表、子进程拉起 | | `runtime.py` | JVM 启动、classpath、profile 合并、端口 kill、日志派发、JaCoCo agent | | `maven_ops.py` | `mvn compile` / `install`(support)/ `dependency:copy-dependencies` | | `db_ops.py` | MySQL 连接、查询/执行、启动前 TRUNCATE、`mysql_list_base_tables` | | `schema_compare.py` | `-sc`:两环境 schema diff(表/列/索引) | | `schema_copy.py` | `-cs`:整库结构+数据复制(先 DROP 目标表);`-cc`:单表复制 | | `http_utils.py` | HTTP 请求、Actuator 健康检查 | | `remote_ops.py` | SSH 执行、SFTP 上传/下载 | | `jtest_orm.py` | 轻量 ORM:`BaseMapper`、`QueryWrapper`、`UpdateWrapper` | | `entity_generator.py` | `-db`:从 MySQL 元数据生成 dataclass 实体 | | `jacoco_ops.py` | `-ja/-b`:采集、merge、HTML/XML 报告、增量覆盖说明 | | `encoding_utils.py` | Windows 控制台 UTF-8 | | `init_project.py` | 在新仓库脚手架生成 `test/jtest.py` 模板 | | `install_extra_deps.py` | 安装 `pymysql`、`paramiko` | 对外导出见 `jtestlib/__init__.py`。 --- ## 3. 配置模型(`models.py`) ### 3.1 `ProjectConfig` ```python ProjectConfig( project_root=Path(...), runtime_modules={ # 可启动的 Java 模块 "server": RuntimeModule( maven_module="dmatch-manage-server", main_class="com.example.Application", port_base=18000, # 节点 n 监听 port_base + n directory=None, # 默认 project_root / maven_module default_nodes=(0,), java_opts=(), http_path_prefix=None, # 如 "/api",拼到 service_http_base 后 ), }, support_modules=("dmatch-manage-api", "dmatch-manage-common"), global_java_opts=("-Xms512m", ...), stop_port_span=10, # kill 时扫描 [port_base, port_base+span) infra_ops_by_env={...}, # 见下 suite_exclude=("slow_suite",), # -a 时跳过 ) ``` ### 3.2 `RuntimeModule` vs `support_modules` | 类型 | 启动进程 | `mvn compile` | `mvn install` | `libs/*` copy | |------|----------|---------------|---------------|----------------| | runtime_modules | 是 | 是 | 否(随 reactor compile) | 是 | | support_modules | 否 | 是 | 是(`-pl -am`) | 否 | ### 3.3 `infra_ops_by_env` 外层 key 与 CLI **`-e`** 一致(默认 `jtest`)。解析逻辑:`resolve_infra_ops(project, env)` → 先精确匹配,否则回退 `jtest`。 ```python infra_ops_by_env={ "jtest": InfraOpsConfig( mysqls={"default": MysqlConfig(host=..., database=..., ...)}, remotes={"default": RemoteConfig(host=..., ...)}, ), "staging": ..., } ``` **`MysqlConfig` 扩展字段(启动前清库)** | 字段 | 默认 | 含义 | |------|------|------| | `truncate_before_start` | `True` | 套件启动 Java **之前** TRUNCATE 该连接库下全部基表 | | `truncate_exclude_tables` | `()` | 排除表名(大小写不敏感) | 共享库、schema 基线库等请设 `truncate_before_start=False`。 --- ## 4. CLI 命令参考 在项目根执行:`python test/jtest.py [选项] [套件名]` ### 4.1 套件运行 | 命令 | 说明 | |------|------| | `python test/jtest.py ` | 运行单套件;**控制台默认打印** Java 日志 | | `python test/jtest.py -c ` | 先 `mvn compile` 再跑套件 | | `python test/jtest.py -a` | 跑全部套件(先 **`-kd`**);**控制台默认不打印** Java 日志 | | `python test/jtest.py -e ` | 指定环境(Spring profile + infra) | | `python test/jtest.py -s ARG ` | 向套件传参(`get_active_suite_context().suite_arg`) | `-a` 结束后查看:`test/jtest-last-run-all.txt`(覆盖写入,含各套件耗时与 pass/fail)。 **`suite_exclude`**:在 `ProjectConfig` 中配置;仅影响 `-a`,单跑某套件不受影响。 ### 4.2 构建与进程 | 命令 | 说明 | |------|------| | `-k` | 按各运行模块 `port_base`~`port_base+stop_port_span-1` 杀监听进程 | | `-d` | `compile` + `install` support + `copy-dependencies` → `/libs` | | `-kd` | `-k` 后 `-d`(`-a` 内部等价先执行此流程) | ### 4.3 数据库工具(均使用两侧 `mysqls["default"]`) | 命令 | 说明 | |------|------| | `-sc <基线env> <目标env>` | 对比 schema(表、列类型/可空/默认值/键、索引);报告 `test/jtest-schema-compare-<基线>-vs-<目标>.txt`;有差异 exit=1 | | `-cs <源env> <目标env>` | 整库复制:目标先 **DROP 全部基表**,再 `SHOW CREATE TABLE` + 批量 `INSERT`;源/目标同库拒绝 | | `-cc <源env> <目标env> <表名>` | 单表复制:仅复制指定基表;目标若已有该表则 **先 DROP**,再复制结构+数据;源/目标同库拒绝 | `-sc`/`-cs` 需 **两个环境名**;`-cc` 需 **三个参数**(源、目标、表名),少参数时 argparse 直接报错。 示例: ```bash python test/jtest.py -sc jtest_ops hc python test/jtest.py -cs jtest_ops hc python test/jtest.py -cc jtest_ops hc rqtg_contract ``` ### 4.4 JaCoCo | 命令 | 说明 | |------|------| | `-ja ` | 单套件:结束 merge,生成 `report/index.html`、`jacoco.xml` 等 | | `-ja -a` | 批量:各套件只采集,**全部结束后统一** merge | | `-ja -b ` | 配合 `-ja`:git diff 增量指令覆盖说明页 | 基线分支:环境变量 `JTEST_JACOCO_BASE_REF`,或自动探测 `main` / `origin/main` / `master`。 ### 4.5 实体生成 | 命令 | 说明 | |------|------| | `python test/jtest.py -db [mysql_name]` | 按当前 `-e` 的 infra,从 MySQL 生成 dataclass 到 `test/entity.py`(默认 `mysql_name=default`) | ### 4.6 其它约定 - **无参数**运行 `python test/jtest.py` → 打印用法并 exit=1(防误触)。 - **`-e`** 写入子进程环境变量 `JTEST_ENV`;直接跑 `test//test.py` 时需自行 `export JTEST_ENV=...`。 - **Windows**:启动前检查端口是否落在系统 TCP 排除段(Hyper-V/WSL);可用 `JTEST_SKIP_WINDOWS_PORT_CHECK=1` 跳过(不推荐)。 --- ## 5. 单次套件执行流程 ```mermaid sequenceDiagram participant CLI as test/jtest.py participant SR as suite_runner participant DB as db_ops participant RT as runtime participant JVM as Java 进程 CLI->>SR: run_single_suite → subprocess test//test.py SR->>SR: run_suite() SR->>SR: 清空 test//*.log SR->>SR: 检查 target/classes、libs SR->>DB: truncate_mysql_before_start (按 -e) SR->>RT: restart_suite_modules RT->>RT: kill 端口段 loop 每个 service × node RT->>JVM: java -Dspring.profiles.active=... RT->>RT: wait_for_actuator_health end SR->>SR: verify(project) SR->>RT: shutdown JVM (JaCoCo 时温和退出) ``` **`run_suite` 钩子** - `before_restart_modules(project)` 或 `(project, suite_arg)`:在清库/启动前执行(如起 Docker) - `after_suite(project)`:套件结束清理 **`module_nodes` 为空**:不启动任何 Java(纯 DB/脚本套件),跳过 classes/libs 自检。 --- ## 6. 运行时细节(`runtime.py`) ### 6.1 Classpath(低 → 高优先级路径) 1. `test//`(节点 yml) 2. `test/`(全局 yml) 3. 当前运行模块 `target/classes` 4. 所有 **runtime** 模块 `target/classes` 5. 所有 **support** 模块 `target/classes` 6. 当前运行模块 `libs/*` ### 6.2 Spring `profiles.active` 合并顺序(逗号连接): 1. `jtest`(仅当存在 `test/application-jtest.yml`) 2. **`-e` 环境名**(与 `jtest` 重名则不重复) 3. **节点 profile**:`{service}-{suite_dir_name}-{node}` 节点配置文件 **必须存在**: ```text test//application-{service}-{suite_dir_name}-{node}.yml ``` ### 6.3 JVM 参数五层合并 (低 → 高,高覆盖低) 1. `project.global_java_opts` 2. `runtime_module.java_opts` 3. `suite_java_opts`(`run_suite` 传入) 4. `suite_module_java_opts[service]` 5. `suite_node_java_opts[service][node]` 规则:`-Dkey=...` 按 **key** 覆盖;其它 flag 按层追加。 ### 6.4 日志 | 场景 | 控制台 | 文件 | |------|--------|------| | 单套件 | 打印 | `test//{service}-{node}.log` | | `-a` | 不打印 | 同上 | 日志经后台线程派发;每行带时间戳、`[service:node]`、STDOUT/STDERR 标记。 ### 6.5 端口与 kill - 监听端口:`port_base + node` - `kill_all` / `kill_module_span`:只杀 **LISTEN** 该端口的进程(避免误杀客户端连接) - Windows:`netstat` + `taskkill`;Unix:`lsof` + `kill` --- ## 7. 数据库能力 ### 7.1 在套件内使用 需在 `run_suite` 执行期间(已 `set_active_suite_context`): ```python from jtestlib import mysql_query_all, mysql_execute, BaseMapper, QueryWrapper rows = mysql_query_all(mysql="default", sql="SELECT ...", args=(...)) mapper = BaseMapper(PROJECT, RqtgContractDo, mysql_name="default") page = mapper.select_page(QueryWrapper(RqtgContractDo).eq("biz_date", "20250101")) ``` `mysql="default"` 对应当前 `-e` 下 `infra_ops_by_env[env].mysqls["default"]`;无 `default` 时 ORM 回退查找。 ### 7.2 启动前 TRUNCATE 对每个 `truncate_before_start=True` 的 `mysqls` 项:`FOREIGN_KEY_CHECKS=0` → 逐表 `TRUNCATE`(排除表除外)→ 恢复。 ### 7.3 ORM(`jtest_orm.py`) - 实体:`@dataclass` + `class Meta: table_name = "..."` 或 `__mp_table__` - `QueryWrapper` / `UpdateWrapper`:链式条件 - 支持分页 `IPage`、`select_list`、`insert`、`update`、`delete`(含逻辑删除字段) 适合集成测试查库断言,**不是**生产 ORM。 ### 7.4 schema 对比(`-sc`) 从 `information_schema` 读取 BASE TABLE,比较: - 仅一侧存在的表 - 列:类型、可空、默认值、键、extra - 索引:唯一性、列序、类型 ### 7.5 schema 复制(`-cs`) 1. 校验源/目标不是同一 `host:port/database` 2. 目标:`DROP` 全部基表 3. 源:逐表 `SHOW CREATE TABLE` → 目标执行 DDL(处理 pymysql 对 `%` 的转义) 4. 源:`SELECT *` 分批 `INSERT`(`DictCursor` 行转 tuple) ### 7.6 单表复制(`-cc`) 1. 校验源/目标不是同一 `host:port/database` 2. 校验源库存在该基表(表名大小写不敏感匹配) 3. 目标:若已有同名基表则 `DROP TABLE` 4. 源:`SHOW CREATE TABLE` → 目标建表 → 分批 `INSERT` 导数据 --- ## 8. HTTP 与远程(`http_utils` / `remote_ops`) ```python from jtestlib import request_post, wait_for_actuator_health, service_http_base base = service_http_base(project, "server", 0) # http://127.0.0.1:{port}[+http_path_prefix] wait_for_actuator_health(port, max_wait=40, http_path_prefix=module.http_path_prefix) # 启动流程内会自动传入 RuntimeModule.host 与 http_path_prefix resp = request_post(f"{base}/rqtgOpsController/exec", json_body={...}) ``` ```python from jtestlib import ssh_exec, sftp_upload ssh_exec(remote="default", command="ls -la") sftp_upload(remote="default", local_path="...", remote_path="...") ``` `remote` / `mysql` 名称对应 `InfraOpsConfig` 内层 key。 --- ## 9. 套件模板 ```python #!/usr/bin/env python3 from pathlib import Path import sys CURRENT_DIR = Path(__file__).resolve().parent TEST_DIR = CURRENT_DIR.parent for p in (str(TEST_DIR), str(TEST_DIR.parent)): if p not in sys.path: sys.path.insert(0, p) from jtest import PROJECT, bootstrap_paths from jtestlib import run_suite bootstrap_paths() def verify(project) -> bool: # 业务断言:优先业务不变量,避免仅复述 Java 实现 return True def main() -> int: return run_suite( project=PROJECT, suite_name=CURRENT_DIR.name, verify=verify, module_nodes={"server": (0,)}, # suite_java_opts=(...), # before_restart_modules=lambda p: ..., ) if __name__ == "__main__": sys.exit(main()) ``` --- ## 10. 依赖与环境 **Python 标准库**即可跑编排;以下能力需额外安装: ```bash python jtestlib/install_extra_deps.py # pymysql, paramiko ``` **外部工具** | 工具 | 用途 | |------|------| | `mvn` / `mvn.cmd` | 编译与 copy-dependencies | | `java` | 启动运行模块 | | `git` | JaCoCo 增量 `-b` | --- ## 11. 在新项目接入 ```bash python jtestlib/init_project.py --root /path/to/your-java-project ``` 生成 `test/jtest.py` 模板后,按实际模块改 `runtime_modules` / `support_modules` / `infra_ops_by_env`,并新增 `test//test.py` 与节点 yml。 --- ## 12. 常见问题 **Q: 启动报缺少 `application-server-xxx-0.yml`?** A: 在套件目录按命名约定放置节点配置,见 §6.2。 **Q: 启动报缺少 `classes` 或 `libs`?** A: 执行 `python test/jtest.py -d` 或 `-kd`。 **Q: `-a` 很慢?** A: 有意在批量前 `-kd` 保证环境一致;可用 `suite_exclude` 排除慢套件。 **Q: Windows 端口杀不掉或启动报排除段?** A: 先 `-k`;检查 Hyper-V 保留端口;见 `JTEST_SKIP_WINDOWS_PORT_CHECK`。 **Q: `-cs` 报 `not enough arguments for format string`?** A: 已在内置 DDL 执行处转义 `%`;若仍出现请检查 pymysql 版本。 **Q: 多套 `mysqls` 配置时 TRUNCATE / `-sc` / `-cc` 作用范围?** A: TRUNCATE 遍历所有 `truncate_before_start=True` 的项;`-sc`/`-cs`/`-cc` 仅 **`default`**。 --- ## 13. 设计取舍(维护者向) 1. **Python 编排、Java 执行业务**:改测试编排不改 Java 构建体系,迭代快。 2. **配置集中在 `test/jtest.py`**:套件薄、库厚,跨项目复用 jtestlib。 3. **目录名 = suite profile**:减少常量漂移。 4. **`-a` 前 `-kd`**:用时间换可重复性。 5. **日志按套件、按进程分文件**:失败定位路径固定。 6. **JVM 五层 opts**:全局默认 + 套件/节点覆盖,`-D` 按 key 合并。 7. **infra API 统一**:HTTP、MySQL、SSH 同一套 env/mysql/remote 命名。 8. **破坏性 DB 操作显式 CLI**:`-cs`、启动前 TRUNCATE 均需在配置或命令行明确意图。 --- ## 14. 版本与仓库 - 本库位于业务仓库的 `jtestlib/` 目录,与 `test/` 同级。 - 业务集成测试约定见项目根 `.cursor/rules/dmatch-integration-tests.mdc`(断言应对齐业务语义,不盲目贴合有问题的 Java 实现)。 如有新子命令或配置字段,请同步更新本文档 §4 与 §3。