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
+97
View File
@@ -0,0 +1,97 @@
# AGENTS.md — AI 代理协作指南
## 项目概述
sap-cli 是 Python CLI 工具,通过 SAP ADT REST API 管理 ABAP 开发对象。
修改代码前请先阅读 `CLAUDE.md` 了解项目结构和开发规范。
## OpenSpec 规范驱动开发
本项目使用 OpenSpec 进行规范驱动开发。**修改任何功能前,必须先查阅对应的 spec 文件。**
### Spec 文件位置
所有系统行为的 source of truth 位于 `openspec/specs/` 下:
| 域 | 文件 | 内容 |
|----|------|------|
| 连接与认证 | `openspec/specs/connection/spec.md` | 登录、认证、会话管理 |
| 对象生命周期 | `openspec/specs/object-lifecycle/spec.md` | 16 种对象类型的 CRUD |
| 批量操作 | `openspec/specs/batch-ops/spec.md` | 初始化、批量同步、清单刷新 |
| 搜索与浏览 | `openspec/specs/search-browse/spec.md` | 对象列表、where-used、源码搜索 |
| 传输管理 | `openspec/specs/transport/spec.md` | 传输请求管理、传输对象列表 |
| 代码质量 | `openspec/specs/quality/spec.md` | ATC 检查、代码格式化、Diff |
| 配置管理 | `openspec/specs/config/spec.md` | 多系统配置、凭据存储、Profile |
### 变更流程
1. **查阅 spec**: 在 `openspec/specs/` 中找到相关的 domain spec
2. **创建 change**: 在 `openspec/changes/<name>/` 下创建提案
3. **编写 proposal.md**: 描述变更内容、影响范围
4. **编写 design.md**: 技术设计,包含 ADT 端点详情
5. **编写 tasks.md**: 任务列表,每个任务独立可测试
6. **实现**: 按 tasks.md 逐步编写代码
7. **更新 spec**: 完成后将变更合并回 specs/
### Spec 编写规则
- 使用 Given/When/Then 格式编写 Scenario
- 每个 Requirement MUST 指定 ADT API 端点
- 使用 RFC 2119 关键词: SHALL(必须)、MUST(强制)、SHOULD(推荐)
- 中文编写说明文字,英文编写 Requirement 名称和 Scenario 关键词
## 代码修改指南
### 修改前检查清单
- [ ] 阅读了相关的 spec 文件
- [ ] 理解了现有的代码结构
- [ ] 已创建 change proposal(如果涉及功能变更)
- [ ] 了解受影响的 ADT 端点
### 代码风格
- PEP 8 编码规范
- 使用 type hints`from __future__ import annotations`
- 使用 f-strings 格式化字符串
- 函数和类使用 docstring
- logging 使用 `logging.getLogger("sapcli.module")`
### 新增对象类型
如需添加新的 ABAP 对象类型支持:
1.`sapcli/types.py` 中定义 `ObjectTypeConfig``_register()`
2. 更新 `openspec/specs/core/spec.md` 的类型注册表
3. 如有特殊的 URI 模板或创建逻辑,在 `sapcli/client.py` 中处理
4.`sapcli/scanner.py``DIRECTORY_TYPE_MAP` 中添加目录映射
5. 编写测试用例
### 新增 CLI 命令
1.`sapcli/cli/parser.py``build_parser()` 中添加 subparser
2.`sapcli/commands/` 下实现 `cmd_xxx()` 函数(或添加到现有模块)
3.`sapcli/commands/__init__.py` 中导出
4.`sapcli/cli/app.py``command_map` 中注册
5. 更新相关 spec 文件
6. 编写测试用例
### 错误处理
- 所有自定义异常继承自 `SapCliError`
- 新增异常类型在 `sapcli/exceptions.py` 中定义
- CLI 主循环捕获 `SapCliError`,打印友好信息后 exit(1)
## 测试
```bash
python -m pytest tests/test_sapcli.py -v
```
## 注意事项
- SAP ADT REST API 使用 HTTP Basic Auth + CSRF Token
- 大部分 ADT 端点需要 stateful 会话(锁定/写入时)
- DDIC 对象通过 XML body 创建(非纯文本)
- function 类型名称格式: `组名/模块名`
- tabletype 使用 VIT 端点,需要大写名称
+180
View File
@@ -0,0 +1,180 @@
# sap-cli — AI 协作指南
> [!danger] 核心约束:文档优先,禁止擅自读取源代码
> 本项目提供了完整的技术文档体系,AI 在协作时**必须优先阅读文档**获取所需信息,**严禁未经许可直接读取 Python 源代码文件**`sapcli/` 包内的 `.py` 文件、`main.py`、`test/` 目录)。
>
> 如果发现文档描述与实际行为不一致,**必须先向用户说明差异并征得同意**,才能读取源代码进行校验。
## 项目简介
sap-cli 是一个命令行工具,通过 SAP ADT REST API 实现 ABAP 开发对象的远程管理(创建、同步、下载、查询、删除),让开发者可以在本地编辑器中编写 ABAP 代码,一行命令同步到 SAP 系统。
## 文档体系(AI 必读)
> [!important] 文档覆盖了项目的全部设计细节,AI 应按需查阅以下文档,而非阅读源代码。
| 文档 | 用途 | 文件路径 |
| 文档 | 说明 | 路径 |
|------|------|------|
| 使用指南 | 命令详解、参数说明、输出示例 | `docs/guide.md` |
| 技术说明 | 架构、模块职责、通信流程、异常体系 | `docs/dev/architecture.md` |
| 批量同步设计 | manifest 清单、拓扑排序、批量 sync | `docs/dev/batch-design.md` |
| 测试报告 | 测试场景覆盖 | `docs/dev/test-report.md` |
| ADT 学习笔记总览 | ADT 原理系列索引 | `docs/adt/README.md` |
### 按需求查阅
| 需求 | 文档 |
|------|------|
| 了解某个命令怎么用 | `docs/guide.md` |
| 了解模块接口 / API 调用链 | `docs/dev/architecture.md` |
| 了解批量同步 / manifest | `docs/dev/batch-design.md` |
| 了解 ADT REST API 原理 | `docs/adt/` 目录下的系列笔记 |
| 了解支持哪些对象类型 | `README.md` 功能矩阵 |
## AI 行为规范
### 1. 文档优先原则
- **所有问题先查文档** — 项目文档完整覆盖了架构、命令、API、模块接口、异常体系等全部信息
- **文档即真相** — 以文档描述为准进行操作,不需要通过读代码"验证"
- **引用文档回答** — 回答用户问题时引用具体文档路径,方便用户追溯
### 2. 源代码访问限制
> [!warning] 以下文件属于源代码,未经用户许可**禁止读取**:
> - `main.py` — CLI 入口
> - `sapcli/*.py` — 核心包(config / types / client / commands / exceptions / manifest / scanner / sorter
> - `test/**/*.py` — 测试套件
**允许读取的文件:**
- `*.md` — 所有文档文件
- `*.abap` — ABAP 源代码文件(这是用户要同步到 SAP 的业务代码)
- `config.ini` — 连接配置(注意不要泄露密码)
- `manifest.json` — 项目清单文件
- `*.drawio` / `*.png` — 架构图等资源文件
### 3. 源代码校验流程
当文档描述与实际执行效果不一致时:
```
1. 向用户报告:"文档描述 XXX,但实际表现为 YYY,差异点为 ZZZ"
2. 说明需要读取哪些源文件、读取目的是什么
3. 等待用户明确同意后,方可读取源代码
4. 读取后只关注差异点,不要全量阅读无关代码
```
### 4. 典型操作流程
#### 帮助用户同步代码到 SAP
```
1. 确认用户的 ABAP 文件路径和对象信息(名称、类型)
2. 执行: python main.py sync --name <名称> --type <类型> --path <文件路径>
3. 如果成功 → 告知用户
4. 如果语法检查失败 → 展示错误信息,提示用户修改后重试
5. 如果激活失败 → 展示错误信息,提示用户修改后重试
```
#### 帮助用户创建新对象
```
1. 确认对象名称、类型、描述
2. 执行: python main.py create --name <名称> --type <类型> --description "<描述>"
3. 创建成功后提示用户下载模板或直接编辑
```
#### 帮助用户批量同步
```
1. 确认项目根目录路径
2. 如果没有 manifest.json,先执行: python main.py init --path <项目目录>
3. 预览执行计划: python main.py sync --all --path <项目目录> --dry-run
4. 确认后执行(推荐指定 --corr_nr 避免交互中断):
python main.py sync --all --path <项目目录> --corr_nr <传输请求号>
5. 如果不指定 --corr_nr,会在启动时弹出一次传输请求选择,后续自动复用
```
## 命令速查
```bash
# 单对象操作
python main.py create --name <名称> --type <类型> [--description <描述>] [--corr_nr <请求号>]
python main.py info --name <名称> --type <类型>
python main.py download --name <名称> --type <类型> --path <保存目录>
python main.py sync --name <名称> --type <类型> --path <.abap文件> [--corr_nr <请求号>]
python main.py delete --name <名称> --type <类型>
# 批量操作(基于 manifest 清单)
python main.py init --path <项目目录>
python main.py sync --all --path <项目目录> [--dry-run] [--fail-fast]
python main.py refresh --path <项目目录>
# 搜索与浏览
python main.py list --type <类型> [--package <包名>] [--prefix <前缀>]
python main.py whereused --name <名称> --type <类型>
python main.py search --query <关键词> [--type <类型>]
# 传输管理
python main.py transport list
python main.py transport info --corr_nr <请求号>
python main.py transport release --corr_nr <请求号>
python main.py transport objects --corr_nr <请求号>
# 代码质量
python main.py check --name <名称> --type <类型>
python main.py format --name <名称> --type <类型>
python main.py diff --name <名称> --type <类型> [--path <本地文件>]
# 包管理
python main.py package create --name <包名> [--description <描述>]
python main.py package info --name <包名>
python main.py package list --name <包名>
# CDS View
python main.py cds download --name <CDS名> --path <目录>
python main.py cds sync --name <CDS名> --path <DDL文件>
python main.py cds create --name <CDS名> [--description <描述>]
# 辅助工具
python main.py analyze --path <项目目录>
python main.py scaffold --name <名称> --template <模板> [--package <包名>]
python main.py config show
python main.py config list-profiles
```
## 对象类型速查
| 类型参数 | 说明 | 名称格式 |
|---------|------|---------|
| `report` | 程序/报表 | 直接使用程序名 |
| `class` | ABAP 类 | 直接使用类名 |
| `interface` | ABAP 接口 | 直接使用接口名 |
| `function` | 函数模块 | `函数组名/函数模块名` |
| `functiongroup` | 函数组 | 直接使用函数组名 |
| `include` | Include 程序 | 直接使用程序名 |
| `domain` | 域 | 直接使用域名 |
| `dataelement` | 数据元素 | 直接使用元素名 |
| `table` | 透明表 | 直接使用表名 |
| `structure` | 结构 | 直接使用结构名 |
| `tabletype` | 表类型 | 直接使用类型名 |
| `cdsview` | CDS View | 直接使用 CDS 名 |
| `messageclass` | 消息类 | 直接使用消息类名 |
| `view` | 数据库视图 | 直接使用视图名 |
| `searchhelp` | 搜索帮助 | 直接使用搜索帮助名 |
| `lockobject` | 锁对象 | 直接使用锁对象名 |
## 依赖排序优先级(批量同步)
```
domain(10) → dataelement(20) → structure(25) → table(30) → tabletype(40) → view(42) → lockobject(44) → searchhelp(46) → messageclass(48) → interface(50) → class(60) → function(70) → functiongroup(75) → include(78) → cdsview(79) → report(80)
```
## 关键技术要点
- **认证**: HTTP Basic Auth + CSRF Token
- **sync 流程**: 检查/创建 → 锁定 → 写入 → 解锁 → 语法检查 → 激活
- **锁定机制**: stateful session,自动检测传输请求绑定
- **配置加载优先级**: 环境变量 > --config 指定文件 > 工作目录 config.ini > 脚本目录 config.ini
- **唯一第三方依赖**: `requests`
+51
View File
@@ -0,0 +1,51 @@
# ADT 对象类型码映射(来源:abap-adt-api objectcreator.ts
## 已确认的 ADT 可创建类型
| TypeId | 对象类型 | ADT 端点路径 |
|--------|---------|-------------|
| CLAS/OC | Class | /oo/classes |
| INTF/OI | Interface | /oo/interfaces |
| PROG/P | Program/Report | /programs/programs |
| PROG/I | Include | /programs/includes |
| FUGR/F | Function Module | /functions/groups/{grp}/fmodules |
| FUGR/FF | Function Group | /functions/groups |
| DOMA/DD | Domain | /ddic/domains |
| DTEL/DE | Data Element | /ddic/dataelements |
| TABL/DT | Table | /ddic/tables |
| TABL/DS | Structure | /ddic/structures |
| MSAG/N | Message Class | /oo/t100/messages/classes |
| DDLS/DF | CDS View (DDL Source) | /ddic/ddlsources |
| DCLS/DL | CDS Access Control (DCL) | /ddic/dclsources |
| DDLX/EX | Metadata Extension | /ddic/metadataextensions |
| DDLA/ADF | Abstract Entity | /ddic/ddlabstractentities (推测) |
| SRVD/SRV | Service Definition | /ddic/servicedefinitions (推测) |
| AUTH | Authorization Object | (需验证) |
| SUSO/B | Auth Check Object | (需验证) |
## 确认不在 ADT 标准创建列表中的类型
| 类型 | 原因 | 替代方案 |
|------|------|---------|
| Type Group (TYPE-POOL) | ADT 可能归入 programs | 通过 /programs/typegroups 尝试 |
| Number Range Object | 无 ADT 创建端点 | 需 RFCNUMBER_RANGE_GET_INFO |
| Enhancement Spot | 使用专用 /enhancements 端点 | 需验证 |
| Behavior Definition (BDEF) | RAP 专用,S/4HANA Cloud 才有 | NW 7.40 可能不支持 |
| SICF Service | 使用 /discovery/services | 需验证 |
| Table Type | 通过 VIT 端点 | 已有部分支持 |
## Phase 2 调整建议
### 可直接实现(ADT 有标准端点)
- ✅ DCL (CDS Access Control) — /ddic/dclsources,类似 CDS View
- ✅ DDLX (Metadata Extension) — /ddic/metadataextensions,类似 CDS View
- ✅ DDLA (Abstract Entity) — 类似 CDS View
### 需要验证端点后决定
- ⚠️ Type Group — 可能在 /programs/typegroups
- ⚠️ Enhancement Spot — 可能在 /enhancements
- ⚠️ SICF Service — 可能通过 /discovery/services
### NW 7.40 不支持(需移到远期)
- ❌ Behavior Definition (BDEF) — RAP 专用,NW 7.40 无此功能
- ❌ Number Range Object — 无 ADT 端点,需 RFC
+379
View File
@@ -0,0 +1,379 @@
# sap-cli — 技术说明
## 目录
- [项目定位](#项目定位)
- [架构概览](#架构概览)
- [包结构](#包结构)
- [模块职责](#模块职责)
- [通信流程](#通信流程)
- [对象类型映射](#对象类型映射)
- [异常体系](#异常体系)
- [配置加载策略](#配置加载策略)
- [日志机制](#日志机制)
- [错误处理策略](#错误处理策略)
- [测试架构](#测试架构)
- [依赖关系](#依赖关系)
---
## 项目定位
sap-cli 是一个轻量级命令行工具,通过 SAP ADT (ABAP Development Tools) REST API 与 SAP NetWeaver 系统交互,实现 ABAP 开发对象的远程管理(创建、同步、下载、查询、删除)。
**核心设计原则:**
- 零 SAP 专有客户端依赖,仅基于标准 HTTP 协议
- 唯一第三方依赖为 `requests`
- 所有通信均通过 ADT REST API 完成,数据格式为 XML / Plain Text
---
## 架构概览
![[sap-cli/doc/assets/architecture.drawio.png]]
架构分为三层:
| 层级 | 组件 | 说明 |
|------|------|------|
| **用户终端** | `main.py` | CLI 入口,argparse 参数解析与命令分发 |
| **核心包** | `sapcli/` | 5 个模块:config / types / exceptions / client / commands |
| **外部系统** | SAP NetWeaver | ADT REST API 端点(Programs / OO Objects / DDIC / CTS |
左侧外部文件通过虚线连接至核心包:
- `config.ini` — 连接配置(读取)
- `.abap 文件` — 本地源代码(读写)
- `log/` — 运行日志(写入)
核心通信链路:`client.py (ADTClient)` 通过 HTTP/RESTBasic Auth + CSRF Token)与 SAP 系统交互。
## 包结构
```
sap-cli/
├── main.py # CLI 入口(argparse + 命令分发)
├── config.ini # 默认连接配置文件
├── sapcli/ # 核心功能包
│ ├── __init__.py
│ ├── config.py # 配置加载(INI / 环境变量)
│ ├── types.py # 对象类型定义与名称解析
│ ├── client.py # ADT REST API 客户端
│ ├── commands.py # 5 个业务命令的实现
│ └── exceptions.py # 自定义异常层级
├── test/
│ └── script/
│ └── test_main.py # 集成测试套件(子进程调用)
├── doc/ # 文档
│ ├── ADT/ # ADT 技术原理系列
│ ├── 技术说明.md # 本文档
│ ├── sap-cli使用指南.md # 用户操作指南
│ └── 测试报告.md # 测试覆盖报告
└── log/ # 运行时日志目录
```
---
## 模块职责
### main.py — CLI 入口
| 组件 | 职责 |
|------|------|
| 参数定义 | 定义 5 个子命令(download / sync / delete / info / create)及其参数 |
| 配置加载 | 调用 `sapcli.config.load_config()` 获取连接参数 |
| 客户端初始化 | 创建 `ADTClient` 实例并执行登录(获取 CSRF Token |
| 命令分发 | 根据子命令名称映射到对应的 `cmd_*()` 函数 |
### sapcli/config.py — 配置管理
| 组件 | 职责 |
|------|------|
| `SAPConfig` | 连接参数数据类(host / client / user / password |
| `load_config()` | 按优先级链加载配置:环境变量 → 指定文件 → 工作目录 → 脚本目录 |
### sapcli/types.py — 对象类型系统
| 组件 | 职责 |
|------|------|
| `ObjectTypeConfig` | 单个 ABAP 对象类型的 URI 模板与元数据(数据类) |
| `OBJECT_TYPE_CONFIG` | 9 种 ABAP 对象类型的注册表 |
| `ParsedName` | 解析后的对象名组件(数据类) |
| `parse_object_name()` | 将用户输入解析为 ADT URI 所需的路径片段 |
| `get_type_config()` | 按类型键查询对象类型配置 |
| `all_type_keys()` | 返回所有受支持的对象类型列表 |
### sapcli/client.py — ADT 通信层
| 方法 | 职责 |
|------|------|
| `login()` | Basic Auth 认证 + 获取 CSRF Token |
| `object_exists()` | 检查对象是否存在于 SAP 系统 |
| `get_source()` | 下载对象源代码 |
| `set_source()` | 上传(写入)对象源代码 |
| `lock()` / `unlock()` | 对象锁定 / 解锁(stateful session |
| `get_transport_request()` | 获取可用的传输请求号 |
| `activate()` | 激活对象 |
| `syntax_check()` | 语法检查 |
| `create_object()` | 创建新对象(支持 9 种类型) |
| `delete_object()` | 删除对象 |
| `function_group_exists()` | 检查函数组是否存在 |
| `create_function_group()` | 创建函数组容器 |
### sapcli/commands.py — 命令处理
| 函数 | 职责 |
|------|------|
| `cmd_download()` | 下载对象源码到本地 `.abap` 文件 |
| `cmd_sync()` | 同步本地源码到 SAP(含自动创建、锁定、写入、检查、激活) |
| `cmd_info()` | 查询并展示对象元数据 |
| `cmd_delete()` | 从 SAP 系统删除对象(需用户确认) |
| `cmd_create()` | 在 SAP 系统创建新对象(支持模板预填充) |
### sapcli/exceptions.py — 异常层级
| 异常类 | 触发场景 |
|--------|---------|
| `SapCliError` | 所有自定义异常的基类 |
| `ConfigError` | 配置文件缺失或格式错误 |
| `LoginError` | SAP 登录失败(认证错误或网络不通) |
| `ObjectNotFoundError` | 请求的对象在 SAP 系统中不存在 |
| `ObjectAlreadyExistsError` | 创建时对象已存在 |
| `LockError` | 对象锁定冲突 |
| `ActivationError` | 对象激活失败 |
| `SyntaxCheckError` | 语法检查未通过 |
| `DeleteError` | 删除操作失败 |
| `CreateError` | 创建操作失败 |
| `InvalidNameError` | 对象名称格式不合法 |
---
## 通信流程
### 认证机制
采用 HTTP Basic Auth + CSRF Token 双重验证:
```
┌────────┐ ┌────────┐
│ Client │ │ SAP │
└───┬────┘ └───┬────┘
│ GET /sap/bc/adt/compatibility/graph │
│ Header: x-csrf-token: fetch │
│ Header: Authorization: Basic *** │
│ ──────────────────────────────────────────► │
│ │
│ 200 OK │
│ Header: x-csrf-token: <TOKEN> │
│ ◄────────────────────────────────────────── │
│ │
│ 后续所有请求携带 CSRF Token │
│ (POST / PUT / DELETE 必须携带) │
│ ──────────────────────────────────────────► │
```
### sync 命令 — 完整 API 调用链
`sync` 是最核心的命令,执行以下完整流程:
```
┌─────────────────────────────────────────────────────────────┐
│ Step 1: 登录获取 CSRF Token │
│ GET /sap/bc/adt/compatibility/graph │
├─────────────────────────────────────────────────────────────┤
│ Step 2: 检查对象存在性(不存在则自动创建) │
│ GET /sap/bc/adt/programs/programs/{name} │
├─────────────────────────────────────────────────────────────┤
│ Step 3: 获取传输请求号 │
│ GET /sap/bc/adt/cts/transportrequests │
├─────────────────────────────────────────────────────────────┤
│ Step 4: 锁定对象(stateful session
│ POST /sap/bc/adt/programs/programs/{name} │
│ ?_action=LOCK&accessMode=MODIFY │
├─────────────────────────────────────────────────────────────┤
│ Step 5: 写入源代码 │
│ PUT /sap/bc/adt/programs/programs/{name}/source/main │
│ ?lockHandle={handle} │
├─────────────────────────────────────────────────────────────┤
│ Step 6: 解锁对象 │
│ POST /sap/bc/adt/programs/programs/{name} │
│ ?_action=UNLOCK&lockHandle={handle} │
├─────────────────────────────────────────────────────────────┤
│ Step 7: 语法检查(可选) │
│ POST /sap/bc/adt/activation?method=check │
├─────────────────────────────────────────────────────────────┤
│ Step 8: 激活对象 │
│ POST /sap/bc/adt/activation?method=activate │
└─────────────────────────────────────────────────────────────┘
```
### 锁定与传输请求 — 容错机制
工具内置了两层自动重试策略:
1. **锁定重试** — 若对象已在其他传输请求中被锁定,从错误响应中提取已有的传输请求号,使用该传输号重新锁定
2. **写入重试** — 若写入源码时因传输号不匹配而失败,自动使用正确的传输号重试
### Session 管理
| 操作类型 | Session 模式 | 说明 |
|---------|-------------|------|
| 锁定 / 写入 / 解锁 | **stateful** | 通过 `X-sap-adt-sessiontype: stateful` 头维持会话状态 |
| 读取 / 激活 | **stateless** | 无状态请求,独立响应 |
| 解锁后 | 自动切换回 **stateless** | 确保会话资源释放 |
---
## 对象类型映射
每种 ABAP 对象类型对应不同的 ADT URI 模板:
| 类型 | 对象 URI | 源码 URI | 备注 |
|------|---------|---------|------|
| `report` | `/sap/bc/adt/programs/programs/{name}` | `.../source/main` | ABAP 报表程序 |
| `class` | `/sap/bc/adt/oo/classes/{name}` | `.../source/main` | ABAP 类 |
| `interface` | `/sap/bc/adt/oo/interfaces/{name}` | `.../source/main` | ABAP 接口 |
| `function` | `/sap/bc/adt/functions/groups/{group}/fmodules/{name}` | `.../source/main` | 需 `函数组/函数名` 格式 |
| `functiongroup` | `/sap/bc/adt/functions/groups/{name}` | 无源码 | 仅容器,不含源码 |
| `domain` | `/sap/bc/adt/ddic/domains/{name}` | `.../source/main` | ABAP 域 |
| `dataelement` | `/sap/bc/adt/ddic/dataelements/{name}` | `.../source/main` | ABAP 数据元素 |
| `table` | `/sap/bc/adt/ddic/tables/{name}` | `.../source/main` | ABAP 透明表 |
| `tabletype` | `/sap/bc/adt/vit/wb/object_type/ttypda/object_name/{name}` | 无源码 (VIT 端点) | ABAP 表类型,仅支持元数据查询 |
> [!note] function 类型的特殊性
> `function` 类型需要 `函数组名/函数名` 格式输入(如 `ZGROUP/Z_MY_FUNC`),URI 中包含两层路径参数(`{group}` 和 `{name}`)。
---
## 配置加载策略
配置按以下优先级链加载,高优先级源覆盖低优先级源:
```
┌──────────────────────────────┐
│ 环境变量 (SAP_HOST 等) │ ← 最高优先级
└──────────────┬───────────────┘
│ 覆盖
┌──────────────▼───────────────┐
│ --config 指定的 INI 文件 │
└──────────────┬───────────────┘
│ 回退
┌──────────────▼───────────────┐
│ 当前工作目录 / config.ini │
└──────────────┬───────────────┘
│ 回退
┌──────────────▼───────────────┐
│ 脚本同目录 / config.ini │ ← 最低优先级
└──────────────────────────────┘
```
支持的环境变量映射:
| INI 字段 | 环境变量 |
|---------|---------|
| `host` | `SAP_HOST` |
| `client` | `SAP_CLIENT` |
| `user` | `SAP_USER` |
| `password` | `SAP_PASSWORD` |
---
## 日志机制
| 属性 | 值 |
|------|-----|
| 日志路径 | `log/adt_tools.log` |
| 日志级别 | `DEBUG`(记录所有 HTTP 请求/响应详情) |
| 日志格式 | `时间 │ 级别 │ 消息` |
| 写入模式 | 追加写入(append) |
日志记录每次 API 调用的完整上下文:请求 URL、请求参数、响应状态码、响应体摘要,便于生产环境问题排查。
---
## 错误处理策略
### 按场景分类
| 场景 | 处理方式 | 对应异常 |
|------|---------|---------|
| 登录失败 | 终止并报错 | `LoginError` |
| 对象不存在(download / info / delete | 提示不存在并退出 | `ObjectNotFoundError` |
| 对象不存在(sync) | 自动创建空对象后继续 | — |
| 对象已存在(create) | 提示已存在并退出 | `ObjectAlreadyExistsError` |
| 语法检查失败 | 显示错误详情,不执行激活 | `SyntaxCheckError` |
| 激活失败 | 显示错误/警告详情并退出 | `ActivationError` |
| 锁定冲突 | 自动提取已有传输号重试 | `LockError` |
| 写入冲突 | 自动使用正确传输号重试 | — |
| 删除操作 | 需用户输入 `yes` 确认 | — |
### 错误处理设计原则
1. **快速失败** — 非预期错误立即终止,避免静默失败
2. **自动恢复** — 锁定冲突和写入冲突具备自动重试能力
3. **信息透明** — 所有错误均输出详细信息,日志记录完整上下文
---
## 测试架构
测试套件位于 `test/script/test_main.py`,通过子进程调用 `main.py`,确保端到端验证。
### 测试设计
- **独立性** — 每个测试场景独立运行,使用时间戳生成唯一临时对象名
- **完整性** — 每个场景覆盖完整生命周期(创建 → 验证 → 修改 → 验证 → 删除 → 验证)
- **自清理** — 测试结束后自动清理临时文件(`test_output/` 目录)
- **可分组** — 支持按场景组选择运行
### 测试场景组
| 组名 | 说明 | 包含场景 |
|------|------|---------|
| `happy` | 正常流程 | Report / Class / Function+FunctionGroup / Interface 生命周期 |
| `error` | 错误处理 | 语法错误 / 对象不存在 / 重复创建 |
| `edge` | 边界条件 | sync 自动创建 / 删除确认 / DDIC 查询 |
| `existing` | 已有对象操作 | 对已有对象的下载和同步 |
| `config` | 配置验证 | 配置文件加载测试 |
---
## 依赖关系
### 运行时依赖
```
main.py
├── sapcli/ # 内部包
│ ├── config.py
│ │ └── configparser # 解析 INI 配置文件
│ ├── client.py
│ │ ├── requests # HTTP 通信(唯一第三方依赖)
│ │ └── xml.etree # 解析 ADT XML 响应
│ ├── commands.py
│ │ └── logging # 日志记录
│ ├── types.py
│ │ └── re # 对象名称解析
│ └── exceptions.py
│ └── (无外部依赖)
├── argparse # 命令行参数解析
└── logging # 日志配置
```
### 测试时依赖
```
test/script/test_main.py
├── subprocess # 子进程调用 main.py
└── 标准库(os, sys, time, shutil, re
```
### 第三方依赖汇总
| 依赖 | 用途 | 安装方式 |
|------|------|---------|
| `requests` | HTTP 通信 | `pip install requests` |
> [!tip] 零额外依赖
> 除 `requests` 外,所有依赖均为 Python 标准库,无需额外安装。
+684
View File
@@ -0,0 +1,684 @@
---
title: 批量同步设计 — 项目清单驱动
created: 2026-05-25
updated: 2026-05-25
tags:
- SAP
- ADT
- design
parent: "[[sap-cli/doc/sap-cli使用指南|sap-cli使用指南]]"
related:
- "[[sap-cli/doc/技术说明|技术说明]]"
- "[[sap-cli/doc/sap-cli使用指南#4.3 源代码同步激活 (`sync`)|sync 命令]]"
---
# 批量同步设计 — 项目清单驱动
> [!abstract] 概述
> 为 sap-cli 工具增加**项目清单**机制,支持批量源代码同步。核心思路:在源代码目录下维护 `manifest.json` 清单文件,记录每个开发对象在 SAP 系统中的状态,后续操作基于清单驱动,避免重复查询。
> [!info] 设计动机
> 现有 `sync` 命令只支持单对象操作,缺少以下能力:
> - 批量扫描本地文件并识别对象
> - 预查询 SAP 系统状态,制定执行计划
> - 按依赖关系排序,批量执行同步
> - 缓存对象状态,避免每次重复查询
## 1. 目录约定
### 1.1 项目目录结构
项目根目录下按对象类型组织子目录,`manifest.json` 位于根目录:
```
project/ ← --path 指向这里(项目根目录)
├── manifest.json ← 状态清单(init 自动生成)
├── reports/
│ └── {对象名小写}.abap
├── classes/
│ └── {对象名小写}.abap
├── interfaces/
│ └── {对象名小写}.abap
├── functions/
│ └── {函数组名小写}/
│ └── {函数模块名小写}.abap
├── domains/
│ └── {对象名小写}.abap
├── dataelements/
│ └── {对象名小写}.abap
├── tables/
│ └── {对象名小写}.abap
└── tabletypes/
└── {对象名小写}.abap
```
### 1.2 目录名 → 类型映射
`init` 扫描时按以下映射表从目录名推断对象类型:
| 目录名 | 对象类型 | 文件 → 对象名规则 |
|--------|---------|------------------|
| `reports/` | `report` | `zmy_report.abap``ZMY_REPORT` |
| `classes/` | `class` | `zcl_my_class.abap``ZCL_MY_CLASS` |
| `interfaces/` | `interface` | `zif_my_interface.abap``ZIF_MY_INTERFACE` |
| `functions/` | `function` | `zgroup/z_my_func.abap``ZGROUP/Z_MY_FUNC` |
| `domains/` | `domain` | `z_status.abap``Z_STATUS` |
| `dataelements/` | `dataelement` | `z_status.abap``Z_STATUS` |
| `tables/` | `table` | `zmy_table.abap``ZMY_TABLE` |
| `tabletypes/` | `tabletype` | `zty_table.abap``ZTY_TABLE` |
> [!note] 文件名 → 对象名规则
> - 文件名去掉 `.abap` 后缀后转为大写即为对象名
> - `functions/` 下的子目录名作为函数组名,子目录内的文件名作为函数模块名
> - 对象名为 `函数组名大写/函数模块名大写` 格式
## 2. 清单文件 (`manifest.json`)
### 2.1 结构定义
```json
{
"version": 1,
"last_init": "2026-05-25T10:00:00",
"last_refresh": "2026-05-25T14:00:00",
"objects": {
"<对象名>": {
"type": "<对象类型>",
"file": "<相对文件路径>",
"system_status": "<状态>",
"corr_nr": "<传输请求号或null>",
"depends_on": ["<依赖对象名>"],
"last_sync": "<ISO时间戳或null>",
"last_sync_result": "<结果>"
}
}
}
```
### 2.2 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `version` | int | 清单格式版本号,当前为 `1` |
| `last_init` | string | 最近一次 `init` 的时间戳 |
| `last_refresh` | string | 最近一次 `refresh` 的时间戳 |
| `objects` | object | 以对象名为 key 的字典 |
| `objects.<name>.type` | string | 对象类型(report/class/interface/... |
| `objects.<name>.file` | string | 相对于项目根目录的文件路径 |
| `objects.<name>.system_status` | string | `active` / `inactive` / `not_exists` |
| `objects.<name>.corr_nr` | string\|null | 传输请求号,`null` 表示本地对象 `$TMP` |
| `objects.<name>.depends_on` | string[] | 依赖的对象名列表 |
| `objects.<name>.last_sync` | string\|null | 最近一次成功同步的时间 |
| `objects.<name>.last_sync_result` | string | `success` / `failed` / `pending` |
### 2.3 示例
```json
{
"version": 1,
"last_init": "2026-05-25T10:00:00",
"last_refresh": null,
"objects": {
"Z_STATUS": {
"type": "domain",
"file": "domains/z_status.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T10:30:00",
"last_sync_result": "success"
},
"ZIF_MY_INTERFACE": {
"type": "interface",
"file": "interfaces/zif_my_interface.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T10:30:00",
"last_sync_result": "success"
},
"ZCL_MY_CLASS": {
"type": "class",
"file": "classes/zcl_my_class.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": ["ZIF_MY_INTERFACE"],
"last_sync": "2026-05-25T11:00:00",
"last_sync_result": "success"
},
"ZMY_REPORT": {
"type": "report",
"file": "reports/zmy_report.abap",
"system_status": "inactive",
"corr_nr": null,
"depends_on": ["ZCL_MY_CLASS"],
"last_sync": null,
"last_sync_result": "pending"
},
"ZGROUP/Z_MY_FUNC": {
"type": "function",
"file": "functions/zgroup/z_my_func.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T10:00:00",
"last_sync_result": "success"
}
}
}
```
## 3. 命令变更一览
| 命令 | 变更类型 | 说明 |
|------|---------|------|
| `init` | **新增** | 扫描项目目录 → 查询 SAP → 生成 `manifest.json` |
| `create` | **变更** | 新增 `--path` 参数;创建成功后自动写入清单 |
| `sync` | **变更** | 新增 `--all` 批量模式;单对象模式操作后更新清单 |
| `delete` | **变更** | 新增 `--path` 参数;删除成功后从清单移除 |
| `refresh` | **新增** | 重新查询清单中所有对象的 SAP 状态 |
## 4. 命令详细设计
### 4.1 `init` — 项目初始化(新增)
#### 命令格式
```bash
python main.py init --path <项目根目录>
```
#### 执行流程
```
扫描项目目录
├─ 按目录映射表识别文件 → 生成 {对象名, 类型, 文件路径} 列表
├─ 输出: "扫描到 N 个本地文件"
└─ 忽略非 .abap 文件和隐藏文件
逐个查询 SAP 系统
├─ 对象存在 → 查询 info → 获取 system_status + corr_nr
├─ 对象不存在 → 标记 system_status: "not_exists"
└─ 查询失败 → 标记 system_status: "unknown"
应用类型默认优先级生成初始 depends_on
(init 时只设置类型层面的默认依赖,用户可后续手动编辑)
domain/dataelement/table → 无依赖
tabletype → 依赖同项目中的 table
interface → 无依赖
class → 依赖同项目中被引用的 interface
function → 无显式依赖
report → 无显式依赖
写入 manifest.json
输出汇总报告
```
#### 输出示例
```
============================================================
sap-cli 项目初始化
============================================================
项目路径: ./project
扫描到 5 个本地文件
→ 正在查询 SAP 系统状态...
✓ Z_STATUS (domain) active corr: DEVK901362
✓ ZIF_MY_INTERFACE (interface) active corr: DEVK901362
✓ ZCL_MY_CLASS (class) active corr: DEVK901362
⚠ ZMY_REPORT (report) inactive corr: 本地对象
✗ ZCL_NEW_CLASS (class) 不存在
✓ 清单已生成: ./project/manifest.json
5 个对象: 3 已激活, 1 未激活, 1 不存在
```
> [!tip] 清单可手动编辑
> `manifest.json` 生成后,用户可以手动编辑 `depends_on` 字段来补充对象间的显式依赖关系。
> `init` 只设置类型层面的默认依赖,具体的引用关系(如某个 class 实现了哪个 interface)需要用户自行标注。
---
### 4.2 `sync --all` — 批量同步(新增)
#### 命令格式
```bash
python main.py sync --all --path <项目根目录> [--corr_nr <传输请求号>] [--fail-fast] [--dry-run]
```
| 参数 | 必需 | 说明 |
|------|------|------|
| `--all` | 是 | 启用批量模式 |
| `--path` | 是 | 项目根目录(包含 `manifest.json` |
| `--corr_nr` | 否 | 统一传输请求号(跳过交互选择,所有对象共用) |
| `--fail-fast` | 否 | 遇到失败立即停止(默认:跳过并继续) |
| `--dry-run` | 否 | 仅输出执行计划,不实际操作 |
#### 执行流程
```
读取 manifest.json
├─ 不存在 → 报错: "请先执行 init"
└─ 存在 → 继续
扫描本地文件 vs 清单差异
├─ 本地有但清单没有的文件 → 提示: "N 个未纳入清单的文件,建议先 create"
└─ 清单有但本地被删的文件 → 警告,跳过
过滤待处理对象
├─ system_status == "active" && last_sync_result == "success" → 跳过(已同步)
├─ system_status == "not_exists" → 标记为 [create + sync]
└─ 其他 → 标记为 [sync]
拓扑排序
├─ 按 depends_on 建有向图
├─ 无显式依赖的按类型默认优先级排序
└─ 检测循环依赖 → 报错退出
--dry-run 模式:
输出执行计划,不实际操作
传输请求预选择(一次性交互):
├─ 扫描待处理对象,统计需要传输请求号的数量
├─ 指定了 --corr_nr → 直接使用,无交互
├─ 全部对象已绑定请求号或为本地对象 → 无交互
└─ 有对象需要请求号 → 弹出一次选择,选定后全局复用:
├─ 选择已有请求号 → 后续所有需要请求号的对象都使用此请求号
├─ 新建请求号 → 同上
└─ 不使用请求号 → 所需对象都作为本地对象处理
执行阶段(按排序顺序逐个处理,使用预选的传输请求号):
对每个对象:
├─ 本地文件不存在 → 跳过,警告
├─ system_status == "not_exists" → 先执行 create(使用预选请求号)
├─ 执行 sync 逻辑(使用预选请求号,跳过逐对象交互):
│ ├─ 对象已绑定请求号 → 使用对象自身绑定的请求号
│ ├─ 本地对象($TMP)→ 无需请求号
│ └─ 未绑定 → 使用预选的统一请求号(不再弹出交互)
├─ 成功 → 更新 manifest:
│ system_status: "active"
│ corr_nr: 实际使用的请求号
│ last_sync: 当前时间
│ last_sync_result: "success"
└─ 失败 →
├─ --fail-fast → 停止,输出已处理和未处理的对象
└─ 默认 → 标记 last_sync_result: "failed",跳过,继续下一个
后续依赖此对象的对象也自动跳过(标记 skipped)
输出汇总报告
```
#### dry-run 输出示例
```
============================================================
sap-cli 批量同步 (dry-run)
============================================================
项目路径: ./project
清单对象: 5
执行计划(按依赖排序):
1. Z_STATUS (domain) → sync [inactive → active]
2. ZIF_MY_INTERFACE (interface) → 跳过 [已是最新]
3. ZCL_MY_CLASS (class) → sync [依赖: ZIF_MY_INTERFACE ✓]
4. ZMY_REPORT (report) → sync [依赖: ZCL_MY_CLASS]
5. ZGROUP/Z_MY_FUNC (function) → 跳过 [已是最新]
待处理: 3 个 | 跳过: 2 个
```
#### 执行输出示例
```
============================================================
sap-cli 批量同步
============================================================
项目路径: ./project
待处理: 3 个对象
→ 预检传输请求需求...
2 个对象已绑定请求号: Z_STATUS(DEVK901362), ZCL_MY_CLASS(DEVK901362)
1 个对象需要传输请求: ZMY_REPORT
→ 查询可用的传输请求...
找到 2 个可修改的传输请求:
1. DEVK901362 Implement sales validation (所有者: ADMIN2)
2. DEVK901365 Bugfix for invoice printing (所有者: ADMIN2)
3. 新建传输请求
0. 不使用传输请求(本地对象)
请选择 [0-3]: 1
✓ 统一传输请求: DEVK901362(后续所有对象共用此请求号)
── [1/3] Z_STATUS (domain) ──
✓ 锁定成功(对象已绑定传输请求: DEVK901362
✓ 源代码写入成功
✓ 解锁成功
✓ 语法检查通过
✓ 激活成功
── [2/3] ZCL_MY_CLASS (class) ──
✓ 锁定成功(对象已绑定传输请求: DEVK901362
✓ 源代码写入成功
✓ 解锁成功
✓ 语法检查通过
✓ 激活成功
── [3/3] ZMY_REPORT (report) ──
✓ 锁定成功(使用统一传输请求: DEVK901362
✓ 源代码写入成功
✓ 解锁成功
✗ 语法检查未通过! 1 个错误
[错误] 行 10: Field "XXX" is unknown
⊘ 跳过(标记为 failed
============================================================
批量同步完成
============================================================
✓ 成功: Z_STATUS, ZCL_MY_CLASS
✗ 失败: ZMY_REPORT (语法错误,行 10)
⊘ 跳过: (无)
清单已更新: ./project/manifest.json
```
> [!tip] 指定 `--corr_nr` 跳过交互
> 如果已知传输请求号,使用 `--corr_nr` 参数可以直接跳过交互选择:
> ```bash
> python main.py sync --all --path ./project --corr_nr DEVK901362
> ```
> 此时所有未绑定请求号的对象都会使用指定的请求号,**全程零交互**。
---
### 4.3 `refresh` — 刷新清单状态(新增)
#### 命令格式
```bash
python main.py refresh --path <项目根目录>
```
#### 执行流程
```
读取 manifest.json
逐个重新查询 SAP info
更新每个对象的 system_status 和 corr_nr
(不修改 last_sync 和 last_sync_result
输出变更摘要
```
#### 使用场景
- 其他人修改了 SAP 端的对象
- 传输请求状态发生变化(释放、导入等)
- 需要确认当前系统状态但不想执行同步
#### 输出示例
```
============================================================
sap-cli 清单刷新
============================================================
项目路径: ./project
清单对象: 5
→ 正在查询 SAP 系统状态...
✓ Z_STATUS (domain) active corr: DEVK901362 (未变化)
✓ ZIF_MY_INTERFACE (interface) active corr: DEVK901362 (未变化)
~ ZCL_MY_CLASS (class) inactive corr: DEVK901362 (状态变更: active → inactive)
✓ ZMY_REPORT (report) inactive corr: null (未变化)
✓ ZGROUP/Z_MY_FUNC (function) active corr: DEVK901362 (未变化)
✓ 刷新完成
变更: 1 个 | 未变化: 4 个
```
---
### 4.4 `create` — 变更:自动写入清单
#### 变更内容
- 新增 `--path` 参数:指向项目根目录
- 创建成功后:自动在 `manifest.json` 中新增一条记录
#### 新增的清单记录
```json
{
"ZCL_NEW_CLASS": {
"type": "class",
"file": "classes/zcl_new_class.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T14:00:00",
"last_sync_result": "success"
}
}
```
#### 向后兼容
- 如果未指定 `--path`,或指定目录下没有 `manifest.json`:仅创建对象,不写清单
- 现有不含 `--path` 的用法不受影响
---
### 4.5 `delete` — 变更:从清单移除
#### 变更内容
- 新增 `--path` 参数:指向项目根目录
- 删除成功后:从 `manifest.json` 中移除该对象记录
#### 向后兼容
- 如果未指定 `--path`,或指定目录下没有 `manifest.json`:仅删除对象,不更新清单
---
### 4.6 `sync`(单对象) — 变更:操作后更新清单
#### 变更内容
- 新增 `--path` 参数:指向项目根目录(可选)
- sync 成功或失败后:更新 `manifest.json` 中对应对象的记录
#### 向后兼容
- 如果未指定 `--path`,或指定目录下没有 `manifest.json`:行为与现有完全一致
## 5. 依赖排序策略
### 5.1 类型默认优先级
无显式 `depends_on` 时,按类型优先级排序:
| 优先级 | 数值 | 类型 | 说明 |
|--------|------|------|------|
| 1 | 10 | `domain` | 基础域定义 |
| 2 | 20 | `dataelement` | 依赖 domain |
| 3 | 30 | `table` | 依赖 dataelement |
| 4 | 40 | `tabletype` | 依赖 table |
| 5 | 50 | `interface` | 接口定义 |
| 6 | 60 | `class` | 可能实现 interface |
| 7 | 70 | `function` | 函数模块 |
| 8 | 80 | `report` | 程序,可能引用以上所有 |
### 5.2 排序算法
采用拓扑排序(Kahn 算法):
```
1. 以 manifest 中所有对象为节点
2. 建立有向边:
- 显式依赖: depends_on 中列出的对象 → 当前对象
- 类型默认优先级: 仅当两个对象之间没有显式依赖时作为 tie-breaker
3. 执行 Kahn 拓扑排序
4. 检测循环依赖 → 若存在未入队的节点则报错,输出循环链
```
### 5.3 依赖失败传播
批量 `sync` 时,若对象 A 依赖对象 B,而 B 同步失败:
```
A.depends_on = ["B"]
B.last_sync_result = "failed"
→ A 自动跳过,标记 last_sync_result: "skipped"
→ 依赖 A 的对象同样跳过(级联跳过)
```
## 6. 边界情况处理
| 场景 | 处理方式 |
|------|---------|
| 本地新增了 .abap 文件但清单中没有 | `sync --all` 时提示: "N 个未纳入清单的文件",建议先执行 `create` |
| 清单中有对象但本地文件被删了 | `sync --all` 时警告并跳过该对象 |
| `manifest.json` 不存在 | `sync --all` 报错,提示先执行 `init` |
| 依赖对象 sync 失败 | 跳过所有依赖它的对象(级联标记 skipped) |
| 循环依赖 | 检测到后报错,输出循环链中的对象名 |
| 非项目目录(无子目录结构) | `init` 时只扫描当前目录下的 `.abap` 文件 |
| SAP 连接中断 | 标记已处理的对象,输出中断位置,下次可从断点继续 |
| `manifest.json` 版本不匹配 | 提示版本不兼容,建议重新 `init` |
| 部分对象已绑定请求号,部分未绑定 | 已绑定的使用自身请求号,未绑定的使用预选的统一请求号 |
| 全部对象都是本地对象($TMP) | 无需传输请求,直接执行,零交互 |
| 指定了 `--corr_nr` 但部分对象已绑定其他请求号 | 已绑定的使用自身请求号(不受 `--corr_nr` 覆盖) |
## 7. 项目生命周期
```
┌─────────────────────────────────────────────────────────┐
│ │
│ init ────────────────────────────────────────────── │
│ │ 扫描本地文件 → 查询 SAP → 生成 manifest.json │
│ │ │
│ ▼ │
│ create(新增对象时) ──────────────────────────────── │
│ │ 在 SAP 创建 → 自动写入 manifest │
│ │ │
│ ▼ │
│ sync --all(日常同步)───────────────────────────── │
│ │ 读 manifest → 拓扑排序 → 逐个 sync → 更新 manifest │
│ │ │
│ ▼ │
│ refresh(SAP 端有变化时)──────────────────────────── │
│ 重新查询 SAP → 更新 manifest 中的状态字段 │
│ │
└─────────────────────────────────────────────────────────┘
```
> [!tip] 典型工作流
> ```bash
> # 1. 首次使用:初始化项目清单
> python main.py init --path ./my_project
>
> # 2. 日常开发:新增对象
> python main.py create --name ZCL_NEW --type class --path ./my_project --description "新类"
>
> # 3. 日常开发:修改本地 .abap 文件后,批量同步
> python main.py sync --all --path ./my_project
>
> # 4. 如需查看执行计划(不实际操作)
> python main.py sync --all --path ./my_project --dry-run
>
> # 5. 其他人修改了 SAP 端对象后,刷新清单
> python main.py refresh --path ./my_project
> ```
## 8. 模块设计(实现层面)
### 8.1 新增模块
| 模块 | 职责 |
|------|------|
| `sapcli/manifest.py` | 清单文件的读取、写入、查询、更新 |
| `sapcli/scanner.py` | 扫描本地目录,识别对象名和类型 |
| `sapcli/sorter.py` | 拓扑排序,依赖检测 |
### 8.2 清单模块 (`manifest.py`) 接口
```python
@dataclass
class ManifestEntry:
name: str
type: str
file: str
system_status: str # "active" / "inactive" / "not_exists"
corr_nr: str | None
depends_on: list[str]
last_sync: str | None # ISO timestamp
last_sync_result: str # "success" / "failed" / "pending"
class Manifest:
version: int
last_init: str | None
last_refresh: str | None
objects: dict[str, ManifestEntry]
@classmethod
def load(cls, project_path: str) -> "Manifest": ...
def save(self) -> None: ...
def upsert(self, entry: ManifestEntry) -> None: ...
def remove(self, name: str) -> None: ...
def get(self, name: str) -> ManifestEntry | None: ...
def pending_objects(self) -> list[ManifestEntry]: ...
```
### 8.3 扫描模块 (`scanner.py`) 接口
```python
DIRECTORY_TYPE_MAP = {
"reports": "report",
"classes": "class",
"interfaces": "interface",
"functions": "function",
"domains": "domain",
"dataelements": "dataelement",
"tables": "table",
"tabletypes": "tabletype",
}
@dataclass
class ScannedObject:
name: str # 大写对象名,function 类型含 /
type: str # 对象类型
file: str # 相对于项目根目录的路径
def scan_project(project_path: str) -> list[ScannedObject]: ...
```
### 8.4 排序模块 (`sorter.py`) 接口
```python
TYPE_PRIORITY = {
"domain": 10,
"dataelement": 20,
"table": 30,
"tabletype": 40,
"interface": 50,
"class": 60,
"function": 70,
"report": 80,
}
class CyclicDependencyError(SapCliError): ...
def topological_sort(
objects: list[ManifestEntry],
) -> list[ManifestEntry]: ...
# → 按 depends_on + TYPE_PRIORITY 排序
# → 检测循环依赖,抛出 CyclicDependencyError
```
## 9. 相关笔记
- [[sap-cli/doc/sap-cli使用指南|sap-cli 使用指南]] — 现有命令的使用说明
- [[sap-cli/doc/技术说明|技术说明]] — 技术架构与模块设计
- [[sap-cli/doc/ADT/03.修改代码-原理|修改代码原理]] — 锁定/编辑/激活工作流
+530
View File
@@ -0,0 +1,530 @@
# sap-cli 功能扩展分析报告
> **项目**: sap-cli — 用你喜欢的编辑器写 ABAP
> **版本**: 当前 (v1.0)
> **编制日期**: 2026-06-09
> **定位**: 基于 SAP ADT REST API 的命令行 ABAP 开发工具
---
## 一、现有功能全景
### 1.1 已实现的 7 个命令
| 命令 | 功能 | 适用对象 |
|:-----|:-----|:---------|
| `download` | 从 SAP 下载源码到本地 .abap 文件 | report / class / interface / function / domain / dataelement / table / structure |
| `sync` | 本地代码 → SAP(锁定→写入→解锁→语法检查→激活) | 同上 |
| `create` | 在 SAP 创建开发对象(支持模板和 JSON 定义) | 10 种类型(含 functiongroup、tabletype |
| `info` | 查询对象元数据(名称/类型/状态/负责人/修改时间等) | 10 种类型 |
| `delete` | 从 SAP 系统删除对象(需确认) | 10 种类型 |
| `init` | 扫描本地目录 → 查询 SAP → 生成 manifest.json | 批量 |
| `refresh` | 刷新清单中对象的 SAP 状态 | 批量 |
### 1.2 已支持的对象类型
**ABAP 程序对象(5 种)**report、class、interface、function、functiongroup
**ABAP 字典对象(5 种)**domain、dataelement、table、structure、tabletype
### 1.3 核心能力
- ✅ 单对象 CRUD 全生命周期
- ✅ 批量同步(基于 manifest.json + 拓扑排序)
- ✅ 传输请求管理(list / create / select
- ✅ DDIC 对象通过 JSON 定义文件创建
- ✅ 锁定-编辑-解锁-激活完整工作流
- ✅ 语法检查集成
---
## 二、功能扩展方向
### 🔵 方向一:对象类型扩展
#### 2.1.1 ADT 已支持但工具未覆盖的对象类型
| 优先级 | 对象类型 | ADT 端点 | 使用场景 | 实现难度 |
|:------:|:---------|:---------|:---------|:--------:|
| ⭐⭐⭐ | **CDS View (DDL)** | `/sap/bc/adt/ddic/ddlsources` | S/4HANA 核心开发,CDS View 是现代 ABAP 开发的基础 | 中 |
| ⭐⭐⭐ | **CDS Access Control (DCL)** | `/sap/bc/adt/authorization/dclsources` | CDS 角色权限控制,与 CDS View 配套 | 中 |
| ⭐⭐⭐ | **Include 程序** | `/sap/bc/adt/programs/programs` | 大型报表拆分,复用代码片段 | 低 |
| ⭐⭐ | **消息类 (Message Class)** | `/sap/bc/adt/messageclasses` | MESSAGE 语句依赖,项目必备 | 低 |
| ⭐⭐ | **数据库视图 (View)** | `/sap/bc/adt/ddic/views` | 数据建模,查询优化 | 中 |
| ⭐⭐ | **搜索帮助 (Search Help)** | `/sap/bc/adt/ddic/searchhelps` | ALV 屏幕、F4 帮助 | 中 |
| ⭐⭐ | **锁对象 (Lock Object)** | `/sap/bc/adt/ddic/lockobjects` | 并发控制,多用户数据一致性 | 中 |
| ⭐ | **类型组 (Type Pool)** | `/sap/bc/adt/programs/programs` | 常量/类型定义集中管理 | 低 |
| ⭐ | **AMDP 类** | 复用 class 端点 | HANA 数据库过程调用 | 低 |
| ⭐ | **Web Dynpro 组件** | 专用端点 | 传统 Web UI(逐渐淘汰) | 高 |
**实现建议**
- 第一批优先:**Include 程序**(改动最小,复用现有 report 逻辑)+ **CDS View**(战略价值最高)
- CDS View 需要新增 `cdsview` 类型,源码是 DDL 语法(非 ABAP),激活流程与 DDIC 类似
#### 2.1.2 tabletype 的完整支持
当前 `tabletype` 使用 VIT 端点,不支持 download/sync/delete。可以探索:
- 通过 DDIC 专用端点(`/sap/bc/adt/ddic/tabletypes`)的源码读写接口
- 或使用 VIT 的 PUT 操作实现间接源码写入
---
### 🔵 方向二:代码浏览与搜索
#### 2.2.1 对象列表浏览(`list` 命令)
```
python main.py list --type class --prefix ZCL_* --package ZFINANCE
```
**ADT API**: `GET /sap/bc/adt/repository/informationsystem/search`
**使用场景**:
- 浏览某个包下的所有对象
- 按名称前缀模糊搜索
- 按类型/所有者/修改时间筛选
#### 2.2.2 Where-Used 引用查询
```
python main.py whereused --name ZCL_MY_CLASS --type class
```
**ADT API**: `GET /sap/bc/adt/whereused`
**使用场景**:
- 修改前评估影响范围
- 自动填充 `manifest.json``depends_on` 字段(替代手动维护)
- 批量同步时自动推导依赖关系
#### 2.2.3 源代码全文搜索
```
python main.py search --query "SELECT * FROM ZMY_TABLE" --type report
```
**ADT API**: `GET /sap/bc/adt/repository/informationsystem/search` (code search)
**使用场景**: 快速定位引用特定表/函数/变量的代码位置
---
### 🔵 方向三:代码质量与检查
#### 2.3.1 ATC 代码检查集成
```
python main.py check --name ZMY_REPORT --type report --variant SAP_ABA_CHECK
```
**ADT API**: `POST /sap/bc/adt/qualitymanager/ats/checkruns`
**使用场景**:
- 同步前自动运行 ATC 检查(替代或补充现有语法检查)
- CI/CD 流水线中的质量门禁
- 支持自定义检查变体
#### 2.3.2 代码格式化(ABAP Pretty Printer
```
python main.py format --name ZMY_REPORT --type report
```
**ADT API**: `POST /sap/bc/adt/prettyprinter`
**使用场景**:
- 下载后自动格式化(统一缩进、大小写风格)
- sync 前自动格式化(保持 SAP 端代码风格一致)
#### 2.3.3 代码差异对比
```
python main.py diff --name ZMY_REPORT --type report --path ./src/zmy_report.abap
```
**使用场景**:
- sync 前预览即将上传的改动
- 显示本地文件与 SAP 端源码的差异
- 防止误覆盖他人的修改
---
### 🔵 方向四:传输管理与多系统
#### 2.4.1 传输请求高级管理
```
python main.py transport list --status D # 查看可修改的传输请求
python main.py transport info DEVK901362 # 查看传输请求详情
python main.py transport release DEVK901362 # 释放传输请求
python main.py transport objects DEVK901362 # 列出传输请求中的所有对象
```
**ADT API**:
- `GET /sap/bc/adt/cts/transportrequests/{id}` — 详情
- `POST /sap/bc/adt/cts/transportrequests/{id}?method=release` — 释放
**使用场景**:
- 批量同步后一键释放传输请求
- 在命令行中管理传输请求,无需进入 SAP GUISE01/SE09
#### 2.4.2 多系统配置与跨系统同步
```ini
# config.ini
[DEV]
host = http://dev-sap:8000
client = 100
[QAS]
host = http://qas-sap:8000
client = 200
[PRD]
host = http://prd-sap:8000
client = 300
```
```
python main.py --profile DEV download --name ZMY_REPORT --type report --path ./src
python main.py --profile QAS info --name ZMY_REPORT --type report
```
**使用场景**:
- 开发/测试/生产多环境切换
- 跨系统对象对比(DEV vs QAS 代码差异)
- 一键从 DEV 下载 → 修改 → 推送到 QAS
#### 2.4.3 传输导入监控
```
python main.py transport import DEVK901362 --target QAS
python main.py transport status DEVK901362
```
**使用场景**: 跟踪传输请求在目标系统的导入状态
---
### 🔵 方向五:包管理与项目结构
#### 2.5.1 ABAP 包(Package)操作
```
python main.py package create ZFINANCE --description "财务模块" --superpackage ZBUSINESS
python main.py package list --superpackage ZBUSINESS
python main.py package move --name ZMY_REPORT --type report --package ZFINANCE
```
**ADT API**: `/sap/bc/adt/packages/{name}`
**使用场景**:
- 项目初始化时自动创建包结构
- 将 $TMP 本地对象迁移到正式包中
#### 2.5.2 项目模板(Scaffolding
```
python main.py scaffold --name ZSALES_ORDER --template "ALV Report" --package ZSALES
```
预置模板:
- **ALV 报表** — 包含 ALV GRID、字段目录、布局管理
- **BAPI 封装** — 函数组 + 函数模块 + 异常处理
- **接口类** — 接口 + 实现类 + 工厂方法
- **数据模型** — domain + dataelement + table + tabletype + CDS View
- **增强实现** — BAdI 定义 + 实现(如果 ADT 支持)
**使用场景**: 快速创建符合项目规范的标准代码骨架
#### 2.5.3 依赖自动分析
```
python main.py analyze --path ./project
```
**功能**:
- 解析本地 .abap 文件中的 `TYPE REF TO``CALL METHOD``PERFORM` 等语句
- 自动生成 `depends_on` 关系
- 替代当前手动维护 manifest.json 依赖的方式
- 结合 Where-Used API 交叉验证
---
### 🔵 方向六:Git 集成与 DevOps
#### 2.6.1 Git Hooks 集成
```bash
# pre-commit hook: sync 前自动语法检查
# post-pull hook: 自动 download 远端最新代码
```
**使用场景**:
- `git commit` 前自动触发 ATC 检查
- `git pull` 后自动拉取 SAP 端最新代码
- `git push` 后自动触发 CI/CD
#### 2.6.2 CI/CD Pipeline 集成
```yaml
# .gitlab-ci.yml 示例
abap-sync:
stage: deploy
script:
- python main.py sync --all --path ./src --corr_nr $TR_NUMBER --fail-fast
- python main.py check --all --path ./src
```
**使用场景**:
- Git 提交后自动同步到 SAP 开发系统
- ATC 检查作为质量门禁
- 传输请求自动创建和释放
#### 2.6.3 代码版本对比(SAP 版本管理集成)
```
python main.py history --name ZMY_REPORT --type report
python main.py version --name ZMY_REPORT --type report --version 1.2
```
**ADT API**: `/sap/bc/adt/programs/programs/{name}/versions`
**使用场景**:
- 查看 SAP 端的版本历史
- 对比不同版本之间的代码差异
- 回滚到之前的版本
---
### 🔵 方向七:交互体验优化
#### 2.7.1 交互式终端 UITUI
```
python main.py tui
```
**功能**:
- 基于 [Textual](https://github.com/Textualize/textual) 或 [Rich](https://github.com/Textualize/rich) 的终端 UI
- 对象树状浏览器(按类型/包/状态分组)
- 源码差异对比视图
- 批量操作进度条
#### 2.7.2 全局配置与 Profile
```bash
sap-cli config set default_host http://dev-sap:8000
sap-cli config set default_client 100
sap-cli config set auto_format true
sap-cli config set atc_variant SAP_ABA_CHECK
sap-cli config set timeout 30
```
**使用场景**:
- 替代手动编辑 config.ini
- 按项目/环境保存不同配置
- 设置全局默认参数(超时、格式化、检查变体等)
#### 2.7.3 Shell 自动补全
```bash
eval "$(sap-cli completion bash)"
eval "$(sap-cli completion zsh)"
```
**使用场景**: Tab 键自动补全命令、对象类型、对象名
---
### 🔵 方向八:AI 与智能化
#### 2.8.1 AI 辅助代码生成
```
python main.py generate --type report --description "根据销售订单号查询交货明细的ALV报表" --output ./src/zdelivery_detail.abap
```
**使用场景**:
- 结合 LLM API(如 Claude/GPT)根据自然语言描述生成 ABAP 代码
- 自动识别所需的表、数据元素、函数模块
- 生成的代码自动通过语法检查
#### 2.8.2 MCP 服务器模式
```
python main.py mcp-server --port 8080
```
**参考**: [erpl-adt](https://github.com/DataZooDE/erpl-adt) 已实现 MCP 服务器
**使用场景**:
- 作为 MCP 工具供 AI 助手(如 Claude Desktop)调用
- AI 可以直接浏览 SAP 对象、下载/同步代码
- 实现"用自然语言修改 SAP 系统"的终极目标
#### 2.8.3 智能代码审查
```
python main.py review --name ZMY_REPORT --type report --rules performance,security
```
**使用场景**:
- 基于规则的静态分析(SQL 注入、性能反模式、命名规范)
- 结合 AI 的代码审查建议
- 自动生成改进建议
---
### 🔵 方向九:安全与运维
#### 2.9.1 密码安全存储
```
python main.py auth login # 交互式输入密码,存入系统 keyring
python main.py auth status # 检查登录状态
python main.py auth logout # 清除凭证
```
**实现方案**:
- 使用 [keyring](https://github.com/jaraco/keyring) 库
- 支持 Windows Credential Manager / macOS Keychain / Linux Secret Service
- 不再明文存储密码
#### 2.9.2 SSL 证书验证
```ini
[DEV]
host = https://dev-sap:44300
verify_ssl = true
ca_bundle = /path/to/corp-ca.pem
```
**使用场景**:
- 生产环境安全要求
- 企业内网 CA 证书支持
- 当前硬编码 `verify=False`,存在中间人攻击风险
#### 2.9.3 审计日志
```
python main.py audit --from 2026-06-01 --to 2026-06-09
```
**使用场景**:
- 记录所有 create/sync/delete 操作的审计日志
- 支持按时间/用户/操作类型查询
- 满足企业合规要求
---
## 三、扩展优先级矩阵
按照 **业务价值 × 实现难度** 排序:
| 优先级 | 扩展方向 | 业务价值 | 实现难度 | 建议版本 |
|:------:|:---------|:--------:|:--------:|:--------:|
| 🔴 P0 | Include 程序支持 | ⭐⭐⭐ | ⭐ | v1.1 |
| 🔴 P0 | 对象列表浏览 (list) | ⭐⭐⭐ | ⭐⭐ | v1.1 |
| 🔴 P0 | 多系统配置 (profile) | ⭐⭐⭐ | ⭐⭐ | v1.2 |
| 🔴 P0 | 密码安全存储 (keyring) | ⭐⭐⭐ | ⭐ | v1.2 |
| 🟠 P1 | CDS View 支持 | ⭐⭐⭐ | ⭐⭐⭐ | v1.3 |
| 🟠 P1 | Where-Used 引用查询 | ⭐⭐⭐ | ⭐⭐ | v1.3 |
| 🟠 P1 | 代码差异对比 (diff) | ⭐⭐⭐ | ⭐⭐ | v1.3 |
| 🟠 P1 | 传输请求高级管理 | ⭐⭐⭐ | ⭐⭐ | v1.4 |
| 🟠 P1 | ABAP 包操作 | ⭐⭐ | ⭐⭐ | v1.4 |
| 🟡 P2 | ATC 代码检查集成 | ⭐⭐ | ⭐⭐⭐ | v2.0 |
| 🟡 P2 | 代码格式化 (Pretty Printer) | ⭐⭐ | ⭐⭐ | v2.0 |
| 🟡 P2 | 依赖自动分析 | ⭐⭐⭐ | ⭐⭐⭐ | v2.0 |
| 🟡 P2 | 项目模板 (Scaffolding) | ⭐⭐ | ⭐⭐ | v2.0 |
| 🟡 P2 | CI/CD Pipeline 集成 | ⭐⭐⭐ | ⭐⭐ | v2.1 |
| 🟢 P3 | 消息类 / 视图 / 搜索帮助 / 锁对象 | ⭐⭐ | ⭐⭐ | v2.x |
| 🟢 P3 | 源代码全文搜索 | ⭐⭐ | ⭐⭐ | v2.x |
| 🟢 P3 | SAP 版本管理集成 | ⭐⭐ | ⭐⭐⭐ | v2.x |
| 🟢 P3 | Shell 自动补全 | ⭐ | ⭐ | v2.x |
| 🔵 P4 | 交互式终端 UI (TUI) | ⭐⭐ | ⭐⭐⭐ | v3.0 |
| 🔵 P4 | AI 辅助代码生成 | ⭐⭐⭐ | ⭐⭐⭐ | v3.0 |
| 🔵 P4 | MCP 服务器模式 | ⭐⭐⭐ | ⭐⭐ | v3.0 |
| 🔵 P4 | 智能代码审查 | ⭐⭐ | ⭐⭐⭐ | v3.0 |
---
## 四、技术架构建议
### 4.1 近期架构优化(支持 v1.x 扩展)
```
sapcli/
├── __init__.py
├── main.py # CLI 入口(拆分 argparse 到独立模块)
├── cli/ # 命令注册与解析(新增)
│ ├── parser.py # argparse 定义
│ ├── completions.py # Shell 补全
│ └── output.py # 格式化输出(替代散落各处的 print)
├── client.py # ADT REST 客户端(保持)
├── commands/ # 按功能拆分命令(新增目录)
│ ├── crud.py # download / sync / create / delete / info
│ ├── batch.py # init / refresh / sync-all
│ ├── transport.py # 传输请求管理(新增)
│ ├── search.py # list / whereused / search(新增)
│ └── check.py # syntax-check / atc(新增)
├── types.py # 对象类型注册(保持)
├── ddic.py # DDIC 定义(保持)
├── manifest.py # 清单管理(保持)
├── scanner.py # 目录扫描(保持)
├── sorter.py # 拓扑排序(保持)
├── config.py # 配置管理(增强多 profile)
├── auth.py # 凭证管理(新增:keyring 集成)
├── exceptions.py # 异常定义(保持)
└── utils/ # 工具函数(新增)
├── xml_utils.py # XML 安全转义、解析
├── diff.py # 代码差异对比
└── format.py # ABAP 格式化
```
### 4.2 新增对象类型的标准流程
每次新增一个对象类型,需要修改的文件:
| 步骤 | 文件 | 内容 |
|:-----|:-----|:-----|
| 1 | `types.py` | 注册 `ObjectTypeConfig`URI 模板、content-type |
| 2 | `client.py` | `_build_create_body()` 新增分支(或数据驱动) |
| 3 | `scanner.py` | `DIRECTORY_TYPE_MAP` 新增目录映射 |
| 4 | `commands.py` | 特殊逻辑处理(如有) |
| 5 | `ddic.py` | 如有 XML/DDL 定义需求 |
### 4.3 建议引入的依赖
| 依赖 | 用途 | 时机 |
|:-----|:-----|:-----|
| `keyring` | 安全凭证存储 | v1.2 |
| `rich` | 终端美化输出(进度条、表格、语法高亮) | v1.3 |
| `textual` | 终端 UI 框架 | v3.0(可选) |
| `pydantic` | JSON 定义文件验证 | v2.0 |
| `httpx` | 替代 requests(支持 async、超时、重试) | v2.0 |
---
## 五、竞品参考
| 项目 | 语言 | 特点 | 值得借鉴 |
|:-----|:-----|:-----|:---------|
| [abap-adt-api](https://github.com/marcellourbani/abap-adt-api) | TypeScript | 最全面的 ADT API 封装 | CDS View、AMDP、Where-Used、搜索 |
| [erpl-adt](https://github.com/DataZooDE/erpl-adt) | TypeScript | CLI + MCP 服务器 | MCP 协议、对象浏览、AI 集成 |
| [abapGit](https://github.com/abapGit/abapGit) | ABAP | SAP 端 Git 客户端 | 版本管理思想、serialize/deserialize |
---
## 六、总结
sap-cli 已经建立了一个坚实的核心:
- **10 种对象类型**的完整 CRUD
- **批量同步** + 拓扑排序 + 传输请求管理
- **manifest.json** 状态驱动的项目管理
最自然、最高价值的扩展路径是:
```
v1.1 Include + list + 消息类 → 补齐日常开发必需类型
v1.2 多系统 + keyring → 企业级安全与多环境
v1.3 CDS View + Where-Used → S/4HANA 现代化开发
v1.4 传输管理 + 包操作 → 运维与管理能力
v2.0 ATC + diff + 依赖分析 → 代码质量保障
v2.x CI/CD + 模板 + 搜索 → DevOps 集成
v3.0 TUI + AI + MCP → 智能化开发
```
这个路径从 **"能用的工具"** 逐步演进到 **"不可替代的开发平台"**,每一步都有明确的用户价值。
+103
View File
@@ -0,0 +1,103 @@
# sap-cli 测试覆盖率报告
> 测试时间:2026-06-09
> 测试用例:6863 pass + 5 skip
> 覆盖率工具:coverage.py
---
## 总体概况
| 指标 | 数值 |
|------|------|
| 源码总行数(Stmts | 3,018 |
| 已覆盖行数 | 674 |
| **总体覆盖率** | **22%** |
---
## 按模块覆盖率(从低到高)
### 🔴 严重不足(< 20%)— 占总代码量 71%
| 模块 | 代码行 | 覆盖率 | 未覆盖行 | 说明 |
|------|--------|--------|---------|------|
| `cli/parser.py` | 114 | **4%** | 110 | CLI 参数定义,仅被 import 触发 |
| `commands/crud.py` | 560 | **5%** | 533 | **最大模块**5 个核心命令零覆盖 |
| `commands/batch.py` | 230 | **7%** | 215 | 批量操作 3 个命令零覆盖 |
| `client.py` | 716 | **7%** | 665 | **第二大的**30 个 API 方法零覆盖 |
| `commands/cds.py` | 126 | **10%** | 114 | CDS 命令零覆盖 |
| `commands/quality.py` | 94 | **10%** | 85 | check + format 零覆盖 |
| `commands/search.py` | 100 | **10%** | 90 | list/whereused/search 零覆盖 |
| `commands/transport.py` | 100 | **10%** | 90 | 传输管理零覆盖 |
| `commands/config_cmd.py` | 75 | **11%** | 67 | config 命令零覆盖 |
| `commands/package_cmd.py` | 78 | **12%** | 69 | package 命令零覆盖 |
| `commands/diff_cmd.py` | 59 | **17%** | 49 | diff 命令零覆盖 |
| `auth.py` | 99 | **31%** | 68 | keyring 方法被测,cmd_auth 未测 |
**小计:2,451 行未覆盖(占总量 81%)**
### 🟡 部分覆盖(20%79%
| 模块 | 代码行 | 覆盖率 | 说明 |
|------|--------|--------|------|
| `commands/analyze.py` | 62 | **34%** | 解析逻辑被测,命令入口未测 |
| `commands/scaffold.py` | 64 | **34%** | 4 个模板生成被测,命令入口未测 |
| `cli/output.py` | 25 | **60%** | print 函数被部分调用 |
| `scanner.py` | 52 | **62%** | 基础扫描被测,函数目录未测 |
| `manifest.py` | 82 | **70%** | CRUD 被测,文件 I/O 边界未测 |
| `config.py` | 62 | **77%** | 环境变量被测,profile/load 未测 |
### 🟢 良好(≥ 80%
| 模块 | 代码行 | 覆盖率 | 说明 |
|------|--------|--------|------|
| `exceptions.py` | 35 | **83%** | 核心异常被测 |
| `ddic.py` | 128 | **84%** | XML/DDL 构建被测 |
| `types.py` | 69 | **90%** | 注册+解析被测,新注册 6 种未测 |
| `sorter.py` | 68 | **96%** | 几乎全覆盖 |
| `xml_utils.py` | 5 | **100%** | 全覆盖 |
| `__init__.py` 系列 | 15 | **100%** | 全覆盖 |
---
## 三层覆盖率分析
```
项目架构 代码行 覆盖率 未覆盖
──────────────────────────────────────────────
CLI 层 parser+app 139 5% 132
命令层 commands/* 1,548 8% 1,424
API 层 client.py 716 7% 665
基础层 其他模块 615 74% 159
──────────────────────────────────────────────
合计 3,018 22% 2,344
```
### 关键发现
1. **CLI 层 + 命令层 + API 层 = 2,403 行,覆盖率仅 7%**
— 这三层占总代码量的 80%,但几乎没有测试
— 原因:现有 68 用例全部测的是基础层(types/config/ddic/manifest/sorter
2. **基础层覆盖率 74%** — 已经不错
— types.py 的 6 种新注册类型需要补充测试
— config.py 的 `load_config(profile=...)` 需要补充测试
3. **单文件代码量 Top 3**
- `client.py` (716行) — 0 测试 ← 最大风险点
- `commands/crud.py` (560行) — 0 测试
- `commands/batch.py` (230行) — 0 测试
---
## 提升路径
| 目标覆盖率 | 需要新增测试 | 预计工时 |
|-----------|-------------|---------|
| 50% | client.py + commands 核心方法 (~130 用例) | 6h |
| 70% | 所有命令 + cli 入口 (~90 用例) | 4h |
| 80% | 边界条件 + 错误路径 (~50 用例) | 3h |
| 90% | 极端场景 + 集成测试 (~40 用例) | 3h |
> HTML 详细报告已生成:`tests/coverage_html/index.html`
+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`
+241
View File
@@ -0,0 +1,241 @@
# ADT 工具集测试报告
> 测试目标:验证 `main.py` 的 create / info / download / sync / delete 五大功能在各种场景下的正确性。
>
> 测试日期:2026-05-20 | 测试脚本:[test_main.py](file:///d:/codeSpace/Gitee/Projects2026/sap-cli/test_main.py)
## 前置条件
- [ ] SAP 系统可连接(配置文件 `config.ini` 正确)
- [ ] Python 3.10+ 和 `requests` 库已安装
- [ ] 系统中存在至少一个 Modifiable 状态的传输请求
- [ ] 系统中存在以下已有对象用于查询测试:
- class: `ZCL_SAP_MCP_HANDLER`
- report: `ZIDTR_IMPORT_BOM`
- domain: `ZSAPILOT_OBJECT_TYPE`
- dataelement: `ZSAPILOT_OBJECT_NAME`
- table: `ZSAPILOT_OBJ`
---
## 测试覆盖矩阵
> 操作(行) × 对象类型(列) 的交叉覆盖情况。
> ✅ = 已测试通过 | ⬜ = 不适用 | ➖ = 未覆盖(系统限制)
| 操作 \ 对象类型 | report | class | function | functiongroup | interface | domain | dataelement | table | tabletype |
|:---------------|:------:|:-----:|:--------:|:-------------:|:---------:|:------:|:-----------:|:-----:|:---------:|
| **create** | ✅ SC1 | ✅ SC2 | ✅ SC3 | ✅ SC3 | ✅ SC4 | 🔒 | 🔒 | 🔒 | ⬜ |
| **info** | ✅ SC1 | ✅ SC2 | ✅ SC3 | ✅ | ✅ SC4 | ✅ SC9 | ✅ SC9 | ✅ SC9| ✅ SC9 |
| **download** | ✅ SC1 | ✅ SC2 | ✅ SC3 | ✅ SC13 | ✅ SC4 | 🔒 | 🔒 | 🔒 | ⬜ |
| **sync** | ✅ SC1,SC5 | ✅ SC2 | ✅ SC3 | ✅ SC13 | ✅ SC4 | 🔒 | 🔒 | 🔒 | ⬜ |
| **delete** | ✅ SC1 | ✅ SC2 | ✅ SC3 | ✅ SC3 | ✅ SC4 | 🔒 | 🔒 | 🔒 | ⬜ |
> **图例:**
> - ✅ 已测试通过
> - ⬜ 不适用(VIT 端点不支持源码读写和锁定操作)
> - 🔒 SAP 系统限制(当前目标系统 NW 7.40 的 ADT API 不支持 DDIC 对象的源码读写和创建/删除)
> **DDIC 对象(🔒)说明:**
> - `info` 可用(使用 `Accept: */*` 头),已在 SC9 验证
> - `create/download/sync/delete` 不可用 — 经实际探测,SAP NetWeaver 7.40 的 ADT 框架对 DDIC 对象(domain/dataelement/table)不提供源码读写端点(返回 404/406)
> - 在 SAP S/4HANA 或更高版本 NetWeaver 系统上,这些操作可能可用
> - 代码层面已实现完整支持(URI 映射、创建模板、argparse choices 均已配置),仅受目标系统 API 限制
> **tabletype(⬜)说明:**
> - `info` 已可用 — 通过 VIT 端点(`/sap/bc/adt/vit/wb/object_type/ttypda/object_name/{name}`)查询元数据,已在 SC9 验证
> - `download/sync/delete` 不可用 — VIT 端点为只读元数据接口,不提供源码读写和锁定操作
> - `create` 不可用 — 无可用的集合端点用于创建新对象
---
## 场景测试详情
### 场景组 happy — 正向完整流程
> 核心流程:创建 → 查询 → 下载 → 修改 → 同步激活 → 验证修改 → 删除 → 验证不存在
#### 场景 1: 正向完整流程 — report
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC1-1 | 创建 report | `create --name ZTEST_SC1_RPT_xxx --type report` | rc == 0 | ✅ |
| SC1-2 | 查询验证 | `info --name ZTEST_SC1_RPT_xxx --type report` | rc == 0 且名称匹配 | ✅ |
| SC1-3 | 下载源码 | `download --name ZTEST_SC1_RPT_xxx --type report` | rc == 0 且文件存在 | ✅ |
| SC1-4 | 修改后同步激活 | 修改源码 → `sync` | 语法检查通过 + 激活成功 | ✅ |
| SC1-5 | 验证修改持久化 | 再次 `download` | 源码包含修改标记 | ✅ |
| SC1-6 | 删除对象 | `delete --name ZTEST_SC1_RPT_xxx` | 删除成功 | ✅ |
| SC1-7 | 验证已不存在 | `info` | rc != 0 | ✅ |
#### 场景 2: 正向完整流程 — class
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC2-1 | 创建 class | `create --name ZTEST_SC2_CLS_xxx --type class` | rc == 0 | ✅ |
| SC2-2 | 查询验证 | `info --name ZTEST_SC2_CLS_xxx --type class` | rc == 0 且名称匹配 | ✅ |
| SC2-3 | 下载源码 | `download --name ZTEST_SC2_CLS_xxx --type class` | rc == 0 且文件存在 | ✅ |
| SC2-4 | 修改后同步激活 | 修改源码 → `sync` | 语法检查通过 + 激活成功 | ✅ |
| SC2-5 | 验证修改持久化 | 再次 `download` | 源码包含修改标记 | ✅ |
| SC2-6 | 删除对象 | `delete --name ZTEST_SC2_CLS_xxx` | 删除成功 | ✅ |
| SC2-7 | 验证已不存在 | `info` | rc != 0 | ✅ |
#### 场景 3: 正向完整流程 — function + functiongroup
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC3-1 | 创建函数组 | `create --name ZTEST_SC3_FG_xxx --type functiongroup` | rc == 0 | ✅ |
| SC3-2 | info 验证函数组存在 | `info --name ZTEST_SC3_FG_xxx --type functiongroup` | rc == 0 且名称匹配 | ✅ |
| SC3-3 | 创建函数 | `create --name ZTEST_SC3_FG_xxx/ZTEST_SC3_FM_xxx --type function` | rc == 0 | ✅ |
| SC3-4 | info 验证函数存在 | `info` | rc == 0 且名称匹配 | ✅ |
| SC3-5 | 下载函数源码 | `download` | rc == 0 且文件存在 | ✅ |
| SC3-6 | 首次修改 → 同步激活 | 修改源码 → `sync` | 语法检查通过 + 激活成功 | ✅ |
| SC3-7 | 再次下载验证修改持久化 | `download` | rc == 0 且文件包含修改标记 | ✅ |
| SC3-7b | 验证修改内容已持久化 | 读取下载文件 | 包含 "SC3 TEST MARKER" | ✅ |
| SC3-8 | 二次修改 → 同步激活 | 替换标记 → `sync` | 语法检查通过 + 激活成功 | ✅ |
| SC3-9 | info 验证函数仍存在 | `info --name ... --type function` | rc == 0 且名称匹配 | ✅ |
| SC3-10 | info 验证函数组仍存在 | `info --name ... --type functiongroup` | rc == 0 且名称匹配 | ✅ |
| SC3-11 | 删除函数 | `delete` 函数模块 | 删除成功 | ✅ |
| SC3-12 | info 验证函数组在函数删除后仍存在 | `info --name ... --type functiongroup` | rc == 0(函数组独立于函数) | ✅ |
| SC3-13 | 删除函数组 | `delete` 函数组 | 删除成功 | ✅ |
| SC3-14 | info 验证函数组已不存在 | `info` | rc != 0 | ✅ |
#### 场景 4: 正向完整流程 — interface
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC4-1 | 创建 interface | `create --name ZTEST_SC4_INT_xxx --type interface` | rc == 0 | ✅ |
| SC4-2 | 查询验证 | `info --name ZTEST_SC4_INT_xxx --type interface` | rc == 0 且名称匹配 | ✅ |
| SC4-3 | 下载源码 | `download --name ZTEST_SC4_INT_xxx --type interface` | rc == 0 且文件存在 | ✅ |
| SC4-4 | 修改后同步激活 | 修改源码 → `sync` | 语法检查通过 + 激活成功 | ✅ |
| SC4-5 | 验证修改持久化 | 再次 `download` | 源码包含修改标记 | ✅ |
| SC4-6 | 删除对象 | `delete --name ZTEST_SC4_INT_xxx` | 删除成功 | ✅ |
| SC4-7 | 验证已不存在 | `info` | rc != 0 | ✅ |
---
### 场景组 error — 逆向错误处理
#### 场景 5: 逆向 — 语法错误
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC5-1 | 创建 report | `create` | rc == 0 | ✅ |
| SC5-2 | 下载源码 | `download` | rc == 0 且文件存在 | ✅ |
| SC5-3 | 注入无效语法 → 同步 | 添加 `INVALID_SYNTAX_HERE.``sync` | rc != 0 且包含错误信息 | ✅ |
| SC5-4 | 恢复源码 → 重新同步 | 还原源码 → `sync` | rc == 0 + 激活成功 | ✅ |
| SC5-5 | 清理删除 | `delete` | 删除成功 | ✅ |
#### 场景 6: 逆向 — 对象不存在
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC6-1 | 查询不存在的 class | `info --name Z_NOT_EXIST_99999 --type class` | rc != 0 且包含"不存在" | ✅ |
| SC6-2 | 下载不存在的 report | `download --name Z_NOT_EXIST_99999` | rc != 0 | ✅ |
| SC6-3 | 删除不存在的 report | `delete --name Z_NOT_EXIST_99999` | rc != 0 | ✅ |
#### 场景 7: 逆向 — 重复创建
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC7-1 | 首次创建 | `create --name ZTEST_SC7_RPT_xxx` | rc == 0 | ✅ |
| SC7-2 | 重复创建 → 报错 | 再次 `create` 同名对象 | rc != 0 | ✅ |
| SC7-3 | 清理删除 | `delete` | 删除成功 | ✅ |
---
### 场景组 edge — 边界场景
#### 场景 8: 边界 — 同步自动创建
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC8-1 | 准备本地源码 | 创建 .abap 文件 | 文件存在 | ✅ |
| SC8-2 | sync 自动创建 | `sync` 不存在的对象 | rc == 0(自动创建) | ✅ |
| SC8-3 | info 验证存在 | `info` | rc == 0 且名称匹配 | ✅ |
| SC8-4 | 清理删除 | `delete` | 删除成功 | ✅ |
#### 场景 9: 边界 — DDIC 对象查询
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC9-1 | info domain | `info --name ZSAPILOT_OBJECT_TYPE --type domain` | rc == 0 且名称匹配 | ✅ |
| SC9-2 | info dataelement | `info --name ZSAPILOT_OBJECT_NAME --type dataelement` | rc == 0 | ✅ |
| SC9-3 | info table | `info --name ZSAPILOT_OBJ --type table` | rc == 0 | ✅ |
| SC9-4 | info tabletype | `info --name ZSAPILOT_OBJECT_TT --type tabletype` | rc == 0 且名称匹配 | ✅ |
#### 场景 10: 边界 — 删除安全确认
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC10-1 | 创建 report | `create` | rc == 0 | ✅ |
| SC10-2 | delete 输入 no → 取消 | `delete` stdin="no" | 输出包含"已取消" | ✅ |
| SC10-3 | info 验证仍存在 | `info` | rc == 0 且名称匹配 | ✅ |
| SC10-4 | delete 输入 yes → 成功 | `delete` stdin="yes" | 删除成功 | ✅ |
| SC10-5 | info 验证已不存在 | `info` | rc != 0 | ✅ |
#### 场景 13: 边界 — functiongroup 不支持源码操作
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC13-1 | download functiongroup → 报错 | `download --name ZIDT_MCP_TOOL --type functiongroup` | rc != 0 且包含"不支持" | ✅ |
| SC13-2 | sync functiongroup → 报错 | `sync --name ZIDT_MCP_TOOL --type functiongroup` | rc != 0 且包含"不支持" | ✅ |
---
### 场景组 existing — 已有对象操作
#### 场景 11: 已有对象查询
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC11-1 | info 已有 class | `info --name ZCL_SAP_MCP_HANDLER --type class` | rc == 0 且名称匹配 | ✅ |
| SC11-2 | 下载已有 class | `download --name ZCL_SAP_MCP_HANDLER` | rc == 0 且文件存在 | ✅ |
| SC11-3 | 同步无修改 → 成功 | `sync`(源码未修改) | 语法检查通过 + 激活成功 | ✅ |
---
### 场景组 config — 配置与环境
#### 场景 12: 配置与环境
| 编号 | 步骤 | 操作 | 验证条件 | 结果 |
|:----:|------|------|---------|:----:|
| SC12-1 | 默认配置运行 | `download`(使用 config.ini | rc == 0 | ✅ |
| SC12-2 | 无效 SAP_HOST | 设置 `SAP_HOST=http://invalid:9999` | rc != 0 | ✅ |
---
## 测试统计
### 按场景组统计
| 场景组 | 场景数 | 步骤总数 | ✅ 通过 | ❌ 失败 | 通过率 |
|--------|:------:|:--------:|:-------:|:-------:|:------:|
| happy(正向完整流程) | 4 | 36 | 36 | 0 | 100% |
| error(逆向错误处理) | 3 | 11 | 11 | 0 | 100% |
| edge(边界场景) | 4 | 14 | 14 | 0 | 100% |
| existing(已有对象) | 1 | 3 | 3 | 0 | 100% |
| config(配置环境) | 1 | 2 | 2 | 0 | 100% |
| **合计** | **13** | **66** | **66** | **0** | **100%** |
### 失败项分析
本次测试全部通过,无失败项。
### 测试过程中发现并修复的问题
| # | 问题 | 修复内容 |
|---|------|---------|
| 1 | 下载文件含 `\r\r\n` 双重回车 | 保存前统一换行符 `source.replace("\r\n", "\n")` |
| 2 | `lock()` 未传 corrNr 导致写入冲突 | `lock()` 增加 `corr_nr` 参数 |
| 3 | `activate()` 误判 inactiveObjects 为成功 | 新增 Content-Type 检测,正确处理 `inactiveCtsObjects` 响应 |
| 4 | `activate()` 使用 `preauditRequested=true` 导致激活失败 | 移除该参数 |
| 5 | `_headers()` 硬编码 `Accept-Language: EN` | 移除硬编码,使用 SAP 系统登录语言 |
| 6 | `cmd_sync` 交互式等待用户输入 | 改为非交互式,报错直接退出 |
| 7 | `cmd_sync` 缺少语法检查步骤 | 新增 `syntax_check()` 方法,激活前先做语法检查 |
| 8 | `cmd_sync` 对象不存在时直接退出 | 改为自动创建空对象后继续同步流程 |
| 9 | `create` 缺少 interface 默认模板 | 新增 interface 模板到 `DEFAULT_TEMPLATES` |
| 10 | `parse_object_name` 对 functiongroup 崩溃(src_uri 为 None | 增加 `if config["src_uri_template"] else None` |
| 11 | `cmd_info` 缺少名称校验 | 新增查询结果名称与请求名称的对比校验 |
| 12 | `cmd_info` function 名称校验失败 | 用 `split("/")[-1]` 取实际对象名进行校验 |
| 13 | DDIC 对象 info 查询 Accept 头不正确 | 改用 `*/*` Accept 头 |