refactor: 本仓升为唯一源(原 sap-cli 源码仓归档)

方向反转:此前 SKILL.md 是「模板渲染产物」、sap-cli 是源;现 sap-cli 归档,
sap-cli-skill 承接开发与分发,SKILL.md 回归手工维护的正本。

迁移(来自 sap-cli,共 104 文件):
- tests/           692 例测试(15 个文件的内联 sys.path 改指 assets/)
- openspec/        SDD 规格与归档变更(42 文件)
- docs/            开发文档与 ADT 原理(含 dev/CLAUDE.md、AGENTS.md)
- .claude/         rules 副本 + settings.json(供 Claude Code)
- .github/ .hermes/ .pre-commit-config.yaml .editorconfig CLAUDE.md
- scripts/ 保持仅 setup.py(pack_skill.py 已随旧仓归档,不迁)

修复(迁移暴露的真实缺陷):
- assets/pyproject.toml 的 build-backend 写作 `setuptools.backends._legacy:_Backend`,
  该模块在 setuptools 中不存在 → `pip install -e` 从来装不上。改为 build_meta。
  实测:临时 venv 安装成功,sap-cli --help 正常列出 31 个命令
- pyproject readme 指向不存在的 assets/README.md(editable 安装会失败)→ 改内联文本
- pyproject urls 改指 sap-cli-skill

机制调整:
- .github/workflows/ci.yml 适配 assets/ 布局;顶部注明该工作流仅 GitHub 执行,
  本仓在 Gitee 不会自动跑
- pre-commit 增本地测试门禁(Gitee 上真正生效的那道)
- .gitignore 合并旧仓完整规则(保留 log/ 下 md 知识库入库,只忽略运行日志)
- 大文件上限 100KB→1MB(架构图 512KB)

守卫测试 tests/unit/test_repo_guards.py(10 → 18 例):
- SKILL.md 须记录 parser 全部 CLI 命令 / 铁律 1-5 须为真实小节标题 / 示例不得违反铁律 5
- references/ 规则齐备;.claude/rules 与 references 必须一致(实测抓到一次真实漂移)
- VERSION == sapcli.__version__ == README 版本
- 仓内不得再出现 pack_skill.py / skill-src(防废弃流程回潮)

698 tests OK;editable 安装与 CLI 入口经临时 venv 实测通过。
docs/RELEASING.md 重写为单源开发流程。
This commit is contained in:
吴让宇
2026-09-11 00:40:15 +08:00
parent e786742bcb
commit c5905a5b1e
104 changed files with 20744 additions and 10 deletions
+344
View File
@@ -0,0 +1,344 @@
# sap-cli 测试方案
> 版本:2.1 | 20 命令 · 16 类型 · 30 API 方法
> 日期:2026-06-09
---
## 一、现状分析
### 1.1 代码架构(四层)
```
┌─────────────────────────────────────────────────┐
│ CLI 层 parser.py + app.py │ ← 参数解析、命令路由
├─────────────────────────────────────────────────┤
│ 命令层 commands/*.py (11 模块, 20 命令) │ ← 业务编排
├─────────────────────────────────────────────────┤
│ API 层 client.py (30 方法) │ ← ADT REST 通信
├─────────────────────────────────────────────────┤
│ 基础层 types/config/auth/ddic/manifest/... │ ← 纯逻辑、无网络
└─────────────────────────────────────────────────┘
```
### 1.2 现有测试
| 文件 | 行数 | 状态 | 说明 |
|------|------|------|------|
| `tests/test_sapcli.py` | 695 | ✅ 活跃 | 68 用例(63 pass + 5 skip |
| `tests/test_batch.py` | 840 | ⚠️ 过时 | 旧版批量测试,API 签名已变 |
| `tests/test_main.py` | 833 | ⚠️ 过时 | 旧版 E2E,引用旧入口 |
**test_sapcli.py 覆盖的模块:**
- ✅ types.py(注册、解析)— 6 用例
- ✅ config.py(环境变量)— 2 用例
- ✅ auth.pykeyring 降级)— 4 用例
- ✅ ddic.pyXML/DDL 构建)— 7 用例
- ✅ manifest.py(增删查)— 5 用例
- ✅ scanner.py(目录扫描)— 2 用例
- ✅ sorter.py(拓扑排序)— 4 用例
- ✅ exceptions.py — 2 用例
- ✅ scaffold.py(模板生成)— 4 用例
- ✅ xml_utils.py — 4 用例
- ✅ XML 注入防御 — 3 用例
- ✅ 模块结构检查 — 5 用例
- ✅ OpenSpec 文件检查 — 3 用例
- ✅ output.py — 3 用例
- ✅ analyze.py — 4 用例
**未覆盖的模块(缺口):**
- ❌ client.py30 个 API 方法)— 0 用例
- ❌ commands/*.py11 个命令模块)— 0 用例
- ❌ cli/parser.py(参数校验)— 0 用例
- ❌ cli/app.py(路由逻辑)— 0 用例
---
## 二、测试分层设计
### 分层策略
```
┌─────────────┐
│ E2E 测试 │ ← 真实 SAP 系统
│ (手动) │ 仅在上线前执行
├─────────────┤
│ 集成测试 │ ← Mock HTTP Server
│ (自动化) │ 验证 client↔ADT 通信
├─────────────┤
│ 单元测试 │ ← unittest.mock
│ (自动化) │ 验证各模块独立逻辑
├─────────────┤
│ 静态分析 │ ← pylint / mypy
│ (CI 自动) │ 语法、类型、规范
└─────────────┘
```
| 层级 | 目标 | 技术 | 运行频率 | 依赖 |
|------|------|------|---------|------|
| 静态分析 | 语法/类型/规范 | pylint, mypy | 每次 commit | 无 |
| 单元测试 | 模块独立逻辑 | unittest + mock | 每次 commit | 无 |
| 集成测试 | API 通信正确性 | unittest + responses/mock HTTP | 每次 commit | 无 |
| E2E 测试 | 端到端真实流程 | 手动执行 | 发布前 | SAP 系统 |
---
## 三、单元测试设计(优先级 P0)
> 目标:每个模块的纯逻辑都有测试,不依赖网络。
### 3.1 保留现有 68 用例
全部保留,但修复 5 个 skip
- `test_multi_profile` / `test_profile_fallback`:现在 config.py 已支持 `--profile`,应改为 pass
- `test_list_objects` / `test_pretty_print` / `test_transport_info`:方法已加回 client.py,应取消 skip
### 3.2 新增用例清单
#### A. client.py — API 层(30 方法)
每个方法测 3 个场景:成功响应、HTTP 错误、XML 解析。
| 方法 | 正常用例 | 错误用例 | mock 要点 |
|------|---------|---------|-----------|
| `login()` | 返回有效 session | 401 → LoginError | mock session.get |
| `lock()` | 返回 lock_handle + corr_nr | 403 → ObjectLockedError | mock session.post |
| `unlock()` | 返回 True | 非 200 返回 False | mock session.post |
| `set_source()` | 返回 True | 非 200 返回 False | mock session.put |
| `get_source()` | 返回源码字符串 | 404 → ObjectNotFoundError | mock session.get |
| `delete_object()` | 返回 (True, msg) | 非 200 (False, msg) | mock session.delete |
| `create_object()` | 返回 (obj_uri, src_uri) | 409 → ObjectAlreadyExistsError | mock session.post |
| `activate()` | 返回 (True, []) | 返回 (False, [errors]) | mock session.post |
| `syntax_check()` | 返回 (True, []) | 返回 (False, [errors]) | mock session.post |
| `get_object_status()` | 返回 status dict | 404 → ObjectNotFoundError | mock session.get |
| `object_exists()` | 返回 True/False | — | mock session.get |
| `list_objects()` | 返回 list[dict] | 空 → [] | mock session.get |
| `where_used()` | 返回 list[dict] | 无引用 → [] | mock session.post |
| `search_code()` | 返回 list[dict] | 无结果 → [] | mock session.post |
| `read_source_for_diff()` | 委托 get_source | — | mock self.get_source |
| `get_cds_source()` | 返回 DDL 字符串 | 404 → ObjectNotFoundError | mock session.get |
| `create_cds()` | 创建+写入+返回 URI | 失败 → SapCliError | mock lock/set_source/unlock |
| `create_package()` | 返回 True | 非 200 → False | mock session.post |
| `get_package_info()` | 返回 dict | 404 → SapCliError | mock session.get |
| `transport_info()` | 返回 dict | 404 → SapCliError | mock session.get |
| `transport_release()` | 返回 True | 非 200 → False | mock session.post |
| `transport_objects()` | 返回 list[dict] | 空 → [] | mock session.get |
| `atc_check()` | 返回 (bool, list) | 无发现 → (True, []) | mock session.post |
| `pretty_print()` | 返回格式化源码 | 失败 → SapCliError | mock session.post |
| `list_transport_requests()` | 返回 list[dict] | 空 → [] | mock session.get |
| `create_transport_request()` | 返回 corr_nr | 失败 → SapCliError | mock session.post |
| `get_transport_request()` | 返回 corr_nr 或 None | 空 → None | mock session.get |
| `create_function_group()` | 成功 | 失败 → SapCliError | mock session.post |
| `function_group_exists()` | True/False | — | mock session.get |
| `create_ddic_object()` | 成功写入 | 失败 → SapCliError | mock self._put_ddic_xml |
**小计:~90 用例**
#### B. commands/*.py — 命令层(11 模块)
每个命令测:正常流程 + 错误处理 + 参数缺失。
| 命令 | 模块 | 正常用例 | 错误用例 |
|------|------|---------|---------|
| `create` | crud.py | 创建成功(4种模板+--source+--definition | 对象已存在、类型不支持 |
| `download` | crud.py | 下载成功、文件名正确 | 对象不存在、无源码 URI |
| `sync` | crud.py | 五步同步成功 | 语法检查失败、激活失败 |
| `info` | crud.py | 显示元数据 | 对象不存在 |
| `delete` | crud.py | 确认删除、取消删除 | 对象不存在 |
| `init` | batch.py | 扫描→生成 manifest | 空目录、无 SAP 连接 |
| `refresh` | batch.py | 更新状态 | — |
| `sync --all` | batch.py | 批量同步+拓扑排序 | 循环依赖、fail-fast |
| `list` | search.py | 列出对象 | 空 |
| `whereused` | search.py | 找到引用 | 无引用 |
| `search` | search.py | 搜索结果 | 无结果 |
| `diff` | diff_cmd.py | 有差异/无差异 | — |
| `cds` | cds.py | 下载DDL/创建CDS | — |
| `package` | package_cmd.py | 创建/查询包 | — |
| `transport` | transport.py | 列出/详情/释放 | — |
| `check` | quality.py | ATC检查有/无发现 | — |
| `format` | quality.py | 格式化成功 | — |
| `analyze` | analyze.py | 分析依赖 | 无依赖 |
| `scaffold` | scaffold.py | 4种模板+无模板列出 | — |
| `config` | config_cmd.py | show/list-profiles/set | — |
| `auth` | auth.py | login/logout/status | keyring 不可用 |
**小计:~60 用例**
#### C. cli/ — 入口层
| 测试场景 | 说明 |
|---------|------|
| 无参数 → exit(1) + 帮助 | `app.py` 检测 no command |
| --profile DEV | 读 [DEV] section |
| --config 自定义路径 | 读指定文件 |
| config set host 1.2.3.4 | 写入 config.ini |
| config set invalid_key | 报错 |
| auth status | keyring 状态 |
| scaffold 无 --template | 列出模板 |
| scaffold --template xxx --name ZZZ | 生成模板 |
**小计:~10 用例**
### 3.3 单元测试总计
| 类别 | 现有 | 新增 | 合计 |
|------|------|------|------|
| 基础层 | 68 | 0 | 68 |
| API 层 (client.py) | 0 | ~90 | 90 |
| 命令层 (commands/) | 0 | ~60 | 60 |
| 入口层 (cli/) | 0 | ~10 | 10 |
| **总计** | **68** | **~160** | **~228** |
---
## 四、集成测试设计(优先级 P1)
> 目标:验证 client.py 与真实 ADT API 的通信协议是否正确。
### 4.1 技术方案:`responses` 库 mock HTTP
```python
import responses
@responses.activate
def test_login_success():
responses.add(responses.GET, "https://sap.example.com/sap/bc/adt/...",
status=200, headers={"x-csrf-token": "TOKEN123"})
client = ADTClient("sap.example.com", "100", "USER", "PASS")
client.login()
assert client.csrf_token == "TOKEN123"
```
### 4.2 集成测试用例
按 ADT API 端点分组,验证 HTTP 方法和 XML 请求体:
| 端点组 | 测试场景 | 数量 |
|--------|---------|------|
| 认证 | login → CSRF token 获取 | 2 |
| 锁管理 | lock/unlock → 正确的 If-Match header | 4 |
| 源码读写 | get/set_source → 正确的 Content-Type | 4 |
| 对象 CRUD | create/delete → 正确的 XML body | 6 |
| DDIC | create_ddic → 正确的 XML namespace | 4 |
| 激活/检查 | activate/syntax_check → 正确的 XML 响应解析 | 4 |
| 搜索 | list/search/whereused → URL 参数编码 | 4 |
| 传输 | transport list/info/release → URL 拼接 | 4 |
| 质量 | atc_check/pretty_print → 请求体格式 | 4 |
| **合计** | | **~36** |
---
## 五、E2E 测试设计(优先级 P2)
> 目标:端到端验证完整用户场景。
### 5.1 前置条件
- 需要一台可连接的 SAP 系统
- 使用测试用户和 `$TMP`
- 所有操作可逆(删除测试对象)
### 5.2 测试场景
| # | 场景 | 步骤 | 验证点 |
|---|------|------|--------|
| E1 | 完整 CRUD | create → info → download → sync → delete | 每步输出正确,对象最终不存在 |
| E2 | DDIC 对象 | create domain → create dataelement → create table | 依赖顺序正确 |
| E3 | 批量同步 | init → 修改本地文件 → sync --all | 所有对象同步成功 |
| E4 | 多 Profile | config set → --profile DEV → download | 读取正确配置 |
| E5 | 传输管理 | create transport → create object → release | 传输号关联正确 |
**E2E 测试建议用手动执行**,因为:
- 依赖真实 SAP 系统可用性
- 测试数据需要隔离
- 某些操作不可逆(如 transport release
### 5.3 E2E 自动化(可选)
如果 SAP 测试系统长期可用,可用 `tests/e2e_project/` 现有的测试数据做半自动 E2E
```bash
python main.py init --path tests/e2e_project
python main.py sync --all --path tests/e2e_project
```
---
## 六、现有测试处理
| 文件 | 处理 | 原因 |
|------|------|------|
| `tests/test_sapcli.py` | ✅ 保留并扩展 | 当前唯一活跃测试 |
| `tests/test_batch.py` | ❌ 废弃 | 840 行,API 签名已变,与新架构不兼容 |
| `tests/test_main.py` | ❌ 废弃 | 833 行,引用旧入口 `src/` 路径 |
| `tests/test_e2e.md` | ✅ 保留 | E2E 手动测试说明仍有参考价值 |
---
## 七、测试文件组织
```
tests/
├── conftest.py # 共享 fixtureSAPConfig mock、临时目录)
├── test_sapcli.py # 基础层测试(保留现有 68 → 修复 5 skip)
├── unit/ # 新增:单元测试
│ ├── test_client.py # client.py 30 方法
│ ├── test_commands.py # commands/ 11 模块
│ └── test_cli.py # parser + app 路由
├── integration/ # 新增:集成测试
│ ├── test_adt_auth.py # 认证通信
│ ├── test_adt_crud.py # CRUD 通信
│ ├── test_adt_search.py # 搜索通信
│ ├── test_adt_transport.py # 传输通信
│ └── test_adt_quality.py # 质量通信
├── e2e/ # E2E 手动测试
│ └── test_e2e.md # 手动测试说明
├── e2e_project/ # E2E 测试数据
│ ├── manifest.json
│ ├── reports/
│ ├── classes/
│ └── functions/
└── fixtures/ # 测试固件
├── adt_responses/ # ADT XML 响应样本
│ ├── login_success.xml
│ ├── object_info.xml
│ └── syntax_errors.xml
└── sample_source/ # 示例 ABAP 源码
├── zhello.abap
└── zcl_class.abap
```
---
## 八、实施优先级
| 阶段 | 内容 | 用例数 | 预计工时 |
|------|------|--------|---------|
| **P0** | 修复 5 个 skip + 清理旧测试 | +5 pass, -2 文件 | 1h |
| **P0** | client.py 单元测试 | ~90 | 4h |
| **P1** | commands/ 单元测试 | ~60 | 3h |
| **P1** | cli/ 入口测试 | ~10 | 1h |
| **P2** | 集成测试(mock HTTP | ~36 | 3h |
| **P3** | E2E 手动测试 | 5 场景 | 2h |
| | **合计** | **~228** | **~14h** |
### 立即可做(本次)
1. 修复 5 个 skip 用例 → 68 pass
2. 清理旧测试文件 → 减少 1673 行过时代码
3. 新建测试目录结构
4. 从 client.py 核心方法开始写单元测试
---
## 九、关键技术选型
| 工具 | 用途 | 是否需要安装 |
|------|------|-------------|
| `unittest` | 测试框架 | ✅ 内置 |
| `unittest.mock` | mock client/requests | ✅ 内置 |
| `responses` | mock HTTP 响应(集成测试) | 需要 `pip install responses` |
| `tempfile` | 临时文件/目录 | ✅ 内置 |
| `pytest` | 可选:更简洁的断言 + fixture | 需要 `pip install pytest` |
**建议**:保持 `unittest` 框架(不引入 pytest),减少外部依赖。集成测试用 `unittest.mock.patch` 替代 `responses`