fix(ddic): create 命令消费 JSON description(--description 优先→JSON→回退名);upload testclasses XML 包裹通道(S4T 三轮实证);SKILL 错误速查表 +4 条 NW740 实证(ADT 创建不持久化文本/classes 激活空响应/unknown comments/STRG+comptype 陷阱)

This commit is contained in:
吴让宇
2026-09-14 10:37:43 +08:00
parent 43037e2379
commit 66ec30fc99
6 changed files with 72 additions and 4 deletions
+1
View File
@@ -192,6 +192,7 @@ CDS View:
upload_parser.add_argument("--type", required=True, choices=type_choices, help="对象类型")
upload_parser.add_argument("--path", required=True, help="本地源代码文件路径 (.abap)")
upload_parser.add_argument("--corr_nr", default=None, help="传输请求编号")
upload_parser.add_argument("--part", default=None, choices=["testclasses"], help="写入局部测试类 include(仅 class 类型,ABAP Unit 局部测试类通道)")
# ── syntax-check (远程对象语法检查,不上传不激活) ──
sc_parser = subparsers.add_parser("syntax-check", help="语法检查(远程对象,不上传不激活)")
+5
View File
@@ -919,6 +919,9 @@ class DdicMixin:
logger.info("RUN PROGRAM: POST %s", url)
resp = self.session.post(url, headers=hdrs)
logger.info("RUN PROGRAM RESPONSE: HTTP %s (%d bytes)", resp.status_code, len(resp.content))
if resp.status_code != 200:
# Log response body to aid diagnosing server-side dumps
logger.error("RUN PROGRAM ERROR BODY: %s", resp.text[:4000])
resp.raise_for_status()
return resp.text
@@ -962,6 +965,8 @@ class DdicMixin:
logger.info("RUN UNIT TEST: POST %s uri=%s", url, obj_uri)
resp = self.session.post(url, headers=hdrs, data=body.encode("utf-8"))
logger.info("RUN UNIT TEST RESPONSE: HTTP %s", resp.status_code)
if resp.status_code != 200 or b"<classes" not in resp.content:
logger.error("UNIT TEST BODY: %s", resp.text[:3000])
resp.raise_for_status()
return self._parse_aunit_result(resp.content)
+4 -2
View File
@@ -42,6 +42,7 @@ class SourceMixin:
source: str,
lock_handle: str,
corr_nr: str | None = None,
content_type: str | None = None,
) -> bool:
params: dict[str, str] = {}
if lock_handle:
@@ -49,8 +50,9 @@ class SourceMixin:
if corr_nr:
params["corrNr"] = corr_nr
url = f"{self.host}{src_uri}"
hdrs = self._headers("text/plain; charset=utf-8")
logger.info("SET SOURCE: PUT %s (%d chars)", url, len(source))
ct = content_type or "text/plain; charset=utf-8"
hdrs = self._headers(ct)
logger.info("SET SOURCE: PUT %s (%d chars, ct=%s)", url, len(source), ct)
resp = self.session.put(
url, headers=hdrs, params=params, data=source.encode("utf-8")
)
+21
View File
@@ -859,6 +859,27 @@ def cmd_create(args: argparse.Namespace, client: ADTClient) -> None:
source_file: str | None = getattr(args, "source", None)
definition_file: str | None = getattr(args, "definition", None)
# JSON definition description: --description wins, then JSON, then name.
# (NW740 ADT create does not persist texts anyway — see SKILL.md known
# limitation; this only affects the XML body sent at creation time.)
if (
definition_file
and not getattr(args, "description", None)
and obj_type in DDIC_TYPES
):
import json as _json
import os as _os
if _os.path.isfile(definition_file):
try:
with open(definition_file, "r", encoding="utf-8") as f:
_data = _json.load(f)
_json_desc = _data.get("description")
if _json_desc:
description = _json_desc
except Exception as _e:
logger.warning("读取定义文件 description 失败: %s", _e)
type_label = get_type_config(obj_type).label
if obj_type == "function":
+35 -1
View File
@@ -33,6 +33,7 @@ def cmd_upload(args: argparse.Namespace, client: ADTClient) -> None:
obj_type: str = args.type
file_path: str = args.path
corr_nr: str | None = getattr(args, "corr_nr", None)
part: str | None = getattr(args, "part", None)
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
@@ -42,6 +43,16 @@ def cmd_upload(args: argparse.Namespace, client: ADTClient) -> None:
print(f" functiongroup 等是容器对象,不包含可编辑的源代码文件")
raise InvalidNameError(f"{type_label}没有源代码,不支持 upload 操作")
src_uri = parsed.src_uri
if part == "testclasses":
# Local test classes include (ABAP Unit): classes only.
# ADT unit testruns only discovers LOCAL test classes — global
# test classes are NOT run by the endpoint (S4T proven).
if obj_type != "class":
print(f"\n ✗ --part testclasses 仅支持 class 类型")
raise InvalidNameError("--part testclasses 仅支持 class 类型")
src_uri = f"/sap/bc/adt/oo/classes/{name.lower()}/source/testclasses"
if not os.path.isfile(file_path):
print(f"\n ✗ 文件不存在: {file_path}")
raise ConfigError(f"文件不存在: {file_path}")
@@ -90,7 +101,30 @@ def cmd_upload(args: argparse.Namespace, client: ADTClient) -> None:
print(f" ✓ 锁定成功(传输请求: {corr_nr}")
try:
client.set_source(parsed.src_uri, source, lock_handle, corr_nr)
if part == "testclasses":
# testclasses endpoint expects XML body: <abapClass> element
# in the oo/classes namespace wrapping the raw source.
from xml.sax.saxutils import escape as _esc
xml_body = (
'<?xml version="1.0" encoding="UTF-8"?>'
'<cls:abapClass xmlns:cls="http://www.sap.com/adt/oo/classes" '
'xmlns:adtcore="http://www.sap.com/adt/core" '
'adtcore:type="testclassInclude" '
'adtcore:description="Local test classes" '
'adtcore:name="zint_cl_route_engine">'
f"<cls:source>{_esc(source)}</cls:source>"
"</cls:abapClass>"
)
client.set_source(
src_uri,
xml_body,
lock_handle,
corr_nr,
content_type="application/vnd.sap.adt.abap.classes+xml; charset=utf-8",
)
else:
client.set_source(src_uri, source, lock_handle, corr_nr)
print(f" ✓ 源代码写入成功")
finally:
client.unlock(parsed.obj_uri, lock_handle)