`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。
1125 lines
48 KiB
Python
1125 lines
48 KiB
Python
"""commands/crud.py 深入单元测试 — 补全 _sync_single / cmd_info / cmd_delete /
|
||
_select_transport_request / cmd_create / _create_ddic / manifest 更新等分支。
|
||
|
||
运行: python tests/unit/test_crud_extra.py
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "assets"))
|
||
|
||
from sapcli.client import ADTClient
|
||
from sapcli.exceptions import (
|
||
CreateError,
|
||
DeleteError,
|
||
InvalidNameError,
|
||
LockError,
|
||
ObjectNotFoundError,
|
||
SapCliError,
|
||
)
|
||
|
||
|
||
# ── 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)
|
||
|
||
|
||
def _write_source(content="REPORT ztest.\nWRITE: / 'hello'.\n"):
|
||
"""写入临时 .abap 文件,返回路径。"""
|
||
f = tempfile.NamedTemporaryFile(
|
||
mode="w", suffix=".abap", delete=False, encoding="utf-8"
|
||
)
|
||
f.write(content)
|
||
f.close()
|
||
return f.name
|
||
|
||
|
||
def _info_xml(name="ZTEST", version="active", package=None):
|
||
head = (
|
||
b'<?xml version="1.0"?>'
|
||
b'<adtcore:object xmlns:adtcore="http://www.sap.com/adt/core"'
|
||
b' adtcore:name="' + name.encode() + b'" adtcore:type="PROG/P"'
|
||
b' adtcore:version="' + version.encode() + b'" adtcore:description="d"'
|
||
)
|
||
if package is None:
|
||
return head + b'/>'
|
||
return (
|
||
head + b'>'
|
||
b'<adtcore:packageRef adtcore:name="' + package.encode() + b'"/>'
|
||
b'</adtcore:object>'
|
||
)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# cmd_download / cmd_sync — 无源码类型
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestCmdDownloadNoSource(unittest.TestCase):
|
||
def test_download_functiongroup_raises(self):
|
||
from sapcli.commands.crud import cmd_download
|
||
|
||
client = _make_client()
|
||
with self.assertRaises(InvalidNameError):
|
||
cmd_download(
|
||
_args(type="functiongroup", name="ZFG", path="."), client
|
||
)
|
||
client.get_source.assert_not_called()
|
||
|
||
|
||
class TestCmdSyncNoSource(unittest.TestCase):
|
||
def test_sync_functiongroup_raises(self):
|
||
from sapcli.commands.crud import cmd_sync
|
||
|
||
client = _make_client()
|
||
tmp = _write_source()
|
||
try:
|
||
with self.assertRaises(InvalidNameError):
|
||
cmd_sync(
|
||
_args(type="functiongroup", name="ZFG", path=tmp), client
|
||
)
|
||
finally:
|
||
os.unlink(tmp)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# cmd_sync — project_path 探测 + 成功/失败清单更新
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestCmdSyncProjectPathDetection(unittest.TestCase):
|
||
@patch("sapcli.commands.crud._sync_single")
|
||
def test_detects_project_path_from_manifest(self, mock_sync):
|
||
"""文件同目录有 manifest.json → project_path 被设为该目录。"""
|
||
from sapcli.commands.crud import cmd_sync
|
||
|
||
client = _make_client()
|
||
mock_sync.return_value = (True, None, "DEVK001")
|
||
|
||
with tempfile.TemporaryDirectory() as td:
|
||
filepath = os.path.join(td, "ztest.abap")
|
||
with open(filepath, "w", encoding="utf-8") as f:
|
||
f.write("REPORT ztest.")
|
||
with open(os.path.join(td, "manifest.json"), "w", encoding="utf-8") as f:
|
||
json.dump({"version": 1, "objects": {}}, f)
|
||
|
||
args = _args(type="report", name="ZTEST", path=filepath)
|
||
cmd_sync(args, client)
|
||
self.assertEqual(args.project_path, td)
|
||
|
||
@patch("sapcli.commands.crud._sync_single")
|
||
def test_no_manifest_sets_project_path_none(self, mock_sync):
|
||
"""文件同目录无 manifest.json → project_path = None。"""
|
||
from sapcli.commands.crud import cmd_sync
|
||
|
||
client = _make_client()
|
||
mock_sync.return_value = (True, None, None)
|
||
|
||
with tempfile.TemporaryDirectory() as td:
|
||
filepath = os.path.join(td, "ztest.abap")
|
||
with open(filepath, "w", encoding="utf-8") as f:
|
||
f.write("REPORT ztest.")
|
||
args = _args(type="report", name="ZTEST", path=filepath)
|
||
cmd_sync(args, client)
|
||
self.assertIsNone(args.project_path)
|
||
|
||
@patch("sapcli.commands.crud._sync_single")
|
||
def test_sync_failure_raises_and_updates_manifest(self, mock_sync):
|
||
"""sync 失败时调用 _update_manifest_after_sync(failed=True) 并抛 SapCliError。"""
|
||
from sapcli.commands.crud import cmd_sync
|
||
|
||
client = _make_client()
|
||
mock_sync.return_value = (False, "激活失败: 行3", "DEVK001")
|
||
|
||
with tempfile.TemporaryDirectory() as td:
|
||
filepath = os.path.join(td, "ztest.abap")
|
||
with open(filepath, "w", encoding="utf-8") as f:
|
||
f.write("REPORT ztest.")
|
||
with open(os.path.join(td, "manifest.json"), "w", encoding="utf-8") as f:
|
||
json.dump({"version": 1, "objects": {}}, f)
|
||
args = _args(type="report", name="ZTEST", path=filepath)
|
||
with self.assertRaises(SapCliError):
|
||
cmd_sync(args, client)
|
||
# 清单被写入失败标记
|
||
with open(os.path.join(td, "manifest.json"), encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
self.assertEqual(
|
||
data["objects"]["ZTEST"]["last_sync_result"], "failed"
|
||
)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# _sync_single — 各分支
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestSyncSingleBranches(unittest.TestCase):
|
||
"""_sync_single 核心同步逻辑的各种分支。"""
|
||
|
||
def test_no_source_type_returns_false(self):
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
client = _make_client()
|
||
ok, err, corr = _sync_single("ZFG", "functiongroup", "/x.abap", client)
|
||
self.assertFalse(ok)
|
||
self.assertIn("没有源代码", err)
|
||
self.assertIsNone(corr)
|
||
|
||
def test_file_not_found_returns_false(self):
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
client = _make_client()
|
||
ok, err, corr = _sync_single(
|
||
"ZTEST", "report", "/nonexistent/no.abap", client
|
||
)
|
||
self.assertFalse(ok)
|
||
self.assertIn("文件不存在", err)
|
||
self.assertIsNone(corr)
|
||
|
||
def test_object_not_exists_auto_creates(self):
|
||
"""对象不存在时自动 create_object。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
client.lock.return_value = ("lh_1", "DEVK001")
|
||
client.set_source.return_value = True
|
||
client.unlock.return_value = True
|
||
client.syntax_check.return_value = (True, [])
|
||
client.activate.return_value = (True, [])
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, _, corr = _sync_single("ZTEST", "report", tmp, client, corr_nr="DEVK001")
|
||
self.assertTrue(ok)
|
||
finally:
|
||
os.unlink(tmp)
|
||
client.create_object.assert_called_once()
|
||
|
||
def test_create_object_failure_returns_false(self):
|
||
"""自动创建对象失败 → 返回 False。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
client.create_object.side_effect = Exception("create boom")
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, err, _ = _sync_single("ZTEST", "report", tmp, client, corr_nr="DEVK001")
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertFalse(ok)
|
||
self.assertIn("创建失败", err)
|
||
|
||
def test_lock_with_corr_nr(self):
|
||
"""指定 corr_nr 时用该号锁定。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
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.syntax_check.return_value = (True, [])
|
||
client.activate.return_value = (True, [])
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, _, corr = _sync_single("ZTEST", "report", tmp, client, corr_nr="DEVK001")
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertTrue(ok)
|
||
self.assertEqual(corr, "DEVK001")
|
||
# lock 第二位置参数为 corr_nr
|
||
self.assertEqual(client.lock.call_args[0][1], "DEVK001")
|
||
|
||
def test_lock_without_corr_detects_transport(self):
|
||
"""无 corr_nr 但 lock 返回检测到的传输请求号。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
client = _make_client()
|
||
client.object_exists.return_value = True
|
||
client.lock.return_value = ("lh_1", "DEVK901999")
|
||
client.set_source.return_value = True
|
||
client.unlock.return_value = True
|
||
client.syntax_check.return_value = (True, [])
|
||
client.activate.return_value = (True, [])
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, _, corr = _sync_single("ZTEST", "report", tmp, client)
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertTrue(ok)
|
||
self.assertEqual(corr, "DEVK901999")
|
||
|
||
def test_lock_without_corr_local_object(self):
|
||
"""无 corr_nr 且 lock 未返回传输请求号 → 本地对象。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
client = _make_client()
|
||
client.object_exists.return_value = True
|
||
client.lock.return_value = ("lh_1", None)
|
||
client.set_source.return_value = True
|
||
client.unlock.return_value = True
|
||
client.syntax_check.return_value = (True, [])
|
||
client.activate.return_value = (True, [])
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, _, corr = _sync_single("ZTEST", "report", tmp, client)
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertTrue(ok)
|
||
self.assertIsNone(corr)
|
||
|
||
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
|
||
def test_lock_error_then_select_transport(self, mock_select):
|
||
"""lock 抛 LockError → 交互式选择传输请求后重试锁定。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
client = _make_client()
|
||
client.object_exists.return_value = True
|
||
client.lock.side_effect = [LockError("need corr"), ("lh_1", "DEVK001")]
|
||
client.set_source.return_value = True
|
||
client.unlock.return_value = True
|
||
client.syntax_check.return_value = (True, [])
|
||
client.activate.return_value = (True, [])
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, _, corr = _sync_single("ZTEST", "report", tmp, client)
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertTrue(ok)
|
||
self.assertEqual(corr, "DEVK001")
|
||
mock_select.assert_called_once()
|
||
|
||
@patch("sapcli.commands.crud._select_transport_request", return_value=None)
|
||
def test_lock_error_select_returns_none_fails(self, mock_select):
|
||
"""lock 抛 LockError 且无法获取传输请求号 → 返回失败。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
client = _make_client()
|
||
client.object_exists.return_value = True
|
||
client.lock.side_effect = [LockError("need corr")]
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, err, corr = _sync_single("ZTEST", "report", tmp, client)
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertFalse(ok)
|
||
self.assertIn("传输请求号", err)
|
||
|
||
def test_lock_error_outer_caught(self):
|
||
"""带 corr_nr 时 lock 直接抛 LockError(不走选择)→ 返回失败。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
client = _make_client()
|
||
client.object_exists.return_value = True
|
||
client.lock.side_effect = LockError("locked by other")
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, err, corr = _sync_single("ZTEST", "report", tmp, client, corr_nr="DEVK001")
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertFalse(ok)
|
||
self.assertIn("locked by other", err)
|
||
|
||
def test_syntax_check_failure_returns_false(self):
|
||
"""语法检查未通过 → 返回失败,列出错误。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
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.syntax_check.return_value = (
|
||
False,
|
||
[
|
||
{"type": "E", "line": "10", "text": "field missing"},
|
||
{"type": "W", "line": "5", "text": "warning msg"},
|
||
],
|
||
)
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, err, corr = _sync_single("ZTEST", "report", tmp, client, corr_nr="DEVK001")
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertFalse(ok)
|
||
self.assertIn("语法检查未通过", err)
|
||
self.assertIn("10", err)
|
||
self.assertEqual(corr, "DEVK001")
|
||
client.activate.assert_not_called()
|
||
|
||
def test_syntax_check_exception_skips_to_activate(self):
|
||
"""语法检查抛异常 → 跳过检查直接激活。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
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.syntax_check.side_effect = Exception("check svc down")
|
||
client.activate.return_value = (True, [])
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, _, _ = _sync_single("ZTEST", "report", tmp, client, corr_nr="DEVK001")
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertTrue(ok)
|
||
client.activate.assert_called_once()
|
||
|
||
def test_activation_failure_returns_false(self):
|
||
"""激活失败 → 返回失败并列出错误。"""
|
||
from sapcli.commands.crud import _sync_single
|
||
|
||
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.syntax_check.return_value = (True, [])
|
||
client.activate.return_value = (
|
||
False,
|
||
[
|
||
{"type": "E", "line": "3", "text": "act error"},
|
||
{"type": "W", "line": "1", "text": "act warn"},
|
||
],
|
||
)
|
||
|
||
tmp = _write_source()
|
||
try:
|
||
ok, err, corr = _sync_single("ZTEST", "report", tmp, client, corr_nr="DEVK001")
|
||
finally:
|
||
os.unlink(tmp)
|
||
self.assertFalse(ok)
|
||
self.assertIn("激活失败", err)
|
||
self.assertIn("3", err)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# _update_manifest_after_sync
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestUpdateManifestAfterSync(unittest.TestCase):
|
||
def _manifest_with_entry(self, td, name="ZTEST"):
|
||
path = os.path.join(td, "manifest.json")
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
json.dump(
|
||
{
|
||
"version": 1,
|
||
"objects": {
|
||
name: {
|
||
"type": "report", "file": "reports/ztest.abap",
|
||
"system_status": "active",
|
||
}
|
||
},
|
||
},
|
||
f,
|
||
)
|
||
return path
|
||
|
||
def test_no_project_path_returns(self):
|
||
from sapcli.commands.crud import _update_manifest_after_sync
|
||
_update_manifest_after_sync(
|
||
argparse.Namespace(project_path=None), "ZTEST", "report", "/x.abap", None
|
||
)
|
||
|
||
def test_no_manifest_file_returns(self):
|
||
from sapcli.commands.crud import _update_manifest_after_sync
|
||
with tempfile.TemporaryDirectory() as td:
|
||
_update_manifest_after_sync(
|
||
argparse.Namespace(project_path=td), "ZTEST", "report", "/x.abap", None
|
||
)
|
||
|
||
def test_existing_entry_updated_success(self):
|
||
from sapcli.commands.crud import _update_manifest_after_sync
|
||
with tempfile.TemporaryDirectory() as td:
|
||
mpath = self._manifest_with_entry(td)
|
||
_update_manifest_after_sync(
|
||
argparse.Namespace(project_path=td), "ZTEST", "report",
|
||
os.path.join(td, "ztest.abap"), "DEVK001",
|
||
)
|
||
with open(mpath, encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
self.assertEqual(data["objects"]["ZTEST"]["corr_nr"], "DEVK001")
|
||
self.assertEqual(data["objects"]["ZTEST"]["last_sync_result"], "success")
|
||
self.assertEqual(data["objects"]["ZTEST"]["system_status"], "active")
|
||
|
||
def test_existing_entry_failure_keeps_status(self):
|
||
from sapcli.commands.crud import _update_manifest_after_sync
|
||
with tempfile.TemporaryDirectory() as td:
|
||
mpath = self._manifest_with_entry(td)
|
||
_update_manifest_after_sync(
|
||
argparse.Namespace(project_path=td), "ZTEST", "report",
|
||
os.path.join(td, "ztest.abap"), "DEVK001", failed=True,
|
||
)
|
||
with open(mpath, encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
self.assertEqual(data["objects"]["ZTEST"]["last_sync_result"], "failed")
|
||
# failed 时 system_status 不变(仍 active)
|
||
self.assertEqual(data["objects"]["ZTEST"]["system_status"], "active")
|
||
|
||
def test_new_entry_upserted(self):
|
||
from sapcli.commands.crud import _update_manifest_after_sync
|
||
with tempfile.TemporaryDirectory() as td:
|
||
with open(os.path.join(td, "manifest.json"), "w", encoding="utf-8") as f:
|
||
json.dump({"version": 1, "objects": {}}, f)
|
||
_update_manifest_after_sync(
|
||
argparse.Namespace(project_path=td), "ZNEW", "report",
|
||
os.path.join(td, "reports", "znew.abap"), "DEVK001",
|
||
)
|
||
with open(os.path.join(td, "manifest.json"), encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
self.assertIn("ZNEW", data["objects"])
|
||
self.assertEqual(data["objects"]["ZNEW"]["system_status"], "active")
|
||
|
||
def test_new_entry_failure_marked_inactive(self):
|
||
from sapcli.commands.crud import _update_manifest_after_sync
|
||
with tempfile.TemporaryDirectory() as td:
|
||
with open(os.path.join(td, "manifest.json"), "w", encoding="utf-8") as f:
|
||
json.dump({"version": 1, "objects": {}}, f)
|
||
_update_manifest_after_sync(
|
||
argparse.Namespace(project_path=td), "ZNEW", "report",
|
||
os.path.join(td, "znew.abap"), None, failed=True,
|
||
)
|
||
with open(os.path.join(td, "manifest.json"), encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
self.assertEqual(data["objects"]["ZNEW"]["system_status"], "inactive")
|
||
self.assertEqual(data["objects"]["ZNEW"]["last_sync_result"], "failed")
|
||
|
||
def test_corrupt_manifest_exception_swallowed(self):
|
||
"""Manifest.load 抛异常时被吞掉,不传播。"""
|
||
from sapcli.commands.crud import _update_manifest_after_sync
|
||
with tempfile.TemporaryDirectory() as td:
|
||
with open(os.path.join(td, "manifest.json"), "w", encoding="utf-8") as f:
|
||
f.write("NOT VALID JSON")
|
||
# 不抛异常
|
||
_update_manifest_after_sync(
|
||
argparse.Namespace(project_path=td), "ZTEST", "report", "/x.abap", None
|
||
)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# cmd_info — 状态图标
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestCmdInfoStatusIcons(unittest.TestCase):
|
||
def test_inactive_version_icon(self):
|
||
from sapcli.commands.crud import cmd_info
|
||
client = _make_client()
|
||
client.session.get.return_value = _mock_resp(200, content=_info_xml(version="inactive"))
|
||
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_unknown_version_icon(self):
|
||
from sapcli.commands.crud import cmd_info
|
||
client = _make_client()
|
||
client.session.get.return_value = _mock_resp(200, content=_info_xml(version="draft"))
|
||
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("draft", printed)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# cmd_info — 开发包字段
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestCmdInfoPackage(unittest.TestCase):
|
||
def test_package_displayed(self):
|
||
from sapcli.commands.crud import cmd_info
|
||
client = _make_client()
|
||
client.session.get.return_value = _mock_resp(
|
||
200, content=_info_xml(package="ZMY_PKG")
|
||
)
|
||
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)
|
||
self.assertIn("ZMY_PKG", printed)
|
||
|
||
def test_package_missing_shows_unspecified(self):
|
||
from sapcli.commands.crud import cmd_info
|
||
client = _make_client()
|
||
client.session.get.return_value = _mock_resp(200, content=_info_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)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# cmd_delete — 分支
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestCmdDeleteBranches(unittest.TestCase):
|
||
def test_delete_object_not_found_raises(self):
|
||
from sapcli.commands.crud import cmd_delete
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
with self.assertRaises(ObjectNotFoundError):
|
||
cmd_delete(_args(), client)
|
||
|
||
@patch("builtins.input", return_value="no")
|
||
def test_delete_cancelled_returns(self, mock_input):
|
||
from sapcli.commands.crud import cmd_delete
|
||
client = _make_client()
|
||
client.object_exists.return_value = True
|
||
with patch("builtins.print") as mock_print:
|
||
cmd_delete(_args(), client)
|
||
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
|
||
self.assertIn("取消", printed)
|
||
client.delete_object.assert_not_called()
|
||
|
||
@patch("builtins.input", return_value="yes")
|
||
def test_delete_no_corr_nr(self, mock_input):
|
||
"""无传输请求号时仍尝试删除。"""
|
||
from sapcli.commands.crud import cmd_delete
|
||
client = _make_client()
|
||
client.object_exists.return_value = True
|
||
client.get_transport_request.return_value = None
|
||
client.delete_object.return_value = (True, "")
|
||
with patch("builtins.print") as mock_print:
|
||
cmd_delete(_args(), client)
|
||
printed = " ".join(str(c) for c, _ in mock_print.call_args_list)
|
||
self.assertIn("无传输号", printed)
|
||
client.delete_object.assert_called_once()
|
||
|
||
@patch("builtins.input", return_value="yes")
|
||
def test_delete_failure_raises(self, mock_input):
|
||
from sapcli.commands.crud import cmd_delete
|
||
client = _make_client()
|
||
client.object_exists.return_value = True
|
||
client.get_transport_request.return_value = "DEVK001"
|
||
client.delete_object.return_value = (False, "locked")
|
||
with patch("builtins.print"):
|
||
with self.assertRaises(DeleteError) as ctx:
|
||
cmd_delete(_args(), client)
|
||
self.assertIn("locked", str(ctx.exception))
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# _update_manifest_after_delete
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestUpdateManifestAfterDelete(unittest.TestCase):
|
||
def test_no_project_path_returns(self):
|
||
from sapcli.commands.crud import _update_manifest_after_delete
|
||
_update_manifest_after_delete(argparse.Namespace(project_path=None), "ZTEST")
|
||
|
||
def test_no_manifest_file_returns(self):
|
||
from sapcli.commands.crud import _update_manifest_after_delete
|
||
with tempfile.TemporaryDirectory() as td:
|
||
_update_manifest_after_delete(argparse.Namespace(project_path=td), "ZTEST")
|
||
|
||
def test_removes_existing_entry(self):
|
||
from sapcli.commands.crud import _update_manifest_after_delete
|
||
with tempfile.TemporaryDirectory() as td:
|
||
mpath = os.path.join(td, "manifest.json")
|
||
with open(mpath, "w", encoding="utf-8") as f:
|
||
json.dump(
|
||
{"version": 1, "objects": {"ZTEST": {"type": "report", "file": "x.abap"}}},
|
||
f,
|
||
)
|
||
_update_manifest_after_delete(argparse.Namespace(project_path=td), "ZTEST")
|
||
with open(mpath, encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
self.assertNotIn("ZTEST", data["objects"])
|
||
|
||
def test_remove_nonexistent_entry_no_save(self):
|
||
"""移除不存在的条目时 remove 返回 False,不保存。"""
|
||
from sapcli.commands.crud import _update_manifest_after_delete
|
||
with tempfile.TemporaryDirectory() as td:
|
||
mpath = os.path.join(td, "manifest.json")
|
||
with open(mpath, "w", encoding="utf-8") as f:
|
||
json.dump({"version": 1, "objects": {}}, f)
|
||
mtime_before = os.path.getmtime(mpath)
|
||
_update_manifest_after_delete(argparse.Namespace(project_path=td), "ZNONE")
|
||
# 不抛异常即可(remove 返回 False → 不 save)
|
||
|
||
def test_corrupt_manifest_exception_swallowed(self):
|
||
from sapcli.commands.crud import _update_manifest_after_delete
|
||
with tempfile.TemporaryDirectory() as td:
|
||
with open(os.path.join(td, "manifest.json"), "w", encoding="utf-8") as f:
|
||
f.write("BAD JSON")
|
||
_update_manifest_after_delete(argparse.Namespace(project_path=td), "ZTEST")
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# _select_transport_request — 交互式
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestSelectTransportRequest(unittest.TestCase):
|
||
def test_api_error_returns_none(self):
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.side_effect = Exception("fail")
|
||
with patch("builtins.print"):
|
||
self.assertIsNone(_select_transport_request(client))
|
||
|
||
def test_choose_existing_by_index(self):
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = [
|
||
{"number": "DEVK001", "description": "a", "owner": "u"},
|
||
{"number": "DEVK002", "description": "b", "owner": "u"},
|
||
]
|
||
with patch("builtins.input", return_value="1"):
|
||
result = _select_transport_request(client)
|
||
self.assertEqual(result, "DEVK001")
|
||
|
||
def test_choose_zero_no_transport(self):
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = [
|
||
{"number": "DEVK001", "description": "a", "owner": "u"},
|
||
]
|
||
with patch("builtins.input", return_value="0"):
|
||
result = _select_transport_request(client)
|
||
self.assertIsNone(result)
|
||
|
||
def test_choose_new_request_success(self):
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = [
|
||
{"number": "DEVK001", "description": "a", "owner": "u"},
|
||
{"number": "DEVK002", "description": "b", "owner": "u"},
|
||
]
|
||
# "3" = 新建(2 + 1),随后输入描述
|
||
with patch("builtins.input", side_effect=["3", "new feature"]):
|
||
client.create_transport_request.return_value = "DEVK999"
|
||
result = _select_transport_request(client)
|
||
self.assertEqual(result, "DEVK999")
|
||
|
||
def test_choose_new_request_create_returns_none(self):
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = [
|
||
{"number": "DEVK001", "description": "a", "owner": "u"},
|
||
]
|
||
# "2" = 新建,描述,create 返回 None
|
||
with patch("builtins.input", side_effect=["2", "desc"]):
|
||
client.create_transport_request.return_value = None
|
||
result = _select_transport_request(client)
|
||
self.assertIsNone(result)
|
||
|
||
def test_choose_new_request_create_raises(self):
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = [
|
||
{"number": "DEVK001", "description": "a", "owner": "u"},
|
||
]
|
||
with patch("builtins.input", side_effect=["2", "desc"]):
|
||
client.create_transport_request.side_effect = Exception("no perm")
|
||
result = _select_transport_request(client)
|
||
self.assertIsNone(result)
|
||
|
||
def test_invalid_input_then_valid(self):
|
||
"""非数字输入后重新输入有效索引。"""
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = [
|
||
{"number": "DEVK001", "description": "a", "owner": "u"},
|
||
]
|
||
with patch("builtins.input", side_effect=["abc", "1"]):
|
||
result = _select_transport_request(client)
|
||
self.assertEqual(result, "DEVK001")
|
||
|
||
def test_empty_input_then_valid(self):
|
||
"""空输入(直接回车)时重新提示。"""
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = [
|
||
{"number": "DEVK001", "description": "a", "owner": "u"},
|
||
]
|
||
with patch("builtins.input", side_effect=["", "1"]):
|
||
result = _select_transport_request(client)
|
||
self.assertEqual(result, "DEVK001")
|
||
|
||
def test_new_request_empty_desc_then_valid(self):
|
||
"""新建请求但描述为空 → 提示后重新选择,再选已有请求。"""
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = [
|
||
{"number": "DEVK001", "description": "a", "owner": "u"},
|
||
]
|
||
# "2"=新建, ""=空描述(continue), "1"=选 DEVK001
|
||
with patch("builtins.input", side_effect=["2", "", "1"]):
|
||
result = _select_transport_request(client)
|
||
self.assertEqual(result, "DEVK001")
|
||
|
||
def test_out_of_range_then_valid(self):
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = [
|
||
{"number": "DEVK001", "description": "a", "owner": "u"},
|
||
]
|
||
with patch("builtins.input", side_effect=["99", "1"]):
|
||
result = _select_transport_request(client)
|
||
self.assertEqual(result, "DEVK001")
|
||
|
||
def test_empty_list_create_new(self):
|
||
"""无可修改请求时,选择新建。"""
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = []
|
||
with patch("builtins.input", side_effect=["y", "new desc"]):
|
||
client.create_transport_request.return_value = "DEVK888"
|
||
result = _select_transport_request(client)
|
||
self.assertEqual(result, "DEVK888")
|
||
|
||
def test_empty_list_decline(self):
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = []
|
||
with patch("builtins.input", return_value="n"):
|
||
result = _select_transport_request(client)
|
||
self.assertIsNone(result)
|
||
|
||
def test_empty_list_empty_description(self):
|
||
"""空列表 + 选择新建但描述为空 → 无传输号创建。"""
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = []
|
||
with patch("builtins.input", side_effect=["y", ""]):
|
||
result = _select_transport_request(client)
|
||
self.assertIsNone(result)
|
||
|
||
def test_empty_list_create_raises(self):
|
||
from sapcli.commands.crud import _select_transport_request
|
||
client = _make_client()
|
||
client.list_transport_requests.return_value = []
|
||
with patch("builtins.input", side_effect=["y", "desc"]):
|
||
client.create_transport_request.side_effect = Exception("err")
|
||
result = _select_transport_request(client)
|
||
self.assertIsNone(result)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# cmd_create — function / functiongroup / 异常
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestCmdCreateBranches(unittest.TestCase):
|
||
def test_create_function_without_slash_raises(self):
|
||
from sapcli.commands.crud import cmd_create
|
||
client = _make_client()
|
||
with self.assertRaises(InvalidNameError):
|
||
cmd_create(
|
||
_args(type="function", name="Z_NO_SLASH", source=None,
|
||
description="d", package="$TMP", corr_nr="DEVK001"),
|
||
client,
|
||
)
|
||
|
||
def test_create_object_already_exists_raises(self):
|
||
"""非 functiongroup 类型且对象已存在 → ObjectAlreadyExistsError。"""
|
||
from sapcli.commands.crud import cmd_create
|
||
from sapcli.exceptions import ObjectAlreadyExistsError
|
||
client = _make_client()
|
||
client.object_exists.return_value = True
|
||
with self.assertRaises(ObjectAlreadyExistsError):
|
||
cmd_create(
|
||
_args(type="class", name="ZCL_EXISTS", source=None,
|
||
description="d", package="$TMP", corr_nr="DEVK001"),
|
||
client,
|
||
)
|
||
|
||
@patch("sapcli.commands.crud.Manifest")
|
||
def test_create_corr_nr_specified_skips_select(self, MockManifest):
|
||
from sapcli.commands.crud import cmd_create
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
client.create_object.return_value = ("/uri/z", "/uri/z/source")
|
||
cmd_create(
|
||
_args(type="report", name="ZTEST", source=None,
|
||
description="d", package="$TMP", corr_nr="DEVK001"),
|
||
client,
|
||
)
|
||
client.create_object.assert_called_once()
|
||
|
||
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
|
||
@patch("sapcli.commands.crud.Manifest")
|
||
def test_create_function_creates_group(self, MockManifest, mock_transport):
|
||
"""function 类型且函数组不存在 → 自动创建函数组。"""
|
||
from sapcli.commands.crud import cmd_create
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
client.function_group_exists.return_value = False
|
||
client.create_function_group.return_value = "/uri/group"
|
||
client.create_object.return_value = ("/uri/func", "/uri/func/source")
|
||
cmd_create(
|
||
_args(type="function", name="ZGRP/Z_FUNC", source=None,
|
||
description="d", package="$TMP", corr_nr=None),
|
||
client,
|
||
)
|
||
client.create_function_group.assert_called_once()
|
||
|
||
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
|
||
@patch("sapcli.commands.crud.Manifest")
|
||
def test_create_function_group_exists(self, MockManifest, mock_transport):
|
||
"""function 类型且函数组已存在 → 跳过创建。"""
|
||
from sapcli.commands.crud import cmd_create
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
client.function_group_exists.return_value = True
|
||
client.create_object.return_value = ("/uri/func", "/uri/func/source")
|
||
cmd_create(
|
||
_args(type="function", name="ZGRP/Z_FUNC", source=None,
|
||
description="d", package="$TMP", corr_nr=None),
|
||
client,
|
||
)
|
||
client.create_function_group.assert_not_called()
|
||
|
||
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
|
||
def test_create_function_group_create_raises(self, mock_transport):
|
||
"""function 函数组创建失败 → CreateError。"""
|
||
from sapcli.commands.crud import cmd_create
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
client.function_group_exists.return_value = False
|
||
client.create_function_group.side_effect = Exception("group fail")
|
||
with self.assertRaises(CreateError):
|
||
cmd_create(
|
||
_args(type="function", name="ZGRP/Z_FUNC", source=None,
|
||
description="d", package="$TMP", corr_nr=None),
|
||
client,
|
||
)
|
||
|
||
@patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001")
|
||
def test_create_functiongroup_create_raises(self, mock_transport):
|
||
"""functiongroup 创建失败 → CreateError。"""
|
||
from sapcli.commands.crud import cmd_create
|
||
client = _make_client()
|
||
client.function_group_exists.return_value = False
|
||
client.create_function_group.side_effect = Exception("boom")
|
||
with self.assertRaises(CreateError):
|
||
cmd_create(
|
||
_args(type="functiongroup", name="ZFG", source=None,
|
||
description="d", package="$TMP", corr_nr=None),
|
||
client,
|
||
)
|
||
|
||
@patch("sapcli.commands.crud.Manifest")
|
||
def test_create_object_raises(self, MockManifest):
|
||
from sapcli.commands.crud import cmd_create
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
client.create_object.side_effect = Exception("create boom")
|
||
with self.assertRaises(CreateError):
|
||
cmd_create(
|
||
_args(type="report", name="ZTEST", source=None,
|
||
description="d", package="$TMP", corr_nr="DEVK001"),
|
||
client,
|
||
)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# _update_manifest_after_create
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestUpdateManifestAfterCreate(unittest.TestCase):
|
||
def test_no_project_path_returns(self):
|
||
from sapcli.commands.crud import _update_manifest_after_create
|
||
_update_manifest_after_create(
|
||
argparse.Namespace(project_path=None), "ZTEST", "report", "DEVK001"
|
||
)
|
||
|
||
def test_no_manifest_file_returns(self):
|
||
from sapcli.commands.crud import _update_manifest_after_create
|
||
with tempfile.TemporaryDirectory() as td:
|
||
_update_manifest_after_create(
|
||
argparse.Namespace(project_path=td), "ZTEST", "report", "DEVK001"
|
||
)
|
||
|
||
def test_report_gets_reports_dir(self):
|
||
from sapcli.commands.crud import _update_manifest_after_create
|
||
with tempfile.TemporaryDirectory() as td:
|
||
with open(os.path.join(td, "manifest.json"), "w", encoding="utf-8") as f:
|
||
json.dump({"version": 1, "objects": {}}, f)
|
||
_update_manifest_after_create(
|
||
argparse.Namespace(project_path=td), "ZTEST", "report", "DEVK001"
|
||
)
|
||
with open(os.path.join(td, "manifest.json"), encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
self.assertEqual(data["objects"]["ZTEST"]["file"], "reports/ztest.abap")
|
||
|
||
def test_function_gets_functions_dir(self):
|
||
from sapcli.commands.crud import _update_manifest_after_create
|
||
with tempfile.TemporaryDirectory() as td:
|
||
with open(os.path.join(td, "manifest.json"), "w", encoding="utf-8") as f:
|
||
json.dump({"version": 1, "objects": {}}, f)
|
||
_update_manifest_after_create(
|
||
argparse.Namespace(project_path=td), "ZGRP/Z_FUNC", "function", None
|
||
)
|
||
with open(os.path.join(td, "manifest.json"), encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
self.assertEqual(
|
||
data["objects"]["ZGRP/Z_FUNC"]["file"], "functions/zgrp/z_func.abap"
|
||
)
|
||
|
||
def test_corrupt_manifest_exception_swallowed(self):
|
||
from sapcli.commands.crud import _update_manifest_after_create
|
||
with tempfile.TemporaryDirectory() as td:
|
||
with open(os.path.join(td, "manifest.json"), "w", encoding="utf-8") as f:
|
||
f.write("BAD")
|
||
_update_manifest_after_create(
|
||
argparse.Namespace(project_path=td), "ZTEST", "report", None
|
||
)
|
||
|
||
|
||
# ═══════════════════════════════════════════
|
||
# _create_ddic — 各类型 + 默认模板 + 异常
|
||
# ═══════════════════════════════════════════
|
||
|
||
class TestCreateDdic(unittest.TestCase):
|
||
def _run_create(self, obj_type, name, definition_file=None):
|
||
from sapcli.commands.crud import cmd_create
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
client.create_ddic_object.return_value = ("/uri", "/uri/source")
|
||
with patch("sapcli.commands.crud.Manifest"):
|
||
cmd_create(
|
||
_args(type=obj_type, name=name, source=None,
|
||
definition=definition_file, description="d",
|
||
package="$TMP", corr_nr="DEVK001"),
|
||
client,
|
||
)
|
||
return client
|
||
|
||
def _write_def(self, payload):
|
||
f = tempfile.NamedTemporaryFile(
|
||
mode="w", suffix=".json", delete=False, encoding="utf-8"
|
||
)
|
||
json.dump(payload, f)
|
||
f.close()
|
||
return f.name
|
||
|
||
def test_structure_with_definition(self):
|
||
from sapcli.commands.crud import cmd_create
|
||
deffile = self._write_def({
|
||
"fields": [{"name": "F1", "type": "char10"}],
|
||
"enhancement_category": "#NOT_CLASSIFIED",
|
||
})
|
||
try:
|
||
client = self._run_create("structure", "ZSTRUCT", deffile)
|
||
client.create_ddic_object.assert_called_once()
|
||
finally:
|
||
os.unlink(deffile)
|
||
|
||
def test_tabletype_with_definition(self):
|
||
from sapcli.commands.crud import cmd_create
|
||
deffile = self._write_def({
|
||
"line_type": "ZSTRUCT",
|
||
"key_type": "#USER_DEFINED",
|
||
"access_mode": "#STANDARD",
|
||
})
|
||
try:
|
||
client = self._run_create("tabletype", "ZTT", deffile)
|
||
client.create_ddic_object.assert_called_once()
|
||
finally:
|
||
os.unlink(deffile)
|
||
|
||
def test_domain_default_template(self):
|
||
"""domain 无 definition → 默认 DomainDefinition 模板。"""
|
||
client = self._run_create("domain", "ZDOM", None)
|
||
client.create_ddic_object.assert_called_once()
|
||
body = client.create_ddic_object.call_args[0][2]
|
||
# 默认模板应生成非空 XML body
|
||
self.assertIsInstance(body, str)
|
||
self.assertTrue(len(body) > 0)
|
||
|
||
def test_table_default_template(self):
|
||
"""table 无 definition → 默认 TableDefinition 模板。"""
|
||
client = self._run_create("table", "ZTAB", None)
|
||
client.create_ddic_object.assert_called_once()
|
||
|
||
def test_dataelement_default_template(self):
|
||
"""dataelement 无 definition → 默认模板。"""
|
||
client = self._run_create("dataelement", "ZDE", None)
|
||
client.create_ddic_object.assert_called_once()
|
||
|
||
def test_structure_default_template(self):
|
||
"""structure 无 definition → 默认 StructureDefinition 模板。"""
|
||
client = self._run_create("structure", "ZSTRUCT", None)
|
||
client.create_ddic_object.assert_called_once()
|
||
|
||
def test_create_ddic_object_raises(self):
|
||
"""create_ddic_object 失败 → CreateError。"""
|
||
from sapcli.commands.crud import cmd_create
|
||
client = _make_client()
|
||
client.object_exists.return_value = False
|
||
client.create_ddic_object.side_effect = Exception("ddic fail")
|
||
with patch("sapcli.commands.crud.Manifest"):
|
||
with self.assertRaises(CreateError):
|
||
cmd_create(
|
||
_args(type="domain", name="ZDOM", source=None,
|
||
definition=None, description="d",
|
||
package="$TMP", corr_nr="DEVK001"),
|
||
client,
|
||
)
|
||
|
||
def test_domain_with_definition_fix_values(self):
|
||
"""domain definition 含 fix_values → 仍走 to_xml。"""
|
||
from sapcli.commands.crud import cmd_create
|
||
deffile = self._write_def({
|
||
"datatype": "CHAR", "length": 1,
|
||
"fix_values": [{"low": "A", "text": "Active"}],
|
||
})
|
||
try:
|
||
client = self._run_create("domain", "ZDOM", deffile)
|
||
client.create_ddic_object.assert_called_once()
|
||
finally:
|
||
os.unlink(deffile)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main(verbosity=2)
|