chore: sync skill to v2.2.1
- NW 7.40 E2E-verified compatibility matrix in SKILL.md - DDIC fixes: include namespace, tabletype exists check, DDIC delete lock Accept - New commands: clone, enhancement, history, unit-test - 665 tests, 96% coverage
This commit is contained in:
@@ -60,6 +60,18 @@ from sapcli.commands.upload import (
|
||||
from sapcli.commands.syntax_check import (
|
||||
cmd_syntax_check,
|
||||
)
|
||||
from sapcli.commands.unit_test import (
|
||||
cmd_unit_test,
|
||||
)
|
||||
from sapcli.commands.history import (
|
||||
cmd_history,
|
||||
)
|
||||
from sapcli.commands.clone import (
|
||||
cmd_clone,
|
||||
)
|
||||
from sapcli.commands.enhancement import (
|
||||
cmd_enhancement,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"cmd_create",
|
||||
@@ -88,4 +100,8 @@ __all__ = [
|
||||
"cmd_activate",
|
||||
"cmd_upload",
|
||||
"cmd_syntax_check",
|
||||
"cmd_unit_test",
|
||||
"cmd_history",
|
||||
"cmd_clone",
|
||||
"cmd_enhancement",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""clone 命令 — 跨系统克隆对象(下载源系统 → 上传目标系统)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from sapcli.client import ADTClient
|
||||
from sapcli.commands.crud import _sync_single
|
||||
from sapcli.config import load_config
|
||||
from sapcli.exceptions import ConfigError, InvalidNameError, ObjectNotFoundError, SapCliError
|
||||
from sapcli.types import get_type_config, parse_object_name
|
||||
|
||||
logger = logging.getLogger("sapcli.commands.clone")
|
||||
|
||||
|
||||
def _make_profile_client(config_path: str | None, profile: str, verify_ssl: bool = False) -> ADTClient:
|
||||
"""按 profile 加载配置并建立已登录的 ADT 客户端。"""
|
||||
sap_cfg, _ = load_config(config_path, profile=profile)
|
||||
client = ADTClient(
|
||||
sap_cfg.host, sap_cfg.client, sap_cfg.user, sap_cfg.password,
|
||||
verify_ssl=verify_ssl,
|
||||
)
|
||||
client.login()
|
||||
return client
|
||||
|
||||
|
||||
def cmd_clone(args: argparse.Namespace, client: ADTClient) -> None:
|
||||
"""从一个系统下载对象,上传到另一个系统。
|
||||
|
||||
注意:``client`` 参数在本命令中被忽略;源/目标客户端分别按
|
||||
``--from`` / ``--to`` profile 构建。
|
||||
"""
|
||||
name: str = args.name
|
||||
obj_type: str = args.type
|
||||
from_profile: str = args.from_profile
|
||||
to_profile: str = args.to_profile
|
||||
corr_nr: str | None = getattr(args, "corr_nr", None)
|
||||
config_path: str | None = getattr(args, "config", None)
|
||||
verify_ssl: bool = getattr(args, "verify_ssl", False)
|
||||
|
||||
parsed = parse_object_name(name, obj_type)
|
||||
type_label = get_type_config(obj_type).label
|
||||
|
||||
if parsed.src_uri is None:
|
||||
print(f"\n ✗ {type_label}没有源代码,不支持 clone 操作")
|
||||
raise InvalidNameError(f"{type_label}没有源代码,不支持 clone 操作")
|
||||
|
||||
print("=" * 60)
|
||||
print(" 跨系统克隆对象")
|
||||
print("=" * 60)
|
||||
print(f" 对象类型: {type_label}")
|
||||
print(f" 对象名称: {parsed.display_name}")
|
||||
print(f" 源系统: {from_profile}")
|
||||
print(f" 目标系统: {to_profile}")
|
||||
if corr_nr:
|
||||
print(f" 目标传输请求: {corr_nr}")
|
||||
|
||||
# ── Step 1: 从源系统下载源码 ──
|
||||
print(f"\n → 连接源系统 ({from_profile})...")
|
||||
try:
|
||||
src_client = _make_profile_client(config_path, from_profile, verify_ssl)
|
||||
except Exception as e:
|
||||
print(f" ✗ 连接源系统失败: {e}")
|
||||
raise ConfigError(f"连接源系统失败: {e}") from e
|
||||
print(f" ✓ 已登录源系统")
|
||||
|
||||
try:
|
||||
print(f"\n → 检查源对象是否存在...")
|
||||
if not src_client.object_exists(parsed.obj_uri):
|
||||
print(f" ✗ 源对象不存在: {parsed.display_name}")
|
||||
raise ObjectNotFoundError(parsed.display_name, obj_type)
|
||||
print(" ✓ 源对象存在")
|
||||
|
||||
print(f" → 正在从源系统下载源代码...")
|
||||
source = src_client.get_source(parsed.src_uri)
|
||||
source = source.replace("\r\n", "\n").replace("\r", "\n")
|
||||
print(f" ✓ 下载成功 ({len(source)} 字符)")
|
||||
finally:
|
||||
src_client.session.close()
|
||||
|
||||
# 写入临时文件供 _sync_single 复用
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".abap", prefix=f"clone_{parsed.file_base}_")
|
||||
try:
|
||||
with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
|
||||
f.write(source)
|
||||
|
||||
# ── Step 2: 连接目标系统 ──
|
||||
print(f"\n → 连接目标系统 ({to_profile})...")
|
||||
try:
|
||||
tgt_client = _make_profile_client(config_path, to_profile, verify_ssl)
|
||||
except Exception as e:
|
||||
print(f" ✗ 连接目标系统失败: {e}")
|
||||
raise ConfigError(f"连接目标系统失败: {e}") from e
|
||||
print(f" ✓ 已登录目标系统")
|
||||
|
||||
try:
|
||||
print(f"\n → 检查目标对象是否存在...")
|
||||
exists = tgt_client.object_exists(parsed.obj_uri)
|
||||
if exists:
|
||||
print(" ✓ 目标对象已存在(将覆盖)")
|
||||
else:
|
||||
print(" ℹ 目标对象不存在(将创建并同步)")
|
||||
|
||||
# ── Step 3: 同步到目标(不存在则自动创建 + 上传 + 激活)──
|
||||
print(f"\n → 同步到目标系统...")
|
||||
success, error_msg, actual_corr = _sync_single(
|
||||
name, obj_type, tmp_path, tgt_client, corr_nr=corr_nr,
|
||||
)
|
||||
finally:
|
||||
tgt_client.session.close()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if success:
|
||||
print(f"\n {'=' * 60}")
|
||||
print(f" ✓ 克隆完成: {parsed.display_name}")
|
||||
print(f" {from_profile} → {to_profile}")
|
||||
if actual_corr:
|
||||
print(f" 目标传输请求: {actual_corr}")
|
||||
print(f" {'=' * 60}")
|
||||
return
|
||||
|
||||
raise SapCliError(error_msg or f"克隆失败: {parsed.display_name}")
|
||||
@@ -56,6 +56,39 @@ DEFAULT_TEMPLATES: dict[str, str] = {
|
||||
' METHODS: hello.\n'
|
||||
'ENDINTERFACE.\n'
|
||||
),
|
||||
# 以下类型不提供完整业务骨架,仅给出占位注释。
|
||||
# 建议通过 --source 指定本地源码文件,或用 --definition 创建 DDIC 定义。
|
||||
"include": (
|
||||
'*&---------------------------------------------------------------------*\n'
|
||||
'*& Include {name}\n'
|
||||
'*&---------------------------------------------------------------------*\n'
|
||||
'* Include 程序骨架:在此编写可被其它程序 INCLUDE 的 ABAP 片段。\n'
|
||||
'* 提示:用 --source 指定本地源码文件可覆盖本模板。\n'
|
||||
),
|
||||
"messageclass": (
|
||||
'* Message Class {name}\n'
|
||||
'* 消息类(T100)不包含 ABAP 源码,消息条目通过 SE91 维护。\n'
|
||||
'* 提示:建议用 --definition 创建,或通过 SAP GUI (SE91) 维护消息。\n'
|
||||
),
|
||||
"view": (
|
||||
'* Database View {name}\n'
|
||||
'* 数据库视图 DDL 骨架(基础 SELECT 形式,按需替换):\n'
|
||||
'* SELECT <字段列表>\n'
|
||||
'* FROM <主表>\n'
|
||||
'* [JOIN <关联表> ON <条件>]\n'
|
||||
'* [WHERE <过滤条件>]\n'
|
||||
'* 提示:用 --source 指定完整视图定义可覆盖本模板。\n'
|
||||
),
|
||||
"searchhelp": (
|
||||
'* Search Help {name}\n'
|
||||
'* 搜索帮助不包含 ABAP 源码,定义含数据源与显示字段。\n'
|
||||
'* 提示:建议用 --definition 创建,或通过 SAP GUI (SE11) 维护。\n'
|
||||
),
|
||||
"lockobject": (
|
||||
'* Lock Object {name}\n'
|
||||
'* 锁对象不包含 ABAP 源码,定义含锁定模式与主表。\n'
|
||||
'* 提示:建议用 --definition 创建,或通过 SAP GUI (SE11) 维护。\n'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +125,7 @@ def cmd_download(args: argparse.Namespace, client: ADTClient) -> None:
|
||||
print(f" 保存路径: {save_dir}")
|
||||
|
||||
print(f"\n → 检查对象是否存在...")
|
||||
if not client.object_exists(parsed.obj_uri):
|
||||
if not client.object_exists(parsed.exists_uri or parsed.obj_uri):
|
||||
print(f" ✗ 对象不存在: {parsed.display_name}")
|
||||
print(f" 请确认名称和类型是否正确")
|
||||
raise ObjectNotFoundError(parsed.display_name, obj_type)
|
||||
@@ -210,7 +243,7 @@ def _sync_single(
|
||||
|
||||
if not quiet:
|
||||
print(f"\n → 检查对象是否存在...")
|
||||
if not client.object_exists(parsed.obj_uri):
|
||||
if not client.object_exists(parsed.exists_uri or parsed.obj_uri):
|
||||
if not quiet:
|
||||
print(f" ℹ 对象不存在,自动创建空对象...")
|
||||
try:
|
||||
@@ -386,6 +419,15 @@ def cmd_info(args: argparse.Namespace, client: ADTClient) -> None:
|
||||
"dataelement": "*/*",
|
||||
"table": "*/*",
|
||||
"tabletype": "*/*",
|
||||
"structure": "application/vnd.sap.adt.ddic.structures.v1+xml",
|
||||
"include": "application/vnd.sap.adt.programs.includes.v2+xml",
|
||||
"cdsview": "application/vnd.sap.adt.ddic.ddls.v3+xml",
|
||||
"messageclass": "application/vnd.sap.adt.t100.message.classes.v1+xml",
|
||||
"view": "application/vnd.sap.adt.ddic.views.v1+xml",
|
||||
"searchhelp": "application/vnd.sap.adt.ddic.searchhelps.v1+xml",
|
||||
"lockobject": "application/vnd.sap.adt.ddic.lockobjects.v1+xml",
|
||||
"dcl": "text/plain",
|
||||
"ddlX": "text/plain",
|
||||
}
|
||||
|
||||
print("=" * 60)
|
||||
@@ -471,7 +513,7 @@ def cmd_delete(args: argparse.Namespace, client: ADTClient) -> None:
|
||||
print(f" 对象名称: {parsed.display_name}")
|
||||
|
||||
print(f"\n → 检查对象是否存在...")
|
||||
if not client.object_exists(parsed.obj_uri):
|
||||
if not client.object_exists(parsed.exists_uri or parsed.obj_uri):
|
||||
print(f" ✗ 对象不存在: {parsed.display_name}")
|
||||
print(f" 请确认名称和类型是否正确")
|
||||
raise ObjectNotFoundError(parsed.display_name, obj_type)
|
||||
@@ -639,7 +681,7 @@ def cmd_create(args: argparse.Namespace, client: ADTClient) -> None:
|
||||
print(f"\n → 检查对象是否存在...")
|
||||
if obj_type != "functiongroup":
|
||||
parsed = parse_object_name(name, obj_type)
|
||||
if client.object_exists(parsed.obj_uri):
|
||||
if client.object_exists(parsed.exists_uri or parsed.obj_uri):
|
||||
print(f" ✗ 对象已存在: {display_name}")
|
||||
raise ObjectAlreadyExistsError(display_name, type_label)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""enhancement 命令 — 查询对象的增强实现。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from sapcli.client import ADTClient
|
||||
from sapcli.exceptions import ObjectNotFoundError
|
||||
from sapcli.types import get_type_config, parse_object_name
|
||||
|
||||
logger = logging.getLogger("sapcli.commands.enhancement")
|
||||
|
||||
|
||||
def cmd_enhancement(args: argparse.Namespace, client: ADTClient) -> None:
|
||||
"""查询对象的增强实现(ENHO)。"""
|
||||
name: str = args.name
|
||||
obj_type: str = args.type
|
||||
|
||||
parsed = parse_object_name(name, obj_type)
|
||||
type_label = get_type_config(obj_type).label
|
||||
|
||||
print("=" * 60)
|
||||
print(" 对象增强实现查询")
|
||||
print("=" * 60)
|
||||
print(f" 对象类型: {type_label}")
|
||||
print(f" 对象名称: {parsed.display_name}")
|
||||
|
||||
print(f"\n → 检查对象是否存在...")
|
||||
if not client.object_exists(parsed.obj_uri):
|
||||
print(f" ✗ 对象不存在: {parsed.display_name}")
|
||||
raise ObjectNotFoundError(parsed.display_name, obj_type)
|
||||
print(" ✓ 对象存在")
|
||||
|
||||
print(f"\n → 正在查询增强实现...")
|
||||
try:
|
||||
enhancements = client.get_enhancements(parsed.obj_uri)
|
||||
except Exception as e:
|
||||
print(f" ✗ 查询增强实现失败: {e}")
|
||||
return
|
||||
|
||||
if not enhancements:
|
||||
print(" ℹ 未找到增强实现")
|
||||
return
|
||||
|
||||
print(f" ✓ 找到 {len(enhancements)} 个增强实现\n")
|
||||
for enh in enhancements:
|
||||
enh_name = enh.get("name", "")
|
||||
enh_type = enh.get("type", "ENHO")
|
||||
print(f" Enhancement Implementation: {enh_name} ({enh_type})")
|
||||
|
||||
enhanced_name = enh.get("enhanced_name", "")
|
||||
enhanced_type = enh.get("enhanced_type", "")
|
||||
if enhanced_name:
|
||||
print(f" Enhanced Object: {enhanced_name} ({enhanced_type})")
|
||||
|
||||
elements = enh.get("elements", [])
|
||||
for i, el in enumerate(elements, 1):
|
||||
el_type = el.get("type", "")
|
||||
el_name = el.get("name", "")
|
||||
label = f"{el_type}: {el_name}" if el_type and el_name else (el_name or el_type)
|
||||
print(f" Element {i}: {label}")
|
||||
extras = []
|
||||
if el.get("mode"):
|
||||
extras.append(f"Mode: {el['mode']}")
|
||||
if el.get("replacing"):
|
||||
extras.append(f"Replacing: {el['replacing']}")
|
||||
if extras:
|
||||
print(f" {', '.join(extras)}")
|
||||
print()
|
||||
@@ -0,0 +1,65 @@
|
||||
"""history 命令 — 查询对象版本历史。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from sapcli.client import ADTClient
|
||||
from sapcli.exceptions import ObjectNotFoundError
|
||||
from sapcli.types import get_type_config, parse_object_name
|
||||
|
||||
logger = logging.getLogger("sapcli.commands.history")
|
||||
|
||||
|
||||
def _format_date(value: str) -> str:
|
||||
"""ISO 日期 ``2026-06-15T10:30:00`` → ``2026-06-15 10:30:00``。"""
|
||||
if not value:
|
||||
return ""
|
||||
return value.replace("T", " ").rstrip("Z").strip()
|
||||
|
||||
|
||||
def cmd_history(args: argparse.Namespace, client: ADTClient) -> None:
|
||||
"""查询对象的版本历史。"""
|
||||
name: str = args.name
|
||||
obj_type: str = args.type
|
||||
|
||||
parsed = parse_object_name(name, obj_type)
|
||||
type_label = get_type_config(obj_type).label
|
||||
|
||||
print("=" * 60)
|
||||
print(" 对象版本历史")
|
||||
print("=" * 60)
|
||||
print(f" 对象类型: {type_label}")
|
||||
print(f" 对象名称: {parsed.display_name}")
|
||||
|
||||
print(f"\n → 检查对象是否存在...")
|
||||
if not client.object_exists(parsed.obj_uri):
|
||||
print(f" ✗ 对象不存在: {parsed.display_name}")
|
||||
raise ObjectNotFoundError(parsed.display_name, obj_type)
|
||||
print(" ✓ 对象存在")
|
||||
|
||||
print(f"\n → 正在查询版本历史...")
|
||||
try:
|
||||
versions = client.get_object_versions(parsed.obj_uri)
|
||||
except Exception as e:
|
||||
print(f" ✗ 查询版本历史失败: {e}")
|
||||
return
|
||||
|
||||
if not versions:
|
||||
print(" ℹ 未找到版本历史")
|
||||
return
|
||||
|
||||
print(f" ✓ 找到 {len(versions)} 个版本\n")
|
||||
print(
|
||||
f" {'Version':<10} {'Author':<12} {'Date':<22} Description"
|
||||
)
|
||||
print(
|
||||
f" {'─' * 10} {'─' * 12} {'─' * 22} {'─' * 30}"
|
||||
)
|
||||
for v in versions:
|
||||
version = v.get("version", "") or "active"
|
||||
author = v.get("author", "")
|
||||
date = _format_date(v.get("date", ""))
|
||||
title = v.get("versionTitle", "")
|
||||
print(f" {version:<10} {author:<12} {date:<22} {title}")
|
||||
print()
|
||||
@@ -25,6 +25,10 @@ _TYPE_FILTER_MAP: dict[str, str] = {
|
||||
"cdsview": "DDLS/DF",
|
||||
"messageclass": "MSAG",
|
||||
"view": "VIEW",
|
||||
"searchhelp": "SHLP",
|
||||
"lockobject": "LOCK",
|
||||
"dcl": "DCLS",
|
||||
"ddlX": "DDLX",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""unit-test 命令 — 执行 ABAP Unit 测试。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from sapcli.client import ADTClient
|
||||
from sapcli.types import get_type_config, parse_object_name
|
||||
|
||||
logger = logging.getLogger("sapcli.commands.unit_test")
|
||||
|
||||
|
||||
def _parse_duration(value: str) -> float:
|
||||
"""安全解析方法耗时(秒),无法解析返回 0.0。"""
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def cmd_unit_test(args: argparse.Namespace, client: ADTClient) -> None:
|
||||
"""执行 ABAP Unit 测试并汇总结果。"""
|
||||
name: str = args.name
|
||||
obj_type: str = args.type
|
||||
|
||||
parsed = parse_object_name(name, obj_type)
|
||||
type_label = get_type_config(obj_type).label
|
||||
|
||||
print("=" * 60)
|
||||
print(" ABAP Unit 测试")
|
||||
print("=" * 60)
|
||||
print(f" 对象类型: {type_label}")
|
||||
print(f" 对象名称: {parsed.display_name}")
|
||||
|
||||
print(f"\n → 正在执行 ABAP Unit 测试...")
|
||||
try:
|
||||
result = client.run_unit_test(parsed.obj_uri)
|
||||
except Exception as e:
|
||||
print(f" ✗ 测试执行失败: {e}")
|
||||
return
|
||||
|
||||
summary = result.get("summary", {})
|
||||
classes = result.get("classes", [])
|
||||
|
||||
if not classes:
|
||||
print(" ℹ 未找到测试类(对象可能不包含 ABAP Unit 测试)")
|
||||
return
|
||||
|
||||
print()
|
||||
for cls in classes:
|
||||
cls_name = cls.get("name") or parsed.display_name
|
||||
methods = cls.get("methods", [])
|
||||
total_duration = sum(_parse_duration(m.get("duration", "")) for m in methods)
|
||||
failed = [m for m in methods if m.get("alert")]
|
||||
|
||||
if not failed:
|
||||
print(
|
||||
f" ✅ {cls_name} {len(methods)} methods "
|
||||
f"{total_duration:.2f}s PASSED"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f" ❌ {cls_name} {len(methods)} methods "
|
||||
f"{total_duration:.2f}s FAILED"
|
||||
)
|
||||
for m in failed:
|
||||
line_info = f" at line {m['line']}" if m.get("line") else ""
|
||||
print(
|
||||
f" method: {m['name']} FAILED: "
|
||||
f"{m['alert']}{line_info}"
|
||||
)
|
||||
|
||||
# 汇总
|
||||
tests = summary.get("tests", "?")
|
||||
failures = summary.get("failures", "0")
|
||||
errors = summary.get("errors", "0")
|
||||
skipped = summary.get("skipped", "0")
|
||||
print(f"\n {'─' * 50}")
|
||||
print(
|
||||
f" 合计: {tests} 个测试 | 失败 {failures} | 错误 {errors} | "
|
||||
f"跳过 {skipped}"
|
||||
)
|
||||
print(f" {'─' * 50}")
|
||||
Reference in New Issue
Block a user