"""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''
b'')
# ═══════════════════════════════════════════════════════════════
# 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''
b''
)
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 专属头 → 406,application/xml → 406,*/* → 200。
即 application/xml 本身也 406,用它做回退等于不重试。
⚠️ 断言必须在**请求发生时快照** headers:cmd_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''
b''
)
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)