Files
吴让宇 c06e350f6d
CI / test (3.10) (push) Waiting to run
CI / test (3.11) (push) Waiting to run
CI / test (3.12) (push) Waiting to run
feat: bgRFC/FM 支撑能力 + ADT 后缀剥离与 CLAS 激活修复(B1 配套)
新增能力:
- remote-enable 子命令:设置函数模块处理类型为远程启用(bgRFC/RFC 执行体必需)。
  实现要点均按实机验证:PUT fmodule:processingType="rfc"(枚举值仅小写 rfc 有效,
  remoteEnabled/remote/RFC 等均 400);lockHandle 走 query 参数(放 header 报
  ParameterNotFound);PUT 后 GET 复核回显;DDIC 结构参数才可远程启用(类本地类型
  报"针对 RFC 不允许使用类或接口的类型")。
- create --type functiongroup 透传 --package / --corr_nr(此前 package 被静默写成 $TMP,
  导致函数组无法归目标包)。

修复:
- activate 请求体补重复引用:ADT CLAS 激活端点对 objectReferences 只有 1 个引用时
  返回 HTTP 200 + 空 body 且不执行激活;引用数 ≥2 才真正激活(同对象重复亦可)。
- scanner 剥离 ADT 文件后缀(xxx.clas.abap 解析错误)。

守卫测试:
- tests/unit/test_bgrfc_support.py(17 例):函数组 package 透传、processingType 助手、
  remote-enable 命令(含 lockHandle 走 query/小写 rfc/错误透传/复核失配判失败)、
  函数 URI 组装不得用组名冒充模块名。
- test_activate.py 增加激活请求体引用数守卫;test_repo_guards.py 增加后缀剥离守卫。

测试:tests/unit 676 + tests/test_sapcli.py 69 = 745 全绿
(PYTHONPATH=assets python -m unittest discover -s tests/unit -t tests)
2026-09-15 22:51:35 +08:00

192 lines
5.4 KiB
Python

"""sap-cli 应用入口:解析参数、初始化连接、路由命令。"""
from __future__ import annotations
import io
import logging
import os
import sys
import requests
from sapcli.config import load_config
from sapcli.client import ADTClient
from sapcli.commands import (
cmd_create,
cmd_delete,
cmd_unlock,
cmd_download,
cmd_info,
cmd_init,
cmd_refresh,
cmd_sync,
cmd_sync_all,
cmd_config,
cmd_list,
cmd_whereused,
cmd_search,
cmd_transport,
cmd_check,
cmd_format,
cmd_diff,
cmd_package,
cmd_cds,
cmd_analyze,
cmd_scaffold,
cmd_show_table,
cmd_read_table,
cmd_run_program,
cmd_activate,
cmd_upload,
cmd_syntax_check,
cmd_unit_test,
cmd_history,
cmd_clone,
cmd_enhancement,
cmd_remote_enable,
)
from sapcli.auth import cmd_auth_login, cmd_auth_logout, cmd_auth_status
from sapcli.exceptions import SapCliError, ConfigError
from sapcli.cli.parser import build_parser
def _setup_encoding() -> None:
"""确保 stdout/stderr 使用 UTF-8 编码。"""
if hasattr(sys.stdout, "buffer"):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "buffer"):
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
def _setup_logging() -> None:
"""配置日志(写入文件,不影响 stdout)。"""
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "log")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "adt_tools.log")
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s | %(levelname)-7s | %(message)s",
handlers=[
logging.FileHandler(log_file, encoding="utf-8", mode="a"),
],
)
def _suppress_ssl_warnings() -> None:
"""禁用 urllib3 的 InsecureRequestWarning。"""
requests.packages.urllib3.disable_warnings(
requests.packages.urllib3.exceptions.InsecureRequestWarning
)
def main() -> None:
"""sap-cli 主入口。"""
_setup_encoding()
_setup_logging()
_suppress_ssl_warnings()
parser = build_parser()
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
# config 命令不需要 SAP 连接
if args.command == "config":
cmd_config(args)
return
# auth 不需要 SAP 连接
if args.command == "auth":
auth_action = getattr(args, "auth_action", None)
if auth_action == "login":
cmd_auth_login(args)
elif auth_action == "logout":
cmd_auth_logout(args)
else:
cmd_auth_status(args)
return
# clone 自行按 --from/--to profile 构建源/目标连接,跳过默认登录
if args.command == "clone":
try:
cmd_clone(args, None)
except SapCliError as e:
print(f"\n{e}")
sys.exit(1)
return
# 加载配置
try:
sap_cfg, loaded_from = load_config(args.config, profile=getattr(args, "profile", None))
if loaded_from:
logging.getLogger("sapcli").info("Config loaded from: %s", loaded_from)
except ConfigError as e:
print(f" ✗ {e}")
print(f" 请创建配置文件 (config.ini) 或设置环境变量:")
print(f" SAP_HOST, SAP_CLIENT, SAP_USER, SAP_PASSWORD")
sys.exit(1)
# 建立 SAP 连接
verify_ssl = getattr(args, "verify_ssl", False)
client = ADTClient(sap_cfg.host, sap_cfg.client, sap_cfg.user, sap_cfg.password, verify_ssl=verify_ssl)
print("\n → 正在登录...")
client.login()
print(" ✓ 登录成功\n")
# 命令路由表
command_map = {
"download": cmd_download,
"sync": cmd_sync_all if getattr(args, "all", False) else cmd_sync,
"info": cmd_info,
"delete": cmd_delete,
"unlock": cmd_unlock,
"create": cmd_create,
"init": cmd_init,
"refresh": cmd_refresh,
"list": cmd_list,
"whereused": cmd_whereused,
"search": cmd_search,
"transport": cmd_transport,
"check": cmd_check,
"format": cmd_format,
"activate": cmd_activate,
"upload": cmd_upload,
"syntax-check": cmd_syntax_check,
"diff": cmd_diff,
"package": cmd_package,
"cds": cmd_cds,
"analyze": cmd_analyze,
"scaffold": cmd_scaffold,
"show-table": cmd_show_table,
"read-table": cmd_read_table,
"run-program": cmd_run_program,
"unit-test": cmd_unit_test,
"history": cmd_history,
"enhancement": cmd_enhancement,
"remote-enable": cmd_remote_enable,
}
# 批量 sync 参数校验
if args.command == "sync" and getattr(args, "all", False):
if args.name and args.type:
print(" ✗ --all 模式下不需要 --name 和 --type 参数")
sys.exit(1)
elif args.command == "sync" and not getattr(args, "all", False):
if not args.name or not args.type:
print(" ✗ 单对象模式需要 --name 和 --type 参数")
sys.exit(1)
# 执行命令
handler = command_map.get(args.command)
if handler is None:
print(f" ✗ 未知命令: {args.command}")
sys.exit(1)
try:
handler(args, client)
except SapCliError as e:
print(f"\n{e}")
sys.exit(1)