方向反转:此前 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 重写为单源开发流程。
337 lines
11 KiB
Markdown
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.修改代码 - 原理]]
|