`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。
1939 lines
74 KiB
Python
1939 lines
74 KiB
Python
"""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 专属头 → 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'<?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)
|