mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-09 08:20:31 +08:00
feat(elevator): 对齐 V1 lib 的 Davinci/扫描/事件与部署配置
- davinci-manager-storage:FilePart 路径与基址按 V1 JAR(/portal/file、/part/*、GET /download) - 启动类:扫描 cn.cloudwalk.serial 与 cn.cloudwalk.cwos.client.resource,补 UUIDSerial 与 ApplicationService - deploy:v1/v2 application 中 cloudwalk.serial.enabled、Kafka 指向 192.168.3.12:9092;deploy/.gitignore 忽略日志 - cloudwalk-common-serial:补充 META-INF/spring.factories(Boot 自动配置) - 电梯:Session 配置、Davinci Bean、Feign 包、MQTT/Visitor/Zone Feign;部署脚本与 API parity 工具更新 - 文档与根脚本若干;未纳入大体积 jar/zip 与 v1 CFR 对比目录 Made-with: Cursor Former-commit-id: b76d142d13ebb5c0898de2d9d11bc583876829c2
This commit is contained in:
@@ -2,3 +2,7 @@ __pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
report/parity-*.md
|
||||
report/smoke-*.md
|
||||
report/SUITE-*.md
|
||||
report/*.json
|
||||
.suite_run_marker
|
||||
|
||||
@@ -1,44 +1,92 @@
|
||||
# elevator_api_parity — 新旧 JAR 接口对拍
|
||||
# elevator_api_parity — V1/V2 接口冒烟与对拍
|
||||
|
||||
## 功能概览
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| **单机冒烟** | 对单个 Base URL 遍历 `api_catalog.json` 中的接口(`include_in_smoke=true`),记录 HTTP 状态、耗时、业务 `code`、响应摘要 → `report/smoke-{label}-*.md` |
|
||||
| **横向对拍** | 旧 JAR(`--base-old`)与新 JAR(`--base-new`)并行调用;仅 **`include_in_parity=true`** 的条目参与 **HTTP 状态 + 业务 code** 一致性断言 → `report/parity-*.md` |
|
||||
| **套件总览** | 合并本次产生的冒烟×2 + 对拍 → `report/SUITE-*.md` |
|
||||
|
||||
接口清单与请求体见 **`api_catalog.json`**(支持 `fixture` 文件或内联 `body`)。
|
||||
|
||||
## 环境
|
||||
|
||||
- Python 3.8+
|
||||
- 安装:`pip install -r requirements.txt`(在**本目录**下执行)
|
||||
- Python 3.8+,`pip install -r requirements.txt`
|
||||
- 若遇全局 pytest 插件冲突:`export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`
|
||||
|
||||
## 环境说明
|
||||
## 启动两实例(示例)
|
||||
|
||||
本机若安装过 `allure_pytest` 等全局 pytest 插件且与 Python 版本冲突,请执行:
|
||||
|
||||
`export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`(`run_elevator_parity.sh` 已自动设置)后再跑 `pytest`。
|
||||
|
||||
## 快速使用
|
||||
|
||||
1. 构建新 JAR(在 reactor 根目录、JDK8)
|
||||
`mvn -DskipTests clean package`
|
||||
得到:`../cw-elevator-application-starter/target/cw-elevator-application-2.0.0.jar`
|
||||
2. 将历史 JAR 放到 `cw-elevator-application-V1.0.0.20211103/cw-elevator-application-V1.0.0.20211103.jar` 或设 `ELEVATOR_JAR_LEGACY`。
|
||||
3. 准备**与现网相同**的 `application.yml`(或目录),如 `ELEVATOR_SPRING_CONFIG=file:/path/to/application-elevator.yml`。
|
||||
4. 从仓库**反编译**根或本目录执行:
|
||||
`../../scripts/run_elevator_parity.sh`(见该脚本内环境变量说明)
|
||||
|
||||
或手动启动两实例后:
|
||||
同一套 `application.properties`(或外部配置),仅端口不同:
|
||||
|
||||
```bash
|
||||
export ELEVATOR_BASE_OLD=http://127.0.0.1:18080
|
||||
export ELEVATOR_BASE_NEW=http://127.0.0.1:18081
|
||||
export ELEVATOR_HEADER_BUSINESSID=... # 与现网/联调一致
|
||||
# ...
|
||||
cd tools/elevator_api_parity
|
||||
python -m pytest tests/ -v --tb=short
|
||||
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
|
||||
CFG="file:/path/to/application.properties"
|
||||
java -jar cw-elevator-application-V1.0.0.20211103.jar --server.port=18080 --spring.config.location=$CFG
|
||||
java -jar cw-elevator-application-2.0.0.jar --server.port=18081 --spring.config.location=$CFG
|
||||
```
|
||||
|
||||
## 配置
|
||||
## 一键完整套件(推荐)
|
||||
|
||||
- `api_catalog.json`:对拍端点、方法、fixture 文件名、说明。
|
||||
- `fixtures/*.json`:各接口请求体。
|
||||
- 归一化忽略键可扩展 `parity/compare.py` 中 `DEFAULT_STRIP_PATHS`(如 `data.rows` 内动态字段,谨慎)。
|
||||
在 **`maven-cw-elevator-application`** 目录:
|
||||
|
||||
## 报告
|
||||
```bash
|
||||
./scripts/run_full_elevator_api_suite.sh
|
||||
```
|
||||
|
||||
- `report/parity-YYYYMMDD-HHMMSS.md`:实跑时由 `conftest` 的 session hook 与 `report/generate_report.py` 组合生成。
|
||||
- JUnit(可选):`--junitxml=report/junit.xml`
|
||||
环境变量(可选):
|
||||
|
||||
- `ELEVATOR_BASE_OLD` — 默认 `http://127.0.0.1:18080`(V1)
|
||||
- `ELEVATOR_BASE_NEW` — 默认 `http://127.0.0.1:18081`(V2)
|
||||
- `ELEVATOR_HEADER_BUSINESSID`、`ELEVATOR_HEADER_LOGINID`、`ELEVATOR_HEADER_AUTHORIZATION` 等 — 与现网一致时业务码更有可比性
|
||||
|
||||
## 分步执行
|
||||
|
||||
```bash
|
||||
cd tools/elevator_api_parity
|
||||
export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
|
||||
|
||||
# 仅逻辑单测
|
||||
python3 -m pytest tests/test_unit_compare.py -q
|
||||
|
||||
# V1 单机冒烟
|
||||
python3 -m pytest tests/test_smoke_catalog.py -m smoke \
|
||||
--smoke-base=http://127.0.0.1:18080 --smoke-label=v1_legacy -q
|
||||
|
||||
# V2 单机冒烟
|
||||
python3 -m pytest tests/test_smoke_catalog.py -m smoke \
|
||||
--smoke-base=http://127.0.0.1:18081 --smoke-label=v2_build -q
|
||||
|
||||
# 双端对拍(双端 /actuator/health 等可达时执行,否则跳过)
|
||||
python3 -m pytest tests/test_parity_endpoints.py -m live -q \
|
||||
--base-old=http://127.0.0.1:18080 --base-new=http://127.0.0.1:18081
|
||||
```
|
||||
|
||||
### 强制要求联调(失败即中断)
|
||||
|
||||
```bash
|
||||
export ELEVATOR_PARITY_REQUIRE_LIVE=1 # 对拍
|
||||
export ELEVATOR_SMOKE_REQUIRE=1 # 冒烟
|
||||
```
|
||||
|
||||
## 仅对拍(不含冒烟)
|
||||
|
||||
```bash
|
||||
./scripts/run_elevator_parity.sh
|
||||
```
|
||||
|
||||
(脚本内会先 `mvn package`,再跑单测 + 对拍。)
|
||||
|
||||
## 报告位置
|
||||
|
||||
- `report/smoke-v1_legacy-*.md` + **同名 `.json`**(结构化结果,供套件矩阵消费)
|
||||
- `report/smoke-v2_build-*.md` + **同名 `.json`**
|
||||
- `report/parity-*.md` + **同名 `.json`**
|
||||
- **`report/SUITE-*.md`**:始终包含 **第二节「全量接口清单」**(来源于 `api_catalog.json`);**第三节「测试结果矩阵」** 在有上述 JSON 时填入 V1/V2 HTTP、业务 code、对拍 Y/N;若本次因未起服务而 **skip**,矩阵中为 **—**(参见第三节说明)。
|
||||
|
||||
## 扩展接口
|
||||
|
||||
编辑 **`api_catalog.json`**:
|
||||
|
||||
- `include_in_parity: false` — 只做冒烟,不参与 V1/V2 等值断言(避免依赖不一致导致误报)。
|
||||
- `include_in_parity: true` — 纳入横向对拍(当前默认对 **访客/人员/规则分页/记录分页** 四个核心场景开启)。
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"description": "电梯应用 HTTP 接口清单:冒烟与对拍共用。include_in_parity=false 的项仅做单机路由/业务响应探测,不参与 V1/V2 等值断言。",
|
||||
"endpoints": [
|
||||
{
|
||||
"id": "person_add_visitor_min",
|
||||
"name": "访客派梯-最小体(多依赖业务失败,仅比对 HTTP+业务code)",
|
||||
"name": "访客派梯-最小体",
|
||||
"method": "POST",
|
||||
"path": "/elevator/person/add/visitor",
|
||||
"fixture": "person_add_visitor_min.json",
|
||||
"compare_mode": "code_only"
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": true,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "person_detail",
|
||||
"name": "人员详情-最小分页体",
|
||||
"name": "人员详情-分页",
|
||||
"method": "POST",
|
||||
"path": "/elevator/person/detail",
|
||||
"fixture": "person_detail_min.json",
|
||||
"compare_mode": "code_only"
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": true,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "passrule_page",
|
||||
@@ -23,7 +28,9 @@
|
||||
"method": "POST",
|
||||
"path": "/elevator/passRule/page",
|
||||
"fixture": "passrule_page.json",
|
||||
"compare_mode": "code_only"
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": true,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "record_page",
|
||||
@@ -31,7 +38,399 @@
|
||||
"method": "POST",
|
||||
"path": "/intelligent/acs/elevator/record/page",
|
||||
"fixture": "record_page.json",
|
||||
"compare_mode": "code_only"
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": true,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "person_add",
|
||||
"name": "人员-从现有人员添加",
|
||||
"method": "POST",
|
||||
"path": "/elevator/person/add",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "person_edit",
|
||||
"name": "人员-编辑",
|
||||
"method": "POST",
|
||||
"path": "/elevator/person/edit",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "person_delete",
|
||||
"name": "人员-删除",
|
||||
"method": "POST",
|
||||
"path": "/elevator/person/delete",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "person_page",
|
||||
"name": "人员-分页",
|
||||
"method": "POST",
|
||||
"path": "/elevator/person/page",
|
||||
"body": { "pageNo": 1, "pageSize": 1 },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "person_time_detail",
|
||||
"name": "人员-时间详情",
|
||||
"method": "POST",
|
||||
"path": "/elevator/person/timeDetail",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "person_page_by_app",
|
||||
"name": "人员-应用分页",
|
||||
"method": "POST",
|
||||
"path": "/elevator/person/pageByApp",
|
||||
"body": { "pageNo": 1, "pageSize": 1 },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "passrule_floor",
|
||||
"name": "通行规则-楼层列表",
|
||||
"method": "POST",
|
||||
"path": "/elevator/passRule/floor",
|
||||
"body": { "pageNo": 1, "pageSize": 1, "zoneId": "" },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "passrule_add",
|
||||
"name": "通行规则-新增",
|
||||
"method": "POST",
|
||||
"path": "/elevator/passRule/add",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "passrule_edit",
|
||||
"name": "通行规则-修改",
|
||||
"method": "POST",
|
||||
"path": "/elevator/passRule/edit",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "passrule_delete",
|
||||
"name": "通行规则-删除",
|
||||
"method": "POST",
|
||||
"path": "/elevator/passRule/delete",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "passrule_detail",
|
||||
"name": "通行规则-详情",
|
||||
"method": "POST",
|
||||
"path": "/elevator/passRule/detail",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "passrule_image",
|
||||
"name": "通行规则-按人像查楼层权限",
|
||||
"method": "POST",
|
||||
"path": "/elevator/passRule/image",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "passrule_image_list",
|
||||
"name": "通行规则-批量人像",
|
||||
"method": "POST",
|
||||
"path": "/elevator/passRule/image/list",
|
||||
"body": { "personList": [] },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "record_analyse_cycle",
|
||||
"name": "记录-周期统计",
|
||||
"method": "POST",
|
||||
"path": "/intelligent/acs/elevator/record/analyse/cycle",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "record_analyse_count",
|
||||
"name": "记录-次数统计",
|
||||
"method": "POST",
|
||||
"path": "/intelligent/acs/elevator/record/analyse/count",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "record_page_request",
|
||||
"name": "记录-请求分页",
|
||||
"method": "POST",
|
||||
"path": "/intelligent/acs/elevator/record/page/request",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "record_device_list",
|
||||
"name": "记录-设备列表",
|
||||
"method": "POST",
|
||||
"path": "/intelligent/acs/elevator/record/device/list",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "record_zone_tree",
|
||||
"name": "记录-区域树",
|
||||
"method": "POST",
|
||||
"path": "/intelligent/acs/elevator/record/zone/tree",
|
||||
"body": { "parentId": "", "businessId": "" },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "device_v2_39201",
|
||||
"name": "设备网关-39201 设备列表",
|
||||
"method": "POST",
|
||||
"path": "/device/v2/39201",
|
||||
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "device_v2_39202",
|
||||
"name": "设备网关-39202 区域电梯码",
|
||||
"method": "POST",
|
||||
"path": "/device/v2/39202",
|
||||
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "device_v2_39203",
|
||||
"name": "设备网关-39203 添加记录",
|
||||
"method": "POST",
|
||||
"path": "/device/v2/39203",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "device_v2_39204",
|
||||
"name": "设备网关-39204 密钥时间戳(已废弃)",
|
||||
"method": "POST",
|
||||
"path": "/device/v2/39204",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "elevator_device_add",
|
||||
"name": "设备-新增",
|
||||
"method": "POST",
|
||||
"path": "/elevator/device/add",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "elevator_device_edit",
|
||||
"name": "设备-编辑",
|
||||
"method": "POST",
|
||||
"path": "/elevator/device/edit",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "elevator_device_get_by_id",
|
||||
"name": "设备-按ID查询",
|
||||
"method": "POST",
|
||||
"path": "/elevator/device/getById",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "elevator_device_delete",
|
||||
"name": "设备-删除",
|
||||
"method": "POST",
|
||||
"path": "/elevator/device/delete",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "elevator_device_edit_code",
|
||||
"name": "设备-改码",
|
||||
"method": "POST",
|
||||
"path": "/elevator/device/editCode",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "elevator_device_get",
|
||||
"name": "设备-查询列表",
|
||||
"method": "POST",
|
||||
"path": "/elevator/device/get",
|
||||
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "elevator_device_export",
|
||||
"name": "设备-导出",
|
||||
"method": "POST",
|
||||
"path": "/elevator/device/export",
|
||||
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "elevator_device_zone_tree_code",
|
||||
"name": "设备-区域树编码",
|
||||
"method": "POST",
|
||||
"path": "/elevator/device/zone/treeCode",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "elevator_device_page",
|
||||
"name": "设备-分页",
|
||||
"method": "POST",
|
||||
"path": "/elevator/device/devicePage",
|
||||
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "restructure_unbind_floors",
|
||||
"name": "改造-未绑定楼层",
|
||||
"method": "POST",
|
||||
"path": "/elevator/restructure/unbind/floors",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "restructure_floors",
|
||||
"name": "改造-楼层列表",
|
||||
"method": "POST",
|
||||
"path": "/elevator/restructure/floors",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "restructure_condition",
|
||||
"name": "改造-条件",
|
||||
"method": "POST",
|
||||
"path": "/elevator/restructure/condition",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "restructure_condition_labels",
|
||||
"name": "改造-条件标签",
|
||||
"method": "POST",
|
||||
"path": "/elevator/restructure/condition/labels",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "restructure_binding",
|
||||
"name": "改造-绑定",
|
||||
"method": "POST",
|
||||
"path": "/elevator/restructure/binding",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "restructure_binding_person",
|
||||
"name": "改造-绑定人员",
|
||||
"method": "POST",
|
||||
"path": "/elevator/restructure/binding/person",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "restructure_task_progress",
|
||||
"name": "改造-任务进度",
|
||||
"method": "POST",
|
||||
"path": "/elevator/restructure/task/progress",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
},
|
||||
{
|
||||
"id": "restructure_task_stop",
|
||||
"name": "改造-停止任务",
|
||||
"method": "POST",
|
||||
"path": "/elevator/restructure/task/stop",
|
||||
"body": {},
|
||||
"compare_mode": "code_only",
|
||||
"include_in_parity": false,
|
||||
"include_in_smoke": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from parity.client import can_reach_both, default_headers
|
||||
from parity.client import can_reach_both, can_reach_one, default_headers
|
||||
|
||||
_DIR = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config._parity_rows = [] # type: ignore[attr-defined]
|
||||
config._smoke_rows = [] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -24,6 +26,14 @@ def pytest_addoption(parser):
|
||||
"--base-new",
|
||||
default=os.environ.get("ELEVATOR_BASE_NEW", "http://127.0.0.1:18081"),
|
||||
)
|
||||
parser.addoption(
|
||||
"--smoke-base",
|
||||
default=os.environ.get("ELEVATOR_SMOKE_BASE", "http://127.0.0.1:18080"),
|
||||
)
|
||||
parser.addoption(
|
||||
"--smoke-label",
|
||||
default=os.environ.get("ELEVATOR_SMOKE_LABEL", "v1_legacy"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -36,6 +46,16 @@ def base_new(request):
|
||||
return str(request.config.getoption("--base-new")).rstrip("/")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def smoke_base(request):
|
||||
return str(request.config.getoption("--smoke-base")).rstrip("/")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def smoke_label(request):
|
||||
return str(request.config.getoption("--smoke-label"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def session_http():
|
||||
s = requests.Session()
|
||||
@@ -55,27 +75,56 @@ def two_instances_ready(base_old, base_new, session_http, request):
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def smoke_instance_ready(smoke_base, session_http, request):
|
||||
ok, _ = can_reach_one(smoke_base, session_http)
|
||||
require = os.environ.get("ELEVATOR_SMOKE_REQUIRE", "")
|
||||
if not ok and not require:
|
||||
pytest.skip(f"单机 {smoke_base} 健康检查不通过(跳过 smoke)")
|
||||
if not ok and require:
|
||||
pytest.fail(f"ELEVATOR_SMOKE_REQUIRE=1 且 {smoke_base} 不可达")
|
||||
return True
|
||||
|
||||
|
||||
def _write_smoke_report(config, srows: list, report_dir: Path) -> None:
|
||||
from report import generate_smoke_report
|
||||
|
||||
label = str(config.getoption("--smoke-label", default="smoke"))
|
||||
p2 = report_dir / f"smoke-{label}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.md"
|
||||
generate_smoke_report.write_file(
|
||||
p2,
|
||||
str(config.getoption("--smoke-base", default="")),
|
||||
label,
|
||||
srows,
|
||||
)
|
||||
print(f"\n[smoke] 报告: {p2}")
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
rows = getattr(session.config, "_parity_rows", None)
|
||||
if not rows:
|
||||
return
|
||||
try:
|
||||
from report import generate_report
|
||||
import importlib
|
||||
|
||||
p = _DIR / "report" / generate_report.timestamped_name("parity")
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
generate_report.write_file(
|
||||
p,
|
||||
str(session.config.getoption("--base-old", default="")),
|
||||
str(session.config.getoption("--base-new", default="")),
|
||||
rows,
|
||||
)
|
||||
print(f"\n[parity] 报告: {p}")
|
||||
except Exception as e:
|
||||
print(f"\n[parity] 报告未生成: {e}")
|
||||
config = session.config
|
||||
report_dir = _DIR / "report"
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
rows = getattr(config, "_parity_rows", None) or []
|
||||
if rows:
|
||||
try:
|
||||
gen = importlib.import_module("report.generate_report")
|
||||
p = report_dir / gen.timestamped_name("parity")
|
||||
gen.write_file(
|
||||
p,
|
||||
str(config.getoption("--base-old", default="")),
|
||||
str(config.getoption("--base-new", default="")),
|
||||
rows,
|
||||
)
|
||||
print(f"\n[parity] 对拍报告: {p}")
|
||||
except Exception as e:
|
||||
print(f"\n[parity] 对拍报告未生成: {e}")
|
||||
|
||||
def load_catalog() -> dict:
|
||||
from parity import catalog_loader
|
||||
|
||||
return catalog_loader.load()
|
||||
srows = getattr(config, "_smoke_rows", None) or []
|
||||
if srows:
|
||||
try:
|
||||
_write_smoke_report(config, srows, report_dir)
|
||||
except Exception as e:
|
||||
print(f"\n[smoke] 报告未生成: {e}")
|
||||
|
||||
@@ -1,8 +1,42 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
return json.loads((_ROOT / "api_catalog.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def endpoint_body(ep: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Resolve request JSON: inline ``body`` wins, else load ``fixture`` file."""
|
||||
if ep.get("body") is not None:
|
||||
return dict(ep["body"])
|
||||
fix = ep.get("fixture")
|
||||
if fix:
|
||||
p = _ROOT / "fixtures" / fix
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
return {}
|
||||
|
||||
|
||||
def iter_endpoints(catalog: dict, *, tag: Optional[str] = None) -> list[dict]:
|
||||
"""If ``tag`` is set, only endpoints with ``tags`` containing it (or legacy entries with no tags = all)."""
|
||||
out: list[dict] = []
|
||||
for ep in catalog.get("endpoints", []):
|
||||
tags = ep.get("tags") or []
|
||||
if tag is None:
|
||||
out.append(ep)
|
||||
elif not tags:
|
||||
out.append(ep)
|
||||
elif tag in tags:
|
||||
out.append(ep)
|
||||
return out
|
||||
|
||||
|
||||
def include_in_parity(ep: dict) -> bool:
|
||||
return bool(ep.get("include_in_parity", True))
|
||||
|
||||
|
||||
def include_in_smoke(ep: dict) -> bool:
|
||||
return bool(ep.get("include_in_smoke", True))
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
@@ -124,6 +125,52 @@ def _safe_json(text: str) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
def call_single(
|
||||
name: str,
|
||||
method: str,
|
||||
path: str,
|
||||
body: Any,
|
||||
base_url: str,
|
||||
session: requests.Session | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""One HTTP call for smoke coverage; returns a flat dict for reporting."""
|
||||
s = session or requests.Session()
|
||||
h = default_headers()
|
||||
url = base_url.rstrip("/") + path
|
||||
data = (
|
||||
None
|
||||
if body is None
|
||||
else (body if isinstance(body, str) else json.dumps(body, ensure_ascii=False))
|
||||
)
|
||||
m = method.upper()
|
||||
t0 = time.perf_counter()
|
||||
if m == "GET":
|
||||
r = s.get(url, headers=h, timeout=120)
|
||||
else:
|
||||
r = s.post(url, headers=h, data=data, timeout=120)
|
||||
elapsed_ms = int((time.perf_counter() - t0) * 1000)
|
||||
txt = r.text or ""
|
||||
oj = _safe_json(txt)
|
||||
bc = compare.business_code(oj) if oj is not None else None
|
||||
head = txt[:400].replace("\n", " ")
|
||||
return {
|
||||
"name": name,
|
||||
"method": m,
|
||||
"path": path,
|
||||
"http_status": r.status_code,
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"business_code": bc,
|
||||
"response_head": head,
|
||||
"reachable": True,
|
||||
}
|
||||
|
||||
|
||||
def can_reach_one(base_url: str, s: requests.Session | None = None) -> tuple[bool, str]:
|
||||
s = s or requests.Session()
|
||||
_, ok, _ = probe_healthy(base_url, s)
|
||||
return ok, base_url
|
||||
|
||||
|
||||
def can_reach_both(
|
||||
base_old: str, base_new: str, s: requests.Session | None = None
|
||||
) -> tuple[bool, str]:
|
||||
|
||||
@@ -4,5 +4,6 @@ addopts = -q --strict-markers
|
||||
testpaths = tests
|
||||
markers =
|
||||
live: 需要两实例可访问
|
||||
smoke: 单机全量 HTTP 探测
|
||||
unit: 纯逻辑单测
|
||||
pythonpath = .
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
@@ -40,3 +41,16 @@ def write_file(
|
||||
f"\n## 汇总\n- 通过: {ok},不一致: {bad}。\n- **上线前请人工与联调/业务确认**。\n"
|
||||
)
|
||||
out_path.write_text("".join(lines), encoding="utf-8")
|
||||
|
||||
payload = {
|
||||
"meta": {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"base_old": base_old,
|
||||
"base_new": base_new,
|
||||
"markdown": str(out_path.resolve()),
|
||||
},
|
||||
"rows": rows,
|
||||
"summary": {"match_ok": ok, "match_bad": bad},
|
||||
}
|
||||
json_path = out_path.with_suffix(".json")
|
||||
json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
||||
def write_file(
|
||||
out_path: Path,
|
||||
base_url: str,
|
||||
label: str,
|
||||
rows: List[dict],
|
||||
) -> None:
|
||||
lines = [
|
||||
f"# 电梯应用 API 单机冒烟报告 — {label}\n",
|
||||
f"- **时间**: {datetime.now().isoformat()}\n",
|
||||
f"- **Base URL**: {base_url}\n",
|
||||
"\n## 接口探测\n\n",
|
||||
"| 用例 | 方法+路径 | HTTP | ms | 业务code | 响应摘要 |\n",
|
||||
"| ---- | -------- | ---- | -- | -------- | -------- |\n",
|
||||
]
|
||||
ok_http = 0
|
||||
for r in rows:
|
||||
name = r.get("name", "")
|
||||
pth = f"{r.get('method', '')} {r.get('path', '')}"
|
||||
hs = r.get("http_status", "")
|
||||
ms = r.get("elapsed_ms", "")
|
||||
bc = r.get("business_code", "") or ""
|
||||
head = (r.get("response_head", "") or "").replace("|", "\\|")[:180]
|
||||
if isinstance(hs, int) and 200 <= hs < 300:
|
||||
ok_http += 1
|
||||
lines.append(f"| {name} | `{pth}` | {hs} | {ms} | {bc} | {head} |\n")
|
||||
lines.append(
|
||||
f"\n## 汇总\n"
|
||||
f"- 用例数: {len(rows)};HTTP 2xx 数量: {ok_http}。\n"
|
||||
f"- 业务失败(非 0 code)仍可能为**预期**(缺数据/缺 token);本报告仅证明路由可达且返回 Cloudwalk 风格 JSON。\n"
|
||||
)
|
||||
out_path.write_text("".join(lines), encoding="utf-8")
|
||||
|
||||
payload = {
|
||||
"meta": {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"base_url": base_url,
|
||||
"label": label,
|
||||
"markdown": str(out_path.resolve()),
|
||||
},
|
||||
"rows": rows,
|
||||
"summary": {"total": len(rows), "http_2xx": ok_http},
|
||||
}
|
||||
out_path.with_suffix(".json").write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""合并电梯 API 测试套件总览:全量清单(catalog)+ 测试结果矩阵(JSON)+ 可选附录(子报告 MD)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
def _tool_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _load_catalog(path: Path) -> List[dict]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return list(data.get("endpoints") or [])
|
||||
|
||||
|
||||
def _safe_json_load(p: Optional[Path]) -> Optional[dict]:
|
||||
if not p or not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
|
||||
def _rows_by_id(rows: Any) -> Dict[str, dict]:
|
||||
out: Dict[str, dict] = {}
|
||||
if not isinstance(rows, list):
|
||||
return out
|
||||
for r in rows:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
eid = r.get("id")
|
||||
if not eid:
|
||||
# 对拍行可能在 id 字段,否则用 path 不可靠
|
||||
continue
|
||||
out[str(eid)] = r
|
||||
return out
|
||||
|
||||
|
||||
def _parity_by_id(rows: Any) -> Dict[str, dict]:
|
||||
out: Dict[str, dict] = {}
|
||||
if not isinstance(rows, list):
|
||||
return out
|
||||
for r in rows:
|
||||
if isinstance(r, dict) and r.get("id"):
|
||||
out[str(r["id"])] = r
|
||||
return out
|
||||
|
||||
|
||||
def _resolve_json_arg(md_or_json: Optional[str]) -> Optional[Path]:
|
||||
if not md_or_json:
|
||||
return None
|
||||
p = Path(md_or_json)
|
||||
if p.suffix.lower() == ".json" and p.is_file():
|
||||
return p
|
||||
if p.suffix.lower() == ".md":
|
||||
cand = p.with_suffix(".json")
|
||||
if cand.is_file():
|
||||
return cand
|
||||
return None
|
||||
|
||||
|
||||
def _markdown_table_catalog(endpoints: List[dict]) -> str:
|
||||
lines = [
|
||||
"| ID | 名称 | 方法 | Path | 冒烟 | 横向对拍 |\n",
|
||||
"| ---- | ---- | ---- | ---- | ---- | -------- |\n",
|
||||
]
|
||||
for ep in endpoints:
|
||||
eid = ep.get("id", "")
|
||||
name = (ep.get("name") or "").replace("|", "\\|")
|
||||
method = ep.get("method") or "POST"
|
||||
path = (ep.get("path") or "").replace("|", "\\|")
|
||||
sm = "是" if ep.get("include_in_smoke", True) else "否"
|
||||
py = "是" if ep.get("include_in_parity", True) else "否"
|
||||
lines.append(f"| `{eid}` | {name} | {method} | `{path}` | {sm} | {py} |\n")
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
def _markdown_matrix(
|
||||
endpoints: List[dict],
|
||||
smoke_v1: Optional[dict],
|
||||
smoke_v2: Optional[dict],
|
||||
parity: Optional[dict],
|
||||
) -> str:
|
||||
m1 = _rows_by_id((smoke_v1 or {}).get("rows"))
|
||||
m2 = _rows_by_id((smoke_v2 or {}).get("rows"))
|
||||
mp = _parity_by_id((parity or {}).get("rows"))
|
||||
|
||||
meta_v1 = (smoke_v1 or {}).get("meta") or {}
|
||||
meta_v2 = (smoke_v2 or {}).get("meta") or {}
|
||||
meta_pr = (parity or {}).get("meta") or {}
|
||||
|
||||
head = ""
|
||||
head += (
|
||||
f"- **V1 冒烟数据来源**: {meta_v1.get('markdown') or meta_v1.get('base_url') or '(无 JSON,未执行或已跳过)'}\n"
|
||||
)
|
||||
head += (
|
||||
f"- **V2 冒烟数据来源**: {meta_v2.get('markdown') or meta_v2.get('base_url') or '(无)'}\n"
|
||||
)
|
||||
head += f"- **对拍数据来源**: {meta_pr.get('markdown') or '(无)'}\n\n"
|
||||
|
||||
lines = [
|
||||
head,
|
||||
"### 测试结果矩阵(按 catalog `id` 对齐)\n\n",
|
||||
"| catalog id | V1 HTTP | V1 code | V2 HTTP | V2 code | 对拍一致(仅参与对拍的条目) | 备注 |\n",
|
||||
"| ---------- | ------- | ------- | ------- | ------- | ---------------------------- | ---- |\n",
|
||||
]
|
||||
for ep in endpoints:
|
||||
eid = str(ep.get("id", ""))
|
||||
include_p = ep.get("include_in_parity", True)
|
||||
v1 = m1.get(eid)
|
||||
v2 = m2.get(eid)
|
||||
pr_row = mp.get(eid) if include_p else None
|
||||
|
||||
def cell_smoke(row: Optional[dict]) -> tuple[str, str]:
|
||||
if not row:
|
||||
return "—", "—"
|
||||
return str(row.get("http_status", "—")), str(row.get("business_code") or "—")
|
||||
|
||||
h1, c1 = cell_smoke(v1)
|
||||
h2, c2 = cell_smoke(v2)
|
||||
if not include_p:
|
||||
par_c = "(不参与)"
|
||||
elif not parity:
|
||||
par_c = "—(未执行)"
|
||||
elif pr_row:
|
||||
par_c = "**Y**" if pr_row.get("match") else "**N**"
|
||||
if not pr_row.get("match"):
|
||||
par_c += " " + (pr_row.get("message") or "")[:60].replace("|", "\\|")
|
||||
else:
|
||||
par_c = "—(本次对拍清单无此项)"
|
||||
|
||||
remark = ""
|
||||
if v1 is None and v2 is None:
|
||||
remark = "冒烟未执行或无该 id 结果"
|
||||
|
||||
lines.append(
|
||||
f"| `{eid}` | {h1} | {c1} | {h2} | {c2} | {par_c} | {remark} |\n"
|
||||
)
|
||||
|
||||
lines.append(
|
||||
"\n**说明**:`code` 为 CloudwalkResult 顶层业务码;HTTP 为传输层状态。"
|
||||
"对拍列为 **Y** 表示旧/新 HTTP 状态一致且业务 code 一致(`code_only` 模式)。\n"
|
||||
)
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="Generate SUITE markdown with catalog + matrix")
|
||||
ap.add_argument("--out", required=True, help="Output SUITE-*.md path")
|
||||
ap.add_argument("--catalog", help="api_catalog.json path")
|
||||
ap.add_argument("--smoke-v1", help="smoke-v1_*.md 或同名 .json")
|
||||
ap.add_argument("--smoke-v2", help="smoke-v2_*.md 或同名 .json")
|
||||
ap.add_argument("--parity", help="parity-*.md 或同名 .json")
|
||||
ap.add_argument(
|
||||
"--embed-full",
|
||||
action="store_true",
|
||||
help="附录中嵌入子报告 Markdown 全文(较长)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
root = _tool_root()
|
||||
catalog_path = Path(args.catalog or (root / "api_catalog.json"))
|
||||
endpoints = _load_catalog(catalog_path)
|
||||
|
||||
js_v1 = _resolve_json_arg(args.smoke_v1)
|
||||
js_v2 = _resolve_json_arg(args.smoke_v2)
|
||||
js_pr = _resolve_json_arg(args.parity)
|
||||
|
||||
doc_v1 = _safe_json_load(js_v1)
|
||||
doc_v2 = _safe_json_load(js_v2)
|
||||
doc_pr = _safe_json_load(js_pr)
|
||||
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
lines: List[str] = [
|
||||
"# 电梯应用 API 测试套件总览\n\n",
|
||||
f"- **生成时间**: {datetime.now().isoformat()}\n",
|
||||
f"- **清单来源**: `{catalog_path.resolve()}`(共 **{len(endpoints)}** 条接口定义)\n\n",
|
||||
"## 1. 说明\n\n",
|
||||
"- **全量清单**:第二节,来自 `api_catalog.json`,含是否参与冒烟/对拍。\n",
|
||||
"- **测试结果矩阵**:第三节,与本次运行生成的 **JSON** 侧车文件对齐(与 `.md` 同名的 `.json`)。"
|
||||
"若应用未启动导致 pytest **skip**,则无 JSON,矩阵中表现为 **—**。\n",
|
||||
"- **横向对拍**:仅 `include_in_parity=true` 的条目会写入对拍 JSON 并参与对比。\n\n",
|
||||
"## 2. 全量接口测试清单(catalog)\n\n",
|
||||
_markdown_table_catalog(endpoints),
|
||||
"\n",
|
||||
"## 3. 测试结果矩阵\n\n",
|
||||
_markdown_matrix(endpoints, doc_v1, doc_v2, doc_pr),
|
||||
]
|
||||
|
||||
sec = 4
|
||||
if args.embed_full:
|
||||
for title, path_s in (
|
||||
("V1 单机冒烟 Markdown", args.smoke_v1),
|
||||
("V2 单机冒烟 Markdown", args.smoke_v2),
|
||||
("横向对拍 Markdown", args.parity),
|
||||
):
|
||||
if not path_s:
|
||||
continue
|
||||
pp = Path(path_s)
|
||||
if pp.suffix.lower() != ".md":
|
||||
pp = pp.with_suffix(".md") if pp.with_suffix(".md").is_file() else pp
|
||||
if not pp.is_file():
|
||||
continue
|
||||
lines.append(f"## {sec}. {title}\n\n")
|
||||
lines.append(f"源文件: `{pp.resolve()}`\n\n---\n\n")
|
||||
lines.append(pp.read_text(encoding="utf-8"))
|
||||
lines.append("\n\n")
|
||||
sec += 1
|
||||
|
||||
lines.append(
|
||||
f"\n## {sec}. 原始报告路径(便于回放)\n\n"
|
||||
f"- V1 冒烟: `{args.smoke_v1 or '(未生成)'}`\n"
|
||||
f"- V2 冒烟: `{args.smoke_v2 or '(未生成)'}`\n"
|
||||
f"- 对拍: `{args.parity or '(未生成)'}`\n"
|
||||
f"- 同名 **`.json`** 与 `.md` 一并生成时可自动填充第三节矩阵。\n"
|
||||
)
|
||||
|
||||
out.write_text("".join(lines), encoding="utf-8")
|
||||
print(out.resolve())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+6
-13
@@ -1,15 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from parity.catalog_loader import endpoint_body as cb_body
|
||||
from parity.catalog_loader import include_in_parity as cb_parity
|
||||
from parity.catalog_loader import load as load_catalog
|
||||
from parity.client import call_both
|
||||
|
||||
_DIR = Path(__file__).resolve().parent.parent
|
||||
_FIX = _DIR / "fixtures"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("two_instances_ready")
|
||||
@pytest.mark.live
|
||||
@@ -19,16 +15,13 @@ def test_parity_from_catalog(
|
||||
base_new,
|
||||
session_http,
|
||||
):
|
||||
"""按 api_catalog 双端对拍。compare_mode: deep | code_only | status_only"""
|
||||
"""按 api_catalog 双端对拍(仅 ``include_in_parity`` 为 true 的条目)。compare_mode: deep | code_only | status_only"""
|
||||
cat = load_catalog()["endpoints"] # type: ignore[operator]
|
||||
for ep in cat:
|
||||
if not cb_parity(ep):
|
||||
continue
|
||||
name = ep["id"]
|
||||
fixture = ep.get("fixture")
|
||||
body: dict
|
||||
if fixture:
|
||||
body = json.loads((_FIX / fixture).read_text(encoding="utf-8"))
|
||||
else:
|
||||
body = {}
|
||||
body = cb_body(ep)
|
||||
pr = call_both(
|
||||
name=ep.get("name", name),
|
||||
method=ep.get("method", "POST"),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from parity.catalog_loader import endpoint_body as cb_body
|
||||
from parity.catalog_loader import include_in_smoke as cb_smoke
|
||||
from parity.catalog_loader import load as load_catalog
|
||||
from parity.client import call_single
|
||||
|
||||
"""单机按 catalog 逐接口 POST/GET,写入 _smoke_rows(sessionfinish 落盘)。"""
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smoke_instance_ready")
|
||||
@pytest.mark.smoke
|
||||
def test_smoke_from_catalog(request, smoke_base, smoke_label, session_http):
|
||||
cat = load_catalog()["endpoints"]
|
||||
for ep in cat:
|
||||
if not cb_smoke(ep):
|
||||
continue
|
||||
name = ep["id"]
|
||||
body = cb_body(ep)
|
||||
row = call_single(
|
||||
name=ep.get("name", name),
|
||||
method=ep.get("method", "POST"),
|
||||
path=ep["path"],
|
||||
body=body,
|
||||
base_url=smoke_base,
|
||||
session=session_http,
|
||||
)
|
||||
row["id"] = name
|
||||
row["label"] = smoke_label
|
||||
request.config._smoke_rows.append(row) # type: ignore
|
||||
Reference in New Issue
Block a user