Files
sap-cli-skill/docs/adt/06.ADT测试代码.md
T
吴让宇 c5905a5b1e 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 重写为单源开发流程。
2026-09-11 00:40:15 +08:00

337 lines
11 KiB
Markdown

---
title: ADT 测试代码 — 读取程序源代码
created: 2026-05-19
tags:
- SAP
- ADT
- REST
- python
- test-code
parent: "[[sap-cli/README|SAP ADT 学习笔记总览]]"
---
# ADT 测试代码 — 通过 REST API 读取程序源代码
> [!abstract] 概述
> 通过 ADT REST API 连接 SAP 系统,测试连接、服务发现、读取程序源代码、搜索仓库对象。
> 参见:[[sap-cli/02.查询代码-原理]]
## 运行方式
```bash
# 安装依赖
pip install requests
# 运行测试
python sap-cli/test_adt_client.py
```
## 测试结果
> [!success] 2026-05-19 测试通过
> - 连接验证: HTTP 200 ✅
> - 服务发现: 311 个可用服务 ✅
> - 读取源代码: `ZIDTR_IMPORT_ACCOUNTING_ORDER` (37,655 字符 / 1,096 行) ✅
> - 仓库搜索: quickSearch 返回 0 结果 ⚠️
## 完整代码
```python
"""
SAP ADT REST Client — 测试脚本
通过 ADT REST API 连接 SAP 系统并获取程序源代码
参考: [[sap-cli/02.查询代码-原理]]
[[sap-cli/01.ADT架构与通信原理]]
端点: /sap/bc/adt/programs/programs/{name}/source/main
"""
import requests
from urllib.parse import urljoin
import xml.etree.ElementTree as ET
import sys
import json
import io
# 修复 Windows 控制台编码问题
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
# ============================================================
# 配置 — 来自 [[sap-cli/README]] 测试服务器信息
# ============================================================
ADT_BASE_URL = "http://support.learningleader.com.cn:55955/sap/bc/adt"
SAP_CLIENT = "100"
USER = "admin2"
PASSWORD = "654321"
PROGRAM_NAME = "ZIDTR_IMPORT_ACCOUNTING_ORDER"
class ADTClient:
"""简易 ADT REST 客户端"""
CSRF_HEADER = "x-csrf-token"
CSRF_FETCH = "Fetch"
def __init__(self, base_url: str, client: str, user: str, password: str):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.auth = (user, password)
self.session.headers.update({
"Accept": "*/*",
"Accept-Language": "EN",
"sap-client": client,
})
# 忽略 SSL 警告(测试环境可能是自签名证书)
requests.packages.urllib3.disable_warnings(
requests.packages.urllib3.exceptions.InsecureRequestWarning
)
self.session.verify = False
self._csrf_token: str | None = None
def _url(self, path: str) -> str:
return f"{self.base_url}/{path}"
def fetch_csrf_token(self) -> str:
"""获取 CSRF Token — 写操作必需"""
print(" → 正在获取 CSRF Token...")
resp = self.session.get(
self._url("discovery"),
headers={self.CSRF_HEADER: self.CSRF_FETCH},
)
resp.raise_for_status()
self._csrf_token = resp.headers.get(self.CSRF_HEADER, "")
print(f" ✓ CSRF Token 获取成功: {self._csrf_token[:20]}...")
return self._csrf_token
def get(self, path: str, **kwargs) -> requests.Response:
"""发送 GET 请求"""
resp = self.session.get(self._url(path), **kwargs)
resp.raise_for_status()
return resp
def test_connection(client: ADTClient) -> bool:
"""测试 1: 验证 ADT 连接"""
print("\n" + "=" * 60)
print("测试 1: 验证 ADT 连接")
print("=" * 60)
print(f" URL: {client.base_url}")
print(f" User: {client.session.auth[0]}")
print(f" Client: {client.session.headers.get('sap-client')}")
try:
resp = client.session.get(client._url("discovery"))
print(f" → HTTP Status: {resp.status_code}")
if resp.status_code == 401:
print(f" ✗ 认证失败 (401 Unauthorized)")
print(f" 响应体: {resp.text[:300]}")
return False
resp.raise_for_status()
print(f" ✓ 连接成功! HTTP {resp.status_code}")
print(f" ✓ Content-Type: {resp.headers.get('content-type', 'N/A')}")
return True
except requests.exceptions.HTTPError as e:
print(f" ✗ HTTP 错误: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f" 响应体: {e.response.text[:300]}")
return False
except requests.exceptions.ConnectionError as e:
print(f" ✗ 网络连接失败: {e}")
return False
def test_discovery(client: ADTClient) -> list[dict]:
"""测试 2: 服务发现 — 列出可用的 ADT 服务"""
print("\n" + "=" * 60)
print("测试 2: 服务发现 (Discovery)")
print("=" * 60)
try:
resp = client.get("discovery")
root = ET.fromstring(resp.content)
# 解析 Atom Feed 中的服务集合
ns = {
"app": "http://www.w3.org/2007/app",
"atom": "http://www.w3.org/2005/Atom",
}
services = []
for collection in root.findall(".//app:collection", ns):
href = collection.attrib.get("href", "")
title_el = collection.find("atom:title", ns)
title = title_el.text if title_el is not None else ""
services.append({"href": href, "title": title})
print(f" ✓ 发现 {len(services)} 个可用服务:")
for svc in services[:15]: # 只显示前 15 个
print(f" • {svc['title']}: {svc['href']}")
if len(services) > 15:
print(f" ... 还有 {len(services) - 15} 个服务")
return services
except Exception as e:
print(f" ✗ 服务发现失败: {e}")
return []
def test_read_program_source(client: ADTClient, program_name: str) -> str | None:
"""测试 3: 读取程序源代码"""
print("\n" + "=" * 60)
print(f"测试 3: 读取程序源代码 ({program_name})")
print("=" * 60)
# 尝试多种可能的端点路径
endpoints = [
f"programs/programs/{program_name.lower()}/source/main",
f"programs/programs/{program_name}/source/main",
]
for endpoint in endpoints:
try:
print(f" → 尝试端点: GET {endpoint}")
resp = client.get(
endpoint,
headers={"Accept": "text/plain"},
)
source = resp.text
print(f" ✓ 源代码获取成功!")
print(f" ✓ 程序名: {program_name}")
print(f" ✓ 源代码长度: {len(source)} 字符")
print(f" ✓ 行数: {len(source.splitlines())}")
# 显示前 30 行
lines = source.splitlines()
print(f"\n ┌─── 源代码 (前 30 行) ──────────────────────")
for i, line in enumerate(lines[:30], 1):
print(f" │ {i:4d} | {line}")
if len(lines) > 30:
print(f" │ ... 省略剩余 {len(lines) - 30} 行 ...")
print(f" └────────────────────────────────────────────")
return source
except requests.exceptions.HTTPError as e:
print(f" ✗ HTTP 错误: {e.response.status_code} {e.response.reason}")
if e.response.status_code == 404:
print(f" (程序不存在或端点路径不正确,尝试下一个...)")
continue
else:
print(f" 响应内容: {e.response.text[:200]}")
return None
except Exception as e:
print(f" ✗ 读取失败: {e}")
return None
print(f" ✗ 所有端点尝试均失败")
return None
def test_search_object(client: ADTClient, program_name: str) -> list[dict]:
"""测试 4: 搜索仓库对象"""
print("\n" + "=" * 60)
print(f"测试 4: 搜索仓库对象 ({program_name})")
print("=" * 60)
try:
resp = client.get(
"repository/informationsystem/search",
params={
"operation": "quickSearch",
"query": program_name,
"maxResults": "10",
},
)
ns = "http://www.sap.com/adt/repository/informationsystem"
root = ET.fromstring(resp.content)
results = []
for obj in root.iter(f"{{{ns}}}object"):
results.append({
"name": obj.attrib.get("name", ""),
"type": obj.attrib.get("type", ""),
"uri": obj.attrib.get("uri", ""),
"package": obj.attrib.get("packageName", ""),
})
print(f" ✓ 搜索完成,找到 {len(results)} 个结果:")
for r in results:
print(f" • [{r['type']}] {r['name']} (Package: {r['package']})")
print(f" URI: {r['uri']}")
return results
except Exception as e:
print(f" ✗ 搜索失败: {e}")
return []
def main():
print("=" * 60)
print(" SAP ADT REST API 测试脚本")
print(" 参考: [[sap-cli/02.查询代码-原理]]")
print("=" * 60)
# 创建客户端
client = ADTClient(
base_url=ADT_BASE_URL,
client=SAP_CLIENT,
user=USER,
password=PASSWORD,
)
# 测试 1: 连接验证
if not test_connection(client):
print("\n❌ 连接失败,请检查服务器配置和网络连接。")
sys.exit(1)
# 测试 2: 服务发现
test_discovery(client)
# 测试 3: 读取程序源代码
source = test_read_program_source(client, PROGRAM_NAME)
# 测试 4: 搜索仓库对象
test_search_object(client, PROGRAM_NAME)
# 汇总结果
print("\n" + "=" * 60)
print(" 测试结果汇总")
print("=" * 60)
if source:
print(" ✅ 程序源代码获取成功!")
print(f" 程序: {PROGRAM_NAME}")
print(f" 长度: {len(source)} 字符, {len(source.splitlines())} 行")
else:
print(" ⚠️ 程序源代码获取失败,请确认程序名称是否正确")
print("=" * 60)
if __name__ == "__main__":
main()
```
## 关键 API 端点速查
| 操作 | HTTP 方法 | 端点 | Accept |
|------|----------|------|--------|
| 服务发现 | `GET` | `/sap/bc/adt/discovery` | `*/*` |
| 读取程序源码 | `GET` | `/sap/bc/adt/programs/programs/{name}/source/main` | `text/plain` |
| 读取类源码 | `GET` | `/sap/bc/adt/oo/classes/{name}/source/main` | `text/plain` |
| 仓库搜索 | `GET` | `/sap/bc/adt/repository/informationsystem/search` | `*/*` |
> [!note] 注意事项
>> - discovery 端点的 Accept 必须用 `*/*`,用 `application/xml` 会返回 406
> - 程序名在 URL 中使用**小写**
## 🔗 相关笔记
- [[sap-cli/01.ADT架构与通信原理|01.ADT架构与通信原理]]
- [[sap-cli/02.查询代码-原理|02.查询代码 - 原理]]
- [[sap-cli/03.修改代码-原理|03.修改代码 - 原理]]