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)
This commit is contained in:
@@ -39,6 +39,50 @@ def _find_local(container: ET.Element, name: str) -> ET.Element | None:
|
||||
return None
|
||||
|
||||
|
||||
def _set_processing_type(xml_text: str, value: str) -> str:
|
||||
"""把 FM 元数据 XML 的 ``processingType`` 字段改为 ``value``(元素/属性两种形态兼容)。
|
||||
|
||||
按本地名匹配(``fm:``/``fmodule:``/无前缀均命中),回写前注册已知命名空间
|
||||
前缀,尽量保持与原 XML 一致。实测(NW 7.52):ADT 只认小写 ``rfc``,
|
||||
``remoteEnabled``/``remote``/``RFC`` 等值全 400。
|
||||
"""
|
||||
ET.register_namespace("fm", "http://www.sap.com/adt/functions/fmodules")
|
||||
ET.register_namespace("adtcore", "http://www.sap.com/adt/core")
|
||||
root = ET.fromstring(xml_text)
|
||||
changed = False
|
||||
# 元素形态:<fm:processingType>normal</fm:processingType>
|
||||
for el in root.iter():
|
||||
if _local(el.tag) == "processingType":
|
||||
el.text = value
|
||||
changed = True
|
||||
# 属性形态:processingType="normal"(含命名空间前缀)
|
||||
if not changed:
|
||||
for el in root.iter():
|
||||
for key in list(el.attrib.keys()):
|
||||
if _local(key) == "processingType":
|
||||
el.attrib[key] = value
|
||||
changed = True
|
||||
if not changed:
|
||||
raise SapCliError("函数模块 XML 中未找到 processingType 字段,无法设置远程启用")
|
||||
return ET.tostring(root, encoding="unicode", xml_declaration=True)
|
||||
|
||||
|
||||
def _get_processing_type(xml_text: str) -> str:
|
||||
"""从 FM 元数据 XML 读取 ``processingType`` 当前值(无则返回空串)。"""
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
except ET.ParseError:
|
||||
return ""
|
||||
for el in root.iter():
|
||||
if _local(el.tag) == "processingType":
|
||||
return (el.text or "").strip()
|
||||
for el in root.iter():
|
||||
for key, val in el.attrib.items():
|
||||
if _local(key) == "processingType":
|
||||
return val
|
||||
return ""
|
||||
|
||||
|
||||
# ADT lock 端点返回 ABAP 结构 XML。DDIC 对象(domain/dataelement/table/structure)
|
||||
# 的 lock 端点对默认 Accept: */* 返回 HTTP 406,必须显式请求 lock 结果类型。
|
||||
# 参考 abap-adt-api objectcontents.ts 的 lock 实现。
|
||||
@@ -531,13 +575,14 @@ class DdicMixin:
|
||||
group_name: str,
|
||||
description: str | None = None,
|
||||
corr_nr: str | None = None,
|
||||
package: str = "$TMP",
|
||||
) -> bool:
|
||||
config = get_type_config("functiongroup")
|
||||
params: dict[str, str] = {"groupname": group_name.upper()}
|
||||
if corr_nr:
|
||||
params["corrNr"] = corr_nr
|
||||
desc = description or group_name
|
||||
body = self._build_create_body("functiongroup", group_name.upper(), desc)
|
||||
body = self._build_create_body("functiongroup", group_name.upper(), desc, package)
|
||||
url = f"{self.host}{config.collection_uri}"
|
||||
hdrs = self._headers(config.create_content_type)
|
||||
logger.info("CREATE FG: POST %s name=%s", url, group_name.upper())
|
||||
@@ -552,6 +597,87 @@ class DdicMixin:
|
||||
)
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Function module remote-enable(bgRFC/RFC 执行体)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_function_module_xml(self, group: str, fm: str) -> str:
|
||||
"""GET 函数模块元数据 XML(含 processingType)。
|
||||
|
||||
Args:
|
||||
group: 函数组名。
|
||||
fm: 函数模块名。
|
||||
|
||||
Returns:
|
||||
FM 元数据 XML 字符串。
|
||||
"""
|
||||
obj_uri = get_type_config("function").format_obj_uri(
|
||||
fm.lower(), group=group.lower()
|
||||
)
|
||||
url = f"{self.host}{obj_uri}"
|
||||
hdrs = self._headers()
|
||||
hdrs["Accept"] = "application/vnd.sap.adt.functions.fmodules.v2+xml"
|
||||
logger.info("GET FM XML: GET %s", url)
|
||||
resp = self.session.get(url, headers=hdrs)
|
||||
logger.info("GET FM XML RESPONSE: HTTP %s", resp.status_code)
|
||||
if resp.status_code != 200:
|
||||
raise SapCliError(
|
||||
f"读取函数模块元数据失败: HTTP {resp.status_code} — {resp.text[:500]}"
|
||||
)
|
||||
return resp.text
|
||||
|
||||
def set_function_remote_enabled(
|
||||
self,
|
||||
group: str,
|
||||
fm: str,
|
||||
corr_nr: str | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""远程启用函数模块:GET 元数据 → ``processingType=rfc`` → PUT → GET 复核。
|
||||
|
||||
实测(NW 7.52)唯一可行方案:PUT 原 XML 且 ``processingType`` 取小写
|
||||
``rfc``(``remoteEnabled``/``remote``/``RFC`` 等值全 400);lockHandle 必须
|
||||
走 query 参数(放 header 报 ``ExceptionParameterNotFound``)。类本地类型参数的
|
||||
FM 会 500「针对 RFC 不允许使用类或接口的类型」,错误透传给调用方。
|
||||
复核后 ``TFDIR.FMODE='R'`` 的最终确认留给调用方 read-table(文档已写清)。
|
||||
|
||||
Returns:
|
||||
``(success, error_message)``。
|
||||
"""
|
||||
obj_uri = get_type_config("function").format_obj_uri(
|
||||
fm.lower(), group=group.lower()
|
||||
)
|
||||
# 1) 锁定
|
||||
lock_handle, _ = self.lock(obj_uri, corr_nr)
|
||||
try:
|
||||
# 2) GET 原 XML
|
||||
xml_text = self.get_function_module_xml(group, fm)
|
||||
# 3) 改 processingType=rfc
|
||||
new_xml = _set_processing_type(xml_text, "rfc")
|
||||
# 4) PUT(lockHandle 走 query 参数)
|
||||
url = f"{self.host}{obj_uri}"
|
||||
params: dict[str, str] = {"lockHandle": lock_handle}
|
||||
if corr_nr:
|
||||
params["corrNr"] = corr_nr
|
||||
hdrs = self._headers(
|
||||
"application/vnd.sap.adt.functions.fmodules.v2+xml"
|
||||
)
|
||||
hdrs["Accept"] = "*/*"
|
||||
logger.info("SET FM REMOTE-ENABLE: PUT %s", url)
|
||||
resp = self.session.put(
|
||||
url, headers=hdrs, params=params, data=new_xml.encode("utf-8")
|
||||
)
|
||||
logger.info("SET FM REMOTE-ENABLE RESPONSE: HTTP %s", resp.status_code)
|
||||
if resp.status_code >= 400:
|
||||
msg = resp.text[:500].strip() if resp.text else f"HTTP {resp.status_code}"
|
||||
return False, f"远程启用失败: HTTP {resp.status_code} — {msg}"
|
||||
# 5) GET 复核回显
|
||||
verify_xml = self.get_function_module_xml(group, fm)
|
||||
if _get_processing_type(verify_xml) != "rfc":
|
||||
return False, "远程启用失败: 复核回显 processingType 不是 rfc"
|
||||
return True, ""
|
||||
finally:
|
||||
self.unlock(obj_uri, lock_handle)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CDS View
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user