Files
sap-cli-skill/tests/unit/test_commands_extra.py
T
吴让宇 86519c51d6 fix(info): 406 回退值改为 */* —— 原值 application/xml 本身也返回 406
`info` 在 NW 7.40 上对 DDIC 类型恒报「查询失败: HTTP 406」,实测根因:
回退分支把 Accept 从专属媒体类型换成 `application/xml`,而该值在本系统同样返回 406,
等于没回退。真实客户端 `_headers()` 的 Accept 默认值本就是 `*/*`,此处相当于
把可用默认值覆盖成了失效值。

实测(S4T / NW 7.40,structure ZMM_BIP_001_HEADER):
  application/vnd.sap.adt.ddic.structures.v1|v2|v3+xml → 406
  application/xml                                      → 406
  */*                                                  → 200
修后真机验证:`info --name ZMM_BIP_001_HEADER --type structure` 查询成功。

已知限制(非本修复引入):NW 7.40 的 DDIC 端点在 */* 下只返回 blueSource,
不含 description/packageRef,故 structure/tabletype 等类型的「描述/开发包/负责人」
为空。需这些字段时用 read-table 查 DD02T(描述)/ TADIR(开发包)。
(objectproperties 端点在本系统返回 422,不可用。)

测试:
- 新增 test_info_406_fallback_accept_is_wildcard —— 断言回退 Accept 必须为 */*,
  且明确排除 application/xml。**必须用 side_effect 在请求发生时快照 headers**:
  cmd_info 重试时复用同一个 hdrs dict,直接读 call_args_list[i].kwargs["headers"]
  会因别名而全部显示最后一次赋的值,令该断言永远无法成立。
- 三个测试文件的 _make_client() 由 `_headers.return_value={...}`(共享 dict)
  改为 side_effect 返回新 dict,与真实实现一致——原写法会让被记录的 headers
  全部指向同一对象,使 header 断言失真。
- 反向验证:把回退改回 application/xml → 测试报红 AssertionError('application/xml' != '*/*')。

699 tests OK。
2026-09-11 00:53:02 +08:00

1939 lines
74 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""commands/ 模块深入单元测试 — 覆盖失败/边界场景。
运行: python tests/unit/test_commands_extra.py
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import tempfile
import unittest
from unittest.mock import MagicMock, patch, mock_open
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "assets"))
from sapcli.client import ADTClient
# ── helpers ──────────────────────────────────────────────────────
def _mock_resp(status_code=200, text="", content=b"", headers=None):
r = MagicMock()
r.status_code = status_code
r.text = text
r.content = content
r.headers = headers or {}
r.raise_for_status = MagicMock()
return r
def _make_client():
"""创建完全 mock 的 ADTClient。"""
client = MagicMock(spec=ADTClient)
client.host = "https://sap.example.com"
client.sap_client = "100"
client.csrf_token = "test-csrf-token"
client.user = "TESTUSER"
client.session = MagicMock()
# 真实 _headers() 每次返回**新** dict。用 return_value 会让所有被记录的
# kwargs["headers"] 指向同一对象——后续赋值会覆盖历史值,令 header 断言失真
# (实测:406 回退的 Accept 断言曾被此别名效应蒙过)。
client._headers.side_effect = lambda content_type="application/xml": {
"content-type": content_type,
"x-csrf-token": "test-csrf-token",
"Accept": "*/*",
}
return client
def _args(**kwargs):
defaults = {"type": "report", "name": "ZTEST", "path": ".", "config": None}
defaults.update(kwargs)
return argparse.Namespace(**defaults)
# 用于 cmd_info 的 XML 响应
_INFO_XML = (
b'<?xml version="1.0"?>'
b'<adtcore:object xmlns:adtcore="http://www.sap.com/adt/core"'
b' adtcore:name="ZTEST" adtcore:type="PROG/P"'
b' adtcore:version="active" adtcore:description="Test report"'
b' adtcore:masterLanguage="EN"/>')
# ═══════════════════════════════════════════════════════════════
# crud.py — cmd_download
# ═══════════════════════════════════════════════════════════════
class TestCmdDownloadObjectNotFound(unittest.TestCase):
"""cmd_download 对象不存在时报错。"""
def test_object_not_found_raises(self):
from sapcli.commands.crud import cmd_download
from sapcli.exceptions import ObjectNotFoundError
client = _make_client()
client.object_exists.return_value = False
with tempfile.TemporaryDirectory() as td:
with self.assertRaises(ObjectNotFoundError) as ctx:
cmd_download(_args(path=td), client)
self.assertIn("ZTEST", str(ctx.exception))
def test_object_not_found_prints_hint(self):
from sapcli.commands.crud import cmd_download
from sapcli.exceptions import ObjectNotFoundError
client = _make_client()
client.object_exists.return_value = False
with tempfile.TemporaryDirectory() as td:
with patch("builtins.print") as mock_print:
with self.assertRaises(ObjectNotFoundError):
cmd_download(_args(path=td), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("不存在", printed)
class TestCmdDownloadFileWriteFailure(unittest.TestCase):
"""cmd_download 文件写入失败(权限/路径不可写)。"""
def test_write_permission_error(self):
from sapcli.commands.crud import cmd_download
client = _make_client()
client.object_exists.return_value = True
client.get_source.return_value = "REPORT ztest."
# 令 open() 抛出 PermissionError
with patch("builtins.open", side_effect=PermissionError("拒绝访问")):
with self.assertRaises(PermissionError):
cmd_download(_args(path="/tmp/some_dir"), client)
# ═══════════════════════════════════════════════════════════════
# crud.py — cmd_create
# ═══════════════════════════════════════════════════════════════
class TestCmdCreateWithSourceFile(unittest.TestCase):
"""cmd_create 有 --source 文件时。"""
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
@patch("sapcli.commands.crud.Manifest")
def test_create_reads_source_file(self, MockManifest, mock_transport):
from sapcli.commands.crud import cmd_create
client = _make_client()
client.object_exists.return_value = False
client.create_object.return_value = ("/uri/ztest", "/uri/ztest/source/main")
source_code = "REPORT ztest.\nWRITE: / 'hello'."
with tempfile.NamedTemporaryFile(
mode="w", suffix=".abap", delete=False, encoding="utf-8"
) as f:
f.write(source_code)
tmpfile = f.name
try:
cmd_create(
_args(source=tmpfile, description="Test", package="$TMP", corr_nr=None),
client,
)
# create_object 的 source 参数应该来自文件内容
call_args = client.create_object.call_args
self.assertEqual(call_args[0][4], source_code)
finally:
os.unlink(tmpfile)
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
@patch("sapcli.commands.crud.Manifest")
def test_create_with_class_source(self, MockManifest, mock_transport):
from sapcli.commands.crud import cmd_create
client = _make_client()
client.object_exists.return_value = False
client.create_object.return_value = ("/uri/zcl_test", "/uri/zcl_test/source/main")
source_code = "CLASS zcl_test DEFINITION PUBLIC. ENDCLASS."
with tempfile.NamedTemporaryFile(
mode="w", suffix=".abap", delete=False, encoding="utf-8"
) as f:
f.write(source_code)
tmpfile = f.name
try:
cmd_create(
_args(
type="class",
name="ZCL_TEST",
source=tmpfile,
description="Test Class",
package="$TMP",
corr_nr=None,
),
client,
)
call_args = client.create_object.call_args
self.assertEqual(call_args[0][4], source_code)
finally:
os.unlink(tmpfile)
class TestCmdCreateWithDefinitionFile(unittest.TestCase):
"""cmd_create 有 --definition 文件时(DDIC 类型)。"""
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
@patch("sapcli.commands.crud.Manifest")
def test_create_domain_with_definition(self, MockManifest, mock_transport):
from sapcli.commands.crud import cmd_create
client = _make_client()
client.object_exists.return_value = False
client.create_ddic_object.return_value = ("/uri/ztest_dom", "/uri/ztest_dom/source/main")
# 创建一个临时 JSON 定义文件
definition = {"datatype": "CHAR", "length": 10, "decimals": 0}
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, encoding="utf-8"
) as f:
json.dump(definition, f)
def_file = f.name
try:
cmd_create(
_args(
type="domain",
name="ZTEST_DOM",
source=None,
definition=def_file,
description="Test Domain",
package="$TMP",
corr_nr=None,
),
client,
)
client.create_ddic_object.assert_called_once()
finally:
os.unlink(def_file)
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
@patch("sapcli.commands.crud.Manifest")
def test_create_dataelement_with_definition(self, MockManifest, mock_transport):
from sapcli.commands.crud import cmd_create
client = _make_client()
client.object_exists.return_value = False
client.create_ddic_object.return_value = ("/uri/ztest_de", "/uri/ztest_de/source/main")
definition = {"datatype": "CHAR", "length": 20, "domain_name": "ZTEST_DOM"}
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, encoding="utf-8"
) as f:
json.dump(definition, f)
def_file = f.name
try:
cmd_create(
_args(
type="dataelement",
name="ZTEST_DE",
source=None,
definition=def_file,
description="Test DE",
package="$TMP",
corr_nr=None,
),
client,
)
client.create_ddic_object.assert_called_once()
finally:
os.unlink(def_file)
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
@patch("sapcli.commands.crud.Manifest")
def test_create_table_with_definition(self, MockManifest, mock_transport):
from sapcli.commands.crud import cmd_create
client = _make_client()
client.object_exists.return_value = False
client.create_ddic_object.return_value = ("/uri/ztest_tab", "/uri/ztest_tab/source/main")
definition = {
"fields": [
{"name": "KEY_FIELD", "type": "char10", "key": True, "not_null": True},
{"name": "VALUE", "type": "char20"},
],
"table_category": "#TRANSPARENT",
"delivery_class": "#A",
}
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, encoding="utf-8"
) as f:
json.dump(definition, f)
def_file = f.name
try:
cmd_create(
_args(
type="table",
name="ZTEST_TAB",
source=None,
definition=def_file,
description="Test Table",
package="$TMP",
corr_nr=None,
),
client,
)
client.create_ddic_object.assert_called_once()
finally:
os.unlink(def_file)
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
def test_create_tabletype_requires_definition(self, mock_transport):
"""tabletype 没有 definition 时应报错。"""
from sapcli.commands.crud import cmd_create
from sapcli.exceptions import CreateError
client = _make_client()
client.object_exists.return_value = False
with self.assertRaises(CreateError):
cmd_create(
_args(
type="tabletype",
name="ZTEST_TT",
source=None,
definition=None,
description="Test TT",
package="$TMP",
corr_nr=None,
),
client,
)
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
@patch("sapcli.commands.crud.Manifest")
def test_create_functiongroup_success(self, MockManifest, mock_transport):
from sapcli.commands.crud import cmd_create
client = _make_client()
client.function_group_exists.return_value = False
client.create_function_group.return_value = "/uri/ztest_fg"
cmd_create(
_args(
type="functiongroup",
name="ZTEST_FG",
source=None,
description="Test FG",
package="$TMP",
corr_nr=None,
),
client,
)
client.create_function_group.assert_called_once()
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
def test_create_functiongroup_already_exists(self, mock_transport):
from sapcli.commands.crud import cmd_create
from sapcli.exceptions import ObjectAlreadyExistsError
client = _make_client()
client.function_group_exists.return_value = True
with self.assertRaises(ObjectAlreadyExistsError):
cmd_create(
_args(
type="functiongroup",
name="ZTEST_FG",
source=None,
description="Test FG",
package="$TMP",
corr_nr=None,
),
client,
)
# ═══════════════════════════════════════════════════════════════
# crud.py — cmd_info
# ═══════════════════════════════════════════════════════════════
class TestCmdInfoObjectNotFound(unittest.TestCase):
"""cmd_info 对象不存在(HTTP 404)。"""
def test_info_404_raises(self):
from sapcli.commands.crud import cmd_info
from sapcli.exceptions import ObjectNotFoundError
client = _make_client()
client.session.get.return_value = _mock_resp(404)
with self.assertRaises(ObjectNotFoundError) as ctx:
cmd_info(_args(), client)
self.assertIn("ZTEST", str(ctx.exception))
def test_info_500_raises(self):
from sapcli.commands.crud import cmd_info
from sapcli.exceptions import SapCliError
client = _make_client()
client.session.get.return_value = _mock_resp(500, text="Internal Server Error")
with self.assertRaises(SapCliError) as ctx:
cmd_info(_args(), client)
self.assertIn("500", str(ctx.exception))
def test_info_name_mismatch(self):
"""返回的名称与请求名称不匹配。"""
from sapcli.commands.crud import cmd_info
from sapcli.exceptions import SapCliError
mismatched_xml = (
b'<?xml version="1.0"?>'
b'<adtcore:object xmlns:adtcore="http://www.sap.com/adt/core"'
b' adtcore:name="OTHER_NAME" adtcore:type="PROG/P"'
b' adtcore:version="active" adtcore:description="Wrong"/>'
)
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=mismatched_xml)
with self.assertRaises(SapCliError) as ctx:
cmd_info(_args(), client)
self.assertIn("不匹配", str(ctx.exception))
def test_info_http_406_fallback(self):
"""HTTP 406 时回退到 */*。
只断言「重试了一次」会漏掉回退值本身无效的情况——实测 NW 7.40 上
application/xml 同样返回 406,回退恒败。故必须断言回退用的 Accept 值。
"""
from sapcli.commands.crud import cmd_info
client = _make_client()
# 第一次 406,第二次 200
client.session.get.side_effect = [
_mock_resp(406),
_mock_resp(200, content=_INFO_XML),
]
cmd_info(_args(), client)
self.assertEqual(client.session.get.call_count, 2)
def test_info_406_fallback_accept_is_wildcard(self):
"""回退请求的 Accept 必须是 */*(NW 7.40 上唯一可用值)。
实测(S4T / NW 7.40):structure 专属头 → 406application/xml → 406*/* → 200。
即 application/xml 本身也 406,用它做回退等于不重试。
⚠️ 断言必须在**请求发生时快照** headerscmd_info 重试时复用同一个 hdrs dict
直接读 call_args_list[i].kwargs["headers"] 会因别名而全部显示最后一次赋的值,
使「回退换了头」这类断言永远无法成立(也无法发现回退值无效)。
"""
from sapcli.commands.crud import cmd_info
client = _make_client()
seen = []
def _get(url, headers=None, **kw):
seen.append(dict(headers or {})) # 快照,断开别名
return _mock_resp(406) if len(seen) == 1 else _mock_resp(200, content=_INFO_XML)
client.session.get.side_effect = _get
with patch("builtins.print"):
cmd_info(_args(), client)
self.assertEqual(len(seen), 2, "应为「原始请求 + 回退请求」两次")
self.assertEqual(seen[1]["Accept"], "*/*")
self.assertNotEqual(seen[1]["Accept"], "application/xml",
"application/xml 在 NW 7.40 上同样返回 406,不能作为回退值")
self.assertNotEqual(seen[0]["Accept"], seen[1]["Accept"],
"回退请求必须换用不同的 Accept 头")
# ═══════════════════════════════════════════════════════════════
# crud.py — cmd_info — 传输请求号字段(纯只读查 E071)
# ═══════════════════════════════════════════════════════════════
class TestCmdInfoTransport(unittest.TestCase):
"""cmd_info 传输请求号字段。"""
@staticmethod
def _xml(name="ZTEST", type_code="PROG/P"):
return (
b'<?xml version="1.0"?>'
b'<adtcore:object xmlns:adtcore="http://www.sap.com/adt/core"'
b' adtcore:name="' + name.encode() + b'" adtcore:type="' + type_code.encode() + b'"'
b' adtcore:version="active" adtcore:description="d"/>'
)
def test_request_found(self):
from sapcli.commands.crud import cmd_info
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=self._xml())
client.query_table_data.return_value = {
"rows": [["DEVK901362"], ["DEVK901400"]]
}
with patch("builtins.print") as mock_print:
cmd_info(_args(), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("DEVK901362", printed)
self.assertIn("DEVK901400", printed)
def test_no_request_shows_none(self):
from sapcli.commands.crud import cmd_info
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=self._xml())
client.query_table_data.return_value = {"rows": []}
with patch("builtins.print") as mock_print:
cmd_info(_args(), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("传输请求: 无", printed)
def test_query_failure_graceful(self):
from sapcli.commands.crud import cmd_info
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=self._xml())
client.query_table_data.side_effect = Exception("boom")
# 不应抛异常
with patch("builtins.print") as mock_print:
cmd_info(_args(), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("(无法获取)", printed)
def test_non_r3tr_type_skips_query(self):
"""include 非独立 R3TR 对象,不查 E071。"""
from sapcli.commands.crud import cmd_info
client = _make_client()
client.session.get.return_value = _mock_resp(
200, content=self._xml(type_code="PROG/I")
)
with patch("builtins.print") as mock_print:
cmd_info(_args(type="include", name="ZTEST"), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("非独立传输对象", printed)
client.query_table_data.assert_not_called()
# ═══════════════════════════════════════════════════════════════
# crud.py — cmd_list(来自 search.py
# ═══════════════════════════════════════════════════════════════
class TestCmdListNoResults(unittest.TestCase):
"""cmd_list 无结果。"""
def test_list_empty_results(self):
from sapcli.commands.search import cmd_list
client = _make_client()
client.list_objects.return_value = []
with patch("builtins.print") as mock_print:
cmd_list(argparse.Namespace(type="report", package=None, prefix=None, config=None), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("未找到", printed)
def test_list_api_error(self):
from sapcli.commands.search import cmd_list
client = _make_client()
client.list_objects.side_effect = Exception("Network error")
# 应不抛异常,只打印错误
with patch("builtins.print") as mock_print:
cmd_list(argparse.Namespace(type="report", package=None, prefix=None, config=None), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_list_with_results(self):
from sapcli.commands.search import cmd_list
client = _make_client()
client.list_objects.return_value = [
{"name": "Z_PROG1", "type": "PROG/P", "package": "$TMP", "description": "Test 1"},
{"name": "Z_PROG2", "type": "PROG/P", "package": "$TMP", "description": "Test 2"},
]
with patch("builtins.print") as mock_print:
cmd_list(argparse.Namespace(type="report", package=None, prefix=None, config=None), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("2", printed)
def test_list_with_package_filter(self):
from sapcli.commands.search import cmd_list
client = _make_client()
client.list_objects.return_value = []
cmd_list(
argparse.Namespace(type="report", package="Z_MY_PKG", prefix=None, config=None),
client,
)
# 检查 list_objects 被调用时包含 package 参数
call_kwargs = client.list_objects.call_args[1]
self.assertEqual(call_kwargs.get("package"), "Z_MY_PKG")
# ═══════════════════════════════════════════════════════════════
# crud.py — cmd_whereused(来自 search.py
# ═══════════════════════════════════════════════════════════════
class TestCmdWhereUsedNoReferences(unittest.TestCase):
"""cmd_whereused 无引用。"""
def test_whereused_empty(self):
from sapcli.commands.search import cmd_whereused
client = _make_client()
client.where_used.return_value = []
with patch("builtins.print") as mock_print:
cmd_whereused(_args(), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("未找到引用", printed)
def test_whereused_api_error(self):
from sapcli.commands.search import cmd_whereused
client = _make_client()
client.where_used.side_effect = Exception("Connection refused")
with patch("builtins.print") as mock_print:
cmd_whereused(_args(), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_whereused_with_results(self):
from sapcli.commands.search import cmd_whereused
client = _make_client()
client.where_used.return_value = [
{"name": "Z_USER1", "type": "PROG/P", "package": "$TMP", "uri": "/prog/z_user1"},
]
with patch("builtins.print") as mock_print:
cmd_whereused(_args(), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("1", printed)
# ═══════════════════════════════════════════════════════════════
# crud.py — cmd_search(来自 search.py
# ═══════════════════════════════════════════════════════════════
class TestCmdSearchNoResults(unittest.TestCase):
"""cmd_search 无结果。"""
def test_search_empty(self):
from sapcli.commands.search import cmd_search
client = _make_client()
client.search_code.return_value = []
with patch("builtins.print") as mock_print:
cmd_search(argparse.Namespace(query="SELECT", type=None, config=None), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("未找到", printed)
def test_search_api_error(self):
from sapcli.commands.search import cmd_search
client = _make_client()
client.search_code.side_effect = Exception("Timeout")
with patch("builtins.print") as mock_print:
cmd_search(argparse.Namespace(query="SELECT", type=None, config=None), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_search_with_type_filter(self):
from sapcli.commands.search import cmd_search
client = _make_client()
client.search_code.return_value = [
{"name": "Z_RESULT", "type": "PROG/P", "description": "found"},
]
cmd_search(
argparse.Namespace(query="WRITE", type="report", config=None),
client,
)
client.search_code.assert_called_once()
# search_code 被调用时传入了 obj_type 参数
call_args = client.search_code.call_args
# 它是 (query, obj_type=adt_type) 形式
obj_type_arg = call_args[1].get("obj_type")
if obj_type_arg is None and len(call_args[0]) > 1:
obj_type_arg = call_args[0][1]
self.assertIsNotNone(obj_type_arg)
# ═══════════════════════════════════════════════════════════════
# crud.py — cmd_format(来自 quality.py
# ═══════════════════════════════════════════════════════════════
class TestCmdFormatSuccess(unittest.TestCase):
"""cmd_format 格式化成功(完整流程)。"""
def test_format_full_success(self):
from sapcli.commands.quality import cmd_format
client = _make_client()
client.get_source.return_value = "REPORT ztest.\nWRITE: / 'hello'."
client.pretty_print.return_value = "REPORT ztest.\n WRITE: / 'hello'."
client.lock.return_value = ("lh_1", "DEVK001")
client.set_source.return_value = True
client.unlock.return_value = True
client.activate.return_value = (True, [])
with patch("builtins.print") as mock_print:
cmd_format(
argparse.Namespace(name="ZTEST", type="report", path=".", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("格式化完成", printed)
client.pretty_print.assert_called_once()
client.set_source.assert_called_once()
client.activate.assert_called_once()
def test_format_no_change(self):
"""格式化结果与原始一致,不写回。"""
from sapcli.commands.quality import cmd_format
client = _make_client()
original = "REPORT ztest.\nWRITE: / 'hello'."
client.get_source.return_value = original
client.pretty_print.return_value = original # 无变化
with patch("builtins.print") as mock_print:
cmd_format(
argparse.Namespace(name="ZTEST", type="report", path=".", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("无需修改", printed)
client.set_source.assert_not_called()
def test_format_get_source_fails(self):
from sapcli.commands.quality import cmd_format
client = _make_client()
client.get_source.side_effect = Exception("Read error")
with patch("builtins.print") as mock_print:
cmd_format(
argparse.Namespace(name="ZTEST", type="report", path=".", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_format_unsupported_type(self):
"""functiongroup 没有 src_uri,应提前返回。"""
from sapcli.commands.quality import cmd_format
client = _make_client()
with patch("builtins.print") as mock_print:
cmd_format(
argparse.Namespace(name="ZTEST_FG", type="functiongroup", path=".", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("不支持", printed)
client.get_source.assert_not_called()
# ═══════════════════════════════════════════════════════════════
# cds.py — cds download
# ═══════════════════════════════════════════════════════════════
class TestCdsDownload(unittest.TestCase):
"""cds download 成功。"""
def test_cds_download_success(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
ddl_source = "define view z_test as select from sflight { carrid, connid, fldate };"
client.get_cds_source.return_value = ddl_source
with tempfile.TemporaryDirectory() as td:
cmd_cds(
argparse.Namespace(cds_action="download", name="Z_TEST", path=td, config=None),
client,
)
expected_file = os.path.join(td, "z_test.ddl")
self.assertTrue(os.path.isfile(expected_file))
with open(expected_file, "r", encoding="utf-8") as f:
content = f.read()
self.assertEqual(content, ddl_source)
def test_cds_download_api_error(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
client.get_cds_source.side_effect = Exception("Not found")
with patch("builtins.print") as mock_print:
cmd_cds(
argparse.Namespace(cds_action="download", name="Z_MISSING", path=".", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_cds_download_creates_dir(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
client.get_cds_source.return_value = "define view z_test as select from mara { matnr };"
with tempfile.TemporaryDirectory() as td:
subdir = os.path.join(td, "new_dir")
cmd_cds(
argparse.Namespace(cds_action="download", name="Z_TEST", path=subdir, config=None),
client,
)
self.assertTrue(os.path.isfile(os.path.join(subdir, "z_test.ddl")))
# ═══════════════════════════════════════════════════════════════
# cds.py — cds create
# ═══════════════════════════════════════════════════════════════
class TestCdsCreate(unittest.TestCase):
"""cds create 有 ddl_path 时。"""
def test_cds_create_with_ddl_file(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
client.create_cds.return_value = ("/uri/z_test", "/uri/z_test/source/main")
ddl_content = "@AbapCatalog.sqlViewName: 'ZTEST'\ndefine view z_test as select from sflight { carrid };"
with tempfile.NamedTemporaryFile(
mode="w", suffix=".ddl", delete=False, encoding="utf-8"
) as f:
f.write(ddl_content)
ddl_file = f.name
try:
with tempfile.TemporaryDirectory() as td:
cmd_cds(
argparse.Namespace(
cds_action="create",
name="Z_TEST",
description="Test CDS",
ddl_path=ddl_file,
path=td,
config=None,
),
client,
)
client.create_cds.assert_called_once()
# 验证 CDS 创建时传入了文件内容
call_args = client.create_cds.call_args
self.assertIn(ddl_content, call_args[0])
finally:
os.unlink(ddl_file)
def test_cds_create_default_template(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
client.create_cds.return_value = ("/uri/z_test", "/uri/z_test/source/main")
with tempfile.TemporaryDirectory() as td:
cmd_cds(
argparse.Namespace(
cds_action="create",
name="Z_TEST",
description="Test CDS",
ddl_path=None,
path=td,
config=None,
),
client,
)
client.create_cds.assert_called_once()
# 检查默认模板被使用
call_args = client.create_cds.call_args
ddl_arg = call_args[0][2]
self.assertIn("define view", ddl_arg)
def test_cds_create_failure(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
client.create_cds.side_effect = Exception("Already exists")
with patch("builtins.print") as mock_print:
cmd_cds(
argparse.Namespace(
cds_action="create",
name="Z_DUP",
description="Duplicate",
ddl_path=None,
path=".",
config=None,
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
# ═══════════════════════════════════════════════════════════════
# cds.py — cds sync
# ═══════════════════════════════════════════════════════════════
class TestCdsSync(unittest.TestCase):
"""cds sync 成功。"""
def test_cds_sync_existing_object(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
client.object_exists.return_value = True
client.lock.return_value = ("lh_1", "DEVK001")
client.set_source.return_value = True
client.unlock.return_value = True
client.activate.return_value = (True, [])
ddl_content = "define view z_test as select from sflight { carrid };"
with tempfile.NamedTemporaryFile(
mode="w", suffix=".ddl", delete=False, encoding="utf-8"
) as f:
f.write(ddl_content)
ddl_file = f.name
try:
with patch("builtins.print") as mock_print:
cmd_cds(
argparse.Namespace(
cds_action="sync",
name="Z_TEST",
path=ddl_file,
config=None,
),
client,
)
client.lock.assert_called_once()
client.set_source.assert_called_once()
client.activate.assert_called_once()
finally:
os.unlink(ddl_file)
def test_cds_sync_nonexistent_object_creates(self):
"""CDS 不存在时自动创建。"""
from sapcli.commands.cds import cmd_cds
client = _make_client()
client.object_exists.return_value = False
client.create_cds.return_value = ("/uri/z_test", "/uri/z_test/source/main")
ddl_content = "define view z_test as select from sflight { carrid };"
with tempfile.NamedTemporaryFile(
mode="w", suffix=".ddl", delete=False, encoding="utf-8"
) as f:
f.write(ddl_content)
ddl_file = f.name
try:
with patch("builtins.print") as mock_print:
cmd_cds(
argparse.Namespace(
cds_action="sync",
name="Z_TEST",
path=ddl_file,
config=None,
),
client,
)
client.create_cds.assert_called_once()
finally:
os.unlink(ddl_file)
def test_cds_sync_ddl_file_not_found(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
with patch("builtins.print") as mock_print:
cmd_cds(
argparse.Namespace(
cds_action="sync",
name="Z_TEST",
path="/nonexistent/file.ddl",
config=None,
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("不存在", printed)
def test_cds_sync_activation_failure(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
client.object_exists.return_value = True
client.lock.return_value = ("lh_1", "DEVK001")
client.set_source.return_value = True
client.unlock.return_value = True
client.activate.return_value = (
False,
[{"type": "E", "line": "5", "text": "Syntax error"}],
)
ddl_content = "define view z_test as select from sflight { invalid_field };"
with tempfile.NamedTemporaryFile(
mode="w", suffix=".ddl", delete=False, encoding="utf-8"
) as f:
f.write(ddl_content)
ddl_file = f.name
try:
with patch("builtins.print") as mock_print:
cmd_cds(
argparse.Namespace(
cds_action="sync",
name="Z_TEST",
path=ddl_file,
config=None,
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("激活失败", printed)
finally:
os.unlink(ddl_file)
def test_cds_unknown_action(self):
from sapcli.commands.cds import cmd_cds
client = _make_client()
with patch("builtins.print") as mock_print:
cmd_cds(
argparse.Namespace(cds_action=None, name="Z_TEST", path=".", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("用法", printed)
# ═══════════════════════════════════════════════════════════════
# transport.py — transport list
# ═══════════════════════════════════════════════════════════════
class TestTransportList(unittest.TestCase):
"""transport list 成功。"""
def test_transport_list_with_requests(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.list_transport_requests.return_value = [
{"number": "DEVK001", "description": "Dev request 1", "owner": "USER1"},
{"number": "DEVK002", "description": "Dev request 2", "owner": "USER2"},
]
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(transport_action="list", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("2", printed)
self.assertIn("DEVK001", printed)
def test_transport_list_empty(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.list_transport_requests.return_value = []
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(transport_action="list", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("未找到", printed)
def test_transport_list_api_error(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.list_transport_requests.side_effect = Exception("Connection failed")
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(transport_action="list", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
# ═══════════════════════════════════════════════════════════════
# transport.py — transport info
# ═══════════════════════════════════════════════════════════════
class TestTransportInfo(unittest.TestCase):
"""transport info 成功。"""
def test_transport_info_success(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.transport_info.return_value = {
"number": "DEVK001",
"description": "Transport for feature X",
"status": "D",
"owner": "TESTUSER",
}
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(
transport_action="info", corr_nr="DEVK001", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("feature X", printed)
self.assertIn("TESTUSER", printed)
client.transport_info.assert_called_once_with("DEVK001")
def test_transport_info_api_error(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.transport_info.side_effect = Exception("Not found")
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(
transport_action="info", corr_nr="DEVK999", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
# ═══════════════════════════════════════════════════════════════
# transport.py — transport release
# ═══════════════════════════════════════════════════════════════
class TestTransportRelease(unittest.TestCase):
"""transport release 成功。"""
def test_transport_release_success(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.transport_release.return_value = True
with patch("builtins.input", return_value="yes"):
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(
transport_action="release", corr_nr="DEVK001", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("成功释放", printed)
def test_transport_release_cancelled(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.transport_release.return_value = True
with patch("builtins.input", return_value="no"):
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(
transport_action="release", corr_nr="DEVK001", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("取消", printed)
client.transport_release.assert_not_called()
def test_transport_release_failure(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.transport_release.return_value = False
with patch("builtins.input", return_value="yes"):
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(
transport_action="release", corr_nr="DEVK001", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_transport_release_api_error(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.transport_release.side_effect = Exception("Timeout")
with patch("builtins.input", return_value="yes"):
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(
transport_action="release", corr_nr="DEVK001", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
# ═══════════════════════════════════════════════════════════════
# transport.py — transport objects
# ═══════════════════════════════════════════════════════════════
class TestTransportObjects(unittest.TestCase):
"""transport objects 成功。"""
def test_transport_objects_with_results(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.transport_objects.return_value = [
{"name": "ZTEST_REPORT", "type": "PROG/P"},
{"name": "ZCL_MY_CLASS", "type": "CLAS/OC"},
]
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(
transport_action="objects", corr_nr="DEVK001", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("2", printed)
self.assertIn("ZTEST_REPORT", printed)
def test_transport_objects_empty(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.transport_objects.return_value = []
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(
transport_action="objects", corr_nr="DEVK001", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("无对象", printed)
def test_transport_objects_api_error(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.transport_objects.side_effect = Exception("Server error")
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(
transport_action="objects", corr_nr="DEVK001", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_transport_unknown_action(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(transport_action=None, config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("用法", printed)
# ═══════════════════════════════════════════════════════════════
# package_cmd.py — package create
# ═══════════════════════════════════════════════════════════════
class TestTransportCreate(unittest.TestCase):
"""transport create — 新建传输请求。
历史背景:该子命令曾只存在于分发副本(源码 v2.1 重构时未纳入),
刷新分发仓前先回迁到源码,避免同步时功能静默消失。
"""
def test_transport_create_success(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.create_transport_request.return_value = "DEVK900123"
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(transport_action="create",
description="CEM BIP 传输请求", config=None),
client,
)
client.create_transport_request.assert_called_once_with("CEM BIP 传输请求")
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("DEVK900123", printed)
def test_transport_create_client_error(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.create_transport_request.side_effect = Exception("HTTP 403")
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(transport_action="create",
description="X", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_transport_create_empty_number(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
client.create_transport_request.return_value = ""
with patch("builtins.print") as mock_print:
cmd_transport(
argparse.Namespace(transport_action="create",
description="X", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_transport_usage_lists_create(self):
from sapcli.commands.transport import cmd_transport
client = _make_client()
with patch("builtins.print") as mock_print:
cmd_transport(argparse.Namespace(transport_action=None, config=None), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("create", printed)
class TestPackageCreate(unittest.TestCase):
"""package create 成功。"""
def test_package_create_success(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.create_package.return_value = True
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(
package_action="create",
name="Z_TEST_PKG",
description="Test Package",
superpackage=None,
config=None,
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("创建成功", printed)
client.create_package.assert_called_once_with("Z_TEST_PKG", "Test Package", None)
def test_package_create_failure(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.create_package.return_value = False
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(
package_action="create",
name="Z_DUP_PKG",
description="Duplicate",
superpackage=None,
config=None,
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_package_create_api_error(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.create_package.side_effect = Exception("Auth failed")
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(
package_action="create",
name="Z_ERR_PKG",
description="Error Pkg",
superpackage=None,
config=None,
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_package_create_with_superpackage(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.create_package.return_value = True
cmd_package(
argparse.Namespace(
package_action="create",
name="Z_SUB_PKG",
description="Sub Package",
superpackage="Z_PARENT",
config=None,
),
client,
)
client.create_package.assert_called_once_with("Z_SUB_PKG", "Sub Package", "Z_PARENT")
# ═══════════════════════════════════════════════════════════════
# package_cmd.py — package info
# ═══════════════════════════════════════════════════════════════
class TestPackageInfo(unittest.TestCase):
"""package info 成功。"""
def test_package_info_success(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.get_package_info.return_value = {
"name": "Z_TEST",
"description": "Test Package",
"owner": "TESTUSER",
"superpackage": "Z_PARENT",
}
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(
package_action="info", name="Z_TEST", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("Test Package", printed)
self.assertIn("Z_PARENT", printed)
client.get_package_info.assert_called_once_with("Z_TEST")
def test_package_info_no_superpackage(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.get_package_info.return_value = {
"name": "Z_TOP",
"description": "Top level",
"owner": "ADMIN",
"superpackage": "",
}
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(
package_action="info", name="Z_TOP", config=None
),
client,
)
# info.get('superpackage', '(无)') returns '' when key exists but is empty
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
client.get_package_info.assert_called_once()
def test_package_info_api_error(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.get_package_info.side_effect = Exception("Not found")
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(
package_action="info", name="Z_MISSING", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
# ═══════════════════════════════════════════════════════════════
# package_cmd.py — package list
# ═══════════════════════════════════════════════════════════════
class TestPackageList(unittest.TestCase):
"""package list 成功。"""
def test_package_list_with_objects(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.list_objects.return_value = [
{"name": "Z_REPORT1", "type": "PROG/P", "description": "Report 1"},
{"name": "Z_CLASS1", "type": "CLAS/OC", "description": "Class 1"},
]
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(
package_action="list", name="Z_TEST_PKG", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("2", printed)
self.assertIn("Z_REPORT1", printed)
def test_package_list_empty(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.list_objects.return_value = []
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(
package_action="list", name="Z_EMPTY_PKG", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("没有找到", printed)
def test_package_list_api_error(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
client.list_objects.side_effect = Exception("Timeout")
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(
package_action="list", name="Z_ERR_PKG", config=None
),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("失败", printed)
def test_package_unknown_action(self):
from sapcli.commands.package_cmd import cmd_package
client = _make_client()
with patch("builtins.print") as mock_print:
cmd_package(
argparse.Namespace(package_action=None, name="Z_TEST", config=None),
client,
)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("用法", printed)
# ═══════════════════════════════════════════════════════════════
# upload.py — cmd_upload(仅锁定→写入→解锁,不检查不激活)
# ═══════════════════════════════════════════════════════════════
class TestUploadCommand(unittest.TestCase):
"""cmd_upload 仅执行锁定→写入→解锁,绝不触发语法检查和激活。"""
def _write_tmp_source(self, source="REPORT ztest.\nWRITE: / 'hello'.\n"):
"""写入临时 .abap 源码文件,返回路径。"""
f = tempfile.NamedTemporaryFile(
mode="w", suffix=".abap", delete=False, encoding="utf-8"
)
f.write(source)
f.close()
return f.name
def test_upload_success(self):
"""成功上传:lock→set_source→unlock 被调用,syntax_check/activate 未被调用。"""
from sapcli.commands.upload import cmd_upload
client = _make_client()
client.object_exists.return_value = True
client.lock.return_value = ("lh_1", "DEVK901362")
client.set_source.return_value = True
client.unlock.return_value = True
tmpfile = self._write_tmp_source()
try:
cmd_upload(
_args(name="ZTEST", type="report", path=tmpfile, corr_nr=None),
client,
)
finally:
os.unlink(tmpfile)
client.object_exists.assert_called_once()
client.lock.assert_called_once()
client.set_source.assert_called_once()
client.unlock.assert_called_once()
# 关键:区分"真实现"和"假实现"——绝不能触发检查和激活
client.syntax_check.assert_not_called()
client.activate.assert_not_called()
def test_upload_nonexistent_object(self):
"""对象不存在时抛 ObjectNotFoundError(不自动创建,区别于 sync)。"""
from sapcli.commands.upload import cmd_upload
from sapcli.exceptions import ObjectNotFoundError
client = _make_client()
client.object_exists.return_value = False
tmpfile = self._write_tmp_source()
try:
with self.assertRaises(ObjectNotFoundError) as ctx:
cmd_upload(
_args(name="ZMISSING", type="report", path=tmpfile, corr_nr=None),
client,
)
self.assertIn("ZMISSING", str(ctx.exception))
finally:
os.unlink(tmpfile)
# 对象不存在时不该尝试锁定/写入
client.lock.assert_not_called()
client.set_source.assert_not_called()
def test_upload_type_without_source(self):
"""functiongroup 无源码类型抛 InvalidNameError。"""
from sapcli.commands.upload import cmd_upload
from sapcli.exceptions import InvalidNameError
client = _make_client()
with self.assertRaises(InvalidNameError):
cmd_upload(
_args(name="ZTEST_FG", type="functiongroup", path=".", corr_nr=None),
client,
)
client.lock.assert_not_called()
client.set_source.assert_not_called()
def test_upload_with_corr_nr(self):
"""通过 --corr_nr 指定传输请求号,lock 使用该号。"""
from sapcli.commands.upload import cmd_upload
client = _make_client()
client.object_exists.return_value = True
client.lock.return_value = ("lh_1", "DEVK901362")
client.set_source.return_value = True
client.unlock.return_value = True
tmpfile = self._write_tmp_source()
try:
cmd_upload(
_args(name="ZTEST", type="class", path=tmpfile, corr_nr="DEVK901362"),
client,
)
finally:
os.unlink(tmpfile)
# corr_nr 可作为位置参数或关键字参数传入
call_args = client.lock.call_args
passed_corr = (
call_args[1].get("corr_nr")
if "corr_nr" in call_args[1]
else call_args[0][-1]
)
self.assertEqual(passed_corr, "DEVK901362")
client.set_source.assert_called_once()
def test_upload_file_not_found(self):
"""本地文件不存在时抛 ConfigError。"""
from sapcli.commands.upload import cmd_upload
from sapcli.exceptions import ConfigError
client = _make_client()
with self.assertRaises(ConfigError):
cmd_upload(
_args(
name="ZTEST", type="report",
path="/nonexistent/no_such_file.abap", corr_nr=None,
),
client,
)
client.lock.assert_not_called()
client.set_source.assert_not_called()
# ═══════════════════════════════════════════════════════════════
# syntax_check.py — cmd_syntax_check(远程对象语法检查,不上传不激活)
# ═══════════════════════════════════════════════════════════════
class TestSyntaxCheckCommand(unittest.TestCase):
"""cmd_syntax_check 检查远程对象语法,不上传代码、不激活。"""
def test_syntax_check_passed(self):
"""语法检查通过,输出含"通过"。"""
from sapcli.commands.syntax_check import cmd_syntax_check
client = _make_client()
client.object_exists.return_value = True
client.syntax_check.return_value = (True, [])
with patch("builtins.print") as mock_print:
cmd_syntax_check(_args(name="ZTEST", type="class"), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("通过", printed)
client.syntax_check.assert_called_once()
# 区分"真实现"和"假实现":绝不锁定/写入/激活
client.lock.assert_not_called()
client.set_source.assert_not_called()
client.activate.assert_not_called()
def test_syntax_check_failed(self):
"""语法检查失败,输出含错误行号和描述。"""
from sapcli.commands.syntax_check import cmd_syntax_check
client = _make_client()
client.object_exists.return_value = True
client.syntax_check.return_value = (
False,
[{"type": "E", "line": "42", "text": "Field UNKNOWN not found", "href": ""}],
)
with patch("builtins.print") as mock_print:
cmd_syntax_check(_args(name="ZTEST", type="report"), client)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("42", printed)
self.assertIn("UNKNOWN", printed)
def test_syntax_check_nonexistent(self):
"""对象不存在时抛 ObjectNotFoundError。"""
from sapcli.commands.syntax_check import cmd_syntax_check
from sapcli.exceptions import ObjectNotFoundError
client = _make_client()
client.object_exists.return_value = False
with self.assertRaises(ObjectNotFoundError):
cmd_syntax_check(_args(name="ZMISSING", type="report"), client)
client.syntax_check.assert_not_called()
def test_syntax_check_type_without_source(self):
"""无源码类型抛 InvalidNameError。"""
from sapcli.commands.syntax_check import cmd_syntax_check
from sapcli.exceptions import InvalidNameError
client = _make_client()
with self.assertRaises(InvalidNameError):
cmd_syntax_check(_args(name="ZTEST_FG", type="functiongroup"), client)
client.syntax_check.assert_not_called()
# ═══════════════════════════════════════════════════════════════
# ddl_query.py — cmd_show_table / cmd_read_table
# ═══════════════════════════════════════════════════════════════
class TestShowTableCommand(unittest.TestCase):
"""cmd_show_table 查询 DDIC 表字段结构。"""
def test_show_table_success(self):
from sapcli.commands.ddl_query import cmd_show_table
client = _make_client()
client.get_table_fields.return_value = [
{"name": "MANDT", "type": "CLNT", "length": "3", "key_attribute": "X", "description": "Client"},
{"name": "OBJ_ID", "type": "CHAR", "length": "20", "key_attribute": "X", "description": "Object ID"},
]
with patch("builtins.print") as mock_print:
cmd_show_table(_args(name="ZMY_TABLE"), client)
# 区分真/假实现:get_table_fields 被调用一次,且参数是大写表名
client.get_table_fields.assert_called_once_with("ZMY_TABLE")
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("ZMY_TABLE", printed)
self.assertIn("OBJ_ID", printed)
def test_show_table_no_fields(self):
from sapcli.commands.ddl_query import cmd_show_table
client = _make_client()
client.get_table_fields.return_value = []
with patch("builtins.print") as mock_print:
cmd_show_table(_args(name="ZEMPTY"), client)
# 空字段列表 → 打印"无字段信息"并返回
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("无字段信息", printed)
client.get_table_fields.assert_called_once_with("ZEMPTY")
def test_show_table_uppercase(self):
"""传入小写表名,client.get_table_fields 收到的应是大写。"""
from sapcli.commands.ddl_query import cmd_show_table
client = _make_client()
client.get_table_fields.return_value = [
{"name": "F1", "type": "CHAR", "length": "10", "key_attribute": "", "description": "F"},
]
with patch("builtins.print"):
cmd_show_table(_args(name="zmy_table"), client)
# 大小写归一化在 cmd 内完成,client 收到 "ZMY_TABLE"
client.get_table_fields.assert_called_once_with("ZMY_TABLE")
class TestReadTableCommand(unittest.TestCase):
"""cmd_read_table 通过 freestyle SQL 查询表数据。"""
def test_read_table_basic(self):
from sapcli.commands.ddl_query import cmd_read_table
client = _make_client()
client.query_table_data.return_value = {
"columns": ["MANDT", "OBJ_ID"],
"rows": [["100", "OBJ1"], ["100", "OBJ2"]],
"total_rows": 2,
}
with patch("builtins.print") as mock_print:
cmd_read_table(_args(name="ZMY_TABLE", max_rows=50), client)
# 区分真/假实现:query_table_data 收到的 SQL 含大写表名和 UP TO N ROWS
sql_arg = client.query_table_data.call_args[0][0]
self.assertIn("ZMY_TABLE", sql_arg)
self.assertIn("UP TO 50 ROWS", sql_arg)
# max_rows 也作为关键字参数传给 query_table_data
self.assertEqual(client.query_table_data.call_args[1].get("max_rows"), 50)
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("OBJ1", printed)
def test_read_table_with_where(self):
from sapcli.commands.ddl_query import cmd_read_table
client = _make_client()
client.query_table_data.return_value = {
"columns": ["MANDT"],
"rows": [["100"]],
"total_rows": 1,
}
with patch("builtins.print"):
cmd_read_table(
_args(name="ZMY_TABLE", where="FIELD1 = 'X'", max_rows=200),
client,
)
sql_arg = client.query_table_data.call_args[0][0]
self.assertIn("WHERE", sql_arg)
self.assertIn("FIELD1 = 'X'", sql_arg)
def test_read_table_with_fields(self):
from sapcli.commands.ddl_query import cmd_read_table
client = _make_client()
client.query_table_data.return_value = {
"columns": ["F1", "F2"],
"rows": [["v1", "v2"]],
"total_rows": 1,
}
with patch("builtins.print"):
cmd_read_table(
_args(name="ZMY_TABLE", fields="F1,F2", max_rows=200),
client,
)
# 指定字段 → SELECT F1,F2 而非 SELECT *
sql_arg = client.query_table_data.call_args[0][0]
self.assertIn("F1,F2", sql_arg)
self.assertTrue(sql_arg.startswith("SELECT F1,F2"))
def test_read_table_empty_data(self):
from sapcli.commands.ddl_query import cmd_read_table
client = _make_client()
client.query_table_data.return_value = {
"columns": ["MANDT"],
"rows": [],
"total_rows": 0,
}
with patch("builtins.print") as mock_print:
cmd_read_table(_args(name="ZMY_TABLE", max_rows=200), client)
# 空 rows → 打印"无数据"并返回
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
self.assertIn("无数据", printed)
if __name__ == "__main__":
unittest.main(verbosity=2)