Files
sap-cli-skill/tests/unit/test_commands_extra.py
T
吴让宇 c5905a5b1e refactor: 本仓升为唯一源(原 sap-cli 源码仓归档)
方向反转:此前 SKILL.md 是「模板渲染产物」、sap-cli 是源;现 sap-cli 归档,
sap-cli-skill 承接开发与分发,SKILL.md 回归手工维护的正本。

迁移(来自 sap-cli,共 104 文件):
- tests/           692 例测试(15 个文件的内联 sys.path 改指 assets/)
- openspec/        SDD 规格与归档变更(42 文件)
- docs/            开发文档与 ADT 原理(含 dev/CLAUDE.md、AGENTS.md)
- .claude/         rules 副本 + settings.json(供 Claude Code)
- .github/ .hermes/ .pre-commit-config.yaml .editorconfig CLAUDE.md
- scripts/ 保持仅 setup.py(pack_skill.py 已随旧仓归档,不迁)

修复(迁移暴露的真实缺陷):
- assets/pyproject.toml 的 build-backend 写作 `setuptools.backends._legacy:_Backend`,
  该模块在 setuptools 中不存在 → `pip install -e` 从来装不上。改为 build_meta。
  实测:临时 venv 安装成功,sap-cli --help 正常列出 31 个命令
- pyproject readme 指向不存在的 assets/README.md(editable 安装会失败)→ 改内联文本
- pyproject urls 改指 sap-cli-skill

机制调整:
- .github/workflows/ci.yml 适配 assets/ 布局;顶部注明该工作流仅 GitHub 执行,
  本仓在 Gitee 不会自动跑
- pre-commit 增本地测试门禁(Gitee 上真正生效的那道)
- .gitignore 合并旧仓完整规则(保留 log/ 下 md 知识库入库,只忽略运行日志)
- 大文件上限 100KB→1MB(架构图 512KB)

守卫测试 tests/unit/test_repo_guards.py(10 → 18 例):
- SKILL.md 须记录 parser 全部 CLI 命令 / 铁律 1-5 须为真实小节标题 / 示例不得违反铁律 5
- references/ 规则齐备;.claude/rules 与 references 必须一致(实测抓到一次真实漂移)
- VERSION == sapcli.__version__ == README 版本
- 仓内不得再出现 pack_skill.py / skill-src(防废弃流程回潮)

698 tests OK;editable 安装与 CLI 入口经临时 venv 实测通过。
docs/RELEASING.md 重写为单源开发流程。
2026-09-11 00:40:15 +08:00

1902 lines
72 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""commands/ 模块深入单元测试 — 覆盖失败/边界场景。
运行: python tests/unit/test_commands_extra.py
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import tempfile
import unittest
from unittest.mock import MagicMock, patch, mock_open
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "assets"))
from sapcli.client import ADTClient
# ── helpers ──────────────────────────────────────────────────────
def _mock_resp(status_code=200, text="", content=b"", headers=None):
r = MagicMock()
r.status_code = status_code
r.text = text
r.content = content
r.headers = headers or {}
r.raise_for_status = MagicMock()
return r
def _make_client():
"""创建完全 mock 的 ADTClient。"""
client = MagicMock(spec=ADTClient)
client.host = "https://sap.example.com"
client.sap_client = "100"
client.csrf_token = "test-csrf-token"
client.user = "TESTUSER"
client.session = MagicMock()
client._headers.return_value = {
"content-type": "application/xml",
"x-csrf-token": "test-csrf-token",
}
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 时回退到 application/xml。"""
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)
# ═══════════════════════════════════════════════════════════════
# 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)