"""client.py 单元测试 — 30 个 ADT API 方法
运行: python tests/unit/test_client.py
"""
from __future__ import annotations
import os
import sys
import unittest
import xml.etree.ElementTree as ET
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 (
LoginError, SapCliError, CreateError, DeleteError,
ActivationError, SyntaxCheckError, LockError,
)
# ADT XML 命名空间
TM_NS = "http://www.sap.com/cts/adt/tm"
ADTCORE_NS = "http://www.sap.com/adt/core"
CHK_NS = "http://www.sap.com/adt/checklist"
LOCK_NS = "http://www.sap.com/adt/lock"
def _make_client() -> ADTClient:
"""创建 mock 好的 ADTClient。"""
client = ADTClient.__new__(ADTClient)
client.host = "https://sap.example.com"
client.sap_client = "100"
client.user = "TESTUSER"
client.password = "TESTPASS"
client.csrf_token = "test-csrf-token"
client.session = MagicMock()
client._stateful = False
return client
def _mock_resp(status_code=200, text="", content=b"", headers=None):
"""快速创建 mock response。"""
resp = MagicMock()
resp.status_code = status_code
resp.text = text
resp.content = content
resp.headers = headers or {}
resp.raise_for_status = MagicMock()
return resp
# ═══════════════════════════════════════════
# 基础方法
# ═══════════════════════════════════════════
class TestHeaders(unittest.TestCase):
def test_default(self):
h = _make_client()._headers()
self.assertEqual(h["content-type"], "application/xml")
self.assertEqual(h["x-csrf-token"], "test-csrf-token")
def test_custom(self):
h = _make_client()._headers("text/plain")
self.assertEqual(h["content-type"], "text/plain")
class TestContextManager(unittest.TestCase):
def test_enter_calls_login(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, headers={"x-csrf-token": "TOK"})
result = client.__enter__()
self.assertIs(result, client)
def test_exit_closes_session(self):
client = _make_client()
client.__exit__(None, None, None)
client.session.close.assert_called_once()
# ═══════════════════════════════════════════
# 认证
# ═══════════════════════════════════════════
class TestLogin(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, headers={"x-csrf-token": "NEW_TOK"})
self.assertTrue(client.login())
self.assertEqual(client.csrf_token, "NEW_TOK")
def test_failure(self):
client = _make_client()
client.session.get.return_value = _mock_resp(401)
with self.assertRaises(LoginError):
client.login()
# ═══════════════════════════════════════════
# 对象存在检查
# ═══════════════════════════════════════════
class TestObjectExists(unittest.TestCase):
def test_exists(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200)
self.assertTrue(client.object_exists("/uri"))
def test_not_exists(self):
client = _make_client()
client.session.get.return_value = _mock_resp(404)
self.assertFalse(client.object_exists("/uri"))
# ═══════════════════════════════════════════
# 源码读取
# ═══════════════════════════════════════════
class TestGetSource(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, text="REPORT ztest.")
self.assertEqual(client.get_source("/src"), "REPORT ztest.")
def test_not_found(self):
client = _make_client()
mock = _mock_resp(404)
mock.raise_for_status.side_effect = Exception("404")
client.session.get.return_value = mock
with self.assertRaises(Exception):
client.get_source("/nonexistent")
# ═══════════════════════════════════════════
# 锁管理
# ═══════════════════════════════════════════
class TestLock(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200, content=(
f''
f'lh_123'
f'DEVK001'
).encode())
handle, corr = client.lock("/uri")
self.assertEqual(handle, "lh_123")
self.assertEqual(corr, "DEVK001")
def test_failure(self):
client = _make_client()
client.session.post.return_value = _mock_resp(403)
with self.assertRaises(LockError):
client.lock("/uri")
def test_no_handle(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200, content=(
f''
).encode())
with self.assertRaises(LockError):
client.lock("/uri")
class TestUnlock(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200)
self.assertTrue(client.unlock("/uri", "lh"))
def test_failure(self):
client = _make_client()
client.session.post.return_value = _mock_resp(500)
self.assertFalse(client.unlock("/uri", "lh"))
# ═══════════════════════════════════════════
# 源码写入
# ═══════════════════════════════════════════
class TestSetSource(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.put.return_value = _mock_resp(200)
self.assertTrue(client.set_source("/src", "code", "lh"))
def test_with_corr_nr(self):
client = _make_client()
client.session.put.return_value = _mock_resp(200)
self.assertTrue(client.set_source("/src", "code", "lh", corr_nr="DEVK001"))
# ═══════════════════════════════════════════
# 传输请求
# ═══════════════════════════════════════════
_TRANSPORT_XML = (
f''
f''
f''
f''
).encode()
_TRANSPORT_EMPTY = (
f''
f''
).encode()
# create_transport_request 返回的 XML(带 tm:number 属性)
_CREATE_TRANSPORT_RESP = (
f''
f''
f''
f''
).encode()
class TestGetTransportRequest(unittest.TestCase):
def test_found(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=_TRANSPORT_XML)
self.assertEqual(client.get_transport_request(), "DEVK001")
def test_none(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=_TRANSPORT_EMPTY)
self.assertIsNone(client.get_transport_request())
class TestListTransportRequests(unittest.TestCase):
def test_found(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=_TRANSPORT_XML)
self.assertIsInstance(client.list_transport_requests(), list)
def test_empty(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=_TRANSPORT_EMPTY)
self.assertEqual(client.list_transport_requests(), [])
class TestCreateTransportRequest(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(201, content=_CREATE_TRANSPORT_RESP)
result = client.create_transport_request("My request")
self.assertEqual(result, "DEVK002")
def test_failure(self):
client = _make_client()
client.session.post.return_value = _mock_resp(500, text="Error")
with self.assertRaises(CreateError):
client.create_transport_request("Fail")
# ═══════════════════════════════════════════
# 激活 — 解析 type="E" 属性
# ═══════════════════════════════════════════
class TestActivate(unittest.TestCase):
def test_success_empty(self):
"""空响应视为成功。"""
client = _make_client()
client.session.post.return_value = _mock_resp(200, text="", content=b"")
ok, msgs = client.activate("ztest", "/uri")
self.assertTrue(ok)
def test_failure_with_error_type(self):
"""包含 type="E" 属性的元素视为失败。"""
client = _make_client()
xml_body = (
f''
f''
f''
f'Activation error'
f''
f''
)
client.session.post.return_value = _mock_resp(200, text=xml_body, content=xml_body.encode())
ok, msgs = client.activate("ztest", "/uri")
self.assertFalse(ok)
def test_http_error(self):
client = _make_client()
client.session.post.return_value = _mock_resp(500)
with self.assertRaises(ActivationError):
client.activate("ztest", "/uri")
# ── NW 7.40 double-activate workaround(空响应验证路径)──────────
def _verify_xml(self, version):
"""构造 verify GET 响应的 XML(bytes),adtcore:version 指定。"""
return (
f''
f''
f''
).encode()
def test_empty_response_already_active(self):
"""空响应 + verify 显示 active → 成功,无需重试(post 仅 1 次)。"""
client = _make_client()
# 激活 POST → 200 空响应
client.session.post.return_value = _mock_resp(200, text="", content=b"")
# verify GET → 200, version="active"
client.session.get.return_value = _mock_resp(200, content=self._verify_xml("active"))
ok, msgs = client.activate("ztest", "/uri")
self.assertTrue(ok)
self.assertEqual(msgs, [])
# 区分真/假实现:已 active 时不应重试激活
self.assertEqual(client.session.post.call_count, 1)
def test_double_activate_success(self):
"""空响应 + inactive → 重试一次 → active(double-activate 成功路径)。"""
client = _make_client()
# 激活 POST + 重试 POST → 均 200 空
client.session.post.side_effect = [
_mock_resp(200, text="", content=b""),
_mock_resp(200, text="", content=b""),
]
# verify GET → inactive;再 verify GET → active
client.session.get.side_effect = [
_mock_resp(200, content=self._verify_xml("inactive")),
_mock_resp(200, content=self._verify_xml("active")),
]
ok, msgs = client.activate("ztest", "/uri")
self.assertTrue(ok)
self.assertEqual(msgs, [])
# 区分真/假实现:确实重试了,post 被调用 2 次
self.assertEqual(client.session.post.call_count, 2)
# 两次 verify GET
self.assertEqual(client.session.get.call_count, 2)
def test_double_activate_still_inactive(self):
"""重试后仍 inactive → 失败并返回 SE09 提示(post 调用 2 次)。"""
client = _make_client()
client.session.post.side_effect = [
_mock_resp(200, text="", content=b""),
_mock_resp(200, text="", content=b""),
]
# 两次 verify GET 都返回 inactive
client.session.get.side_effect = [
_mock_resp(200, content=self._verify_xml("inactive")),
_mock_resp(200, content=self._verify_xml("inactive")),
]
ok, msgs = client.activate("ztest", "/uri")
self.assertFalse(ok)
# 重试过一次,共 2 次 POST
self.assertEqual(client.session.post.call_count, 2)
# 返回的消息含 SE09 或 inactive 提示
msg_text = " ".join(m.get("text", "") for m in msgs)
self.assertTrue("SE09" in msg_text or "inactive" in msg_text)
# ═══════════════════════════════════════════
# 语法检查 — 同样解析 type="E"
# ═══════════════════════════════════════════
class TestSyntaxCheck(unittest.TestCase):
def test_clean(self):
"""空响应视为通过。"""
client = _make_client()
client.session.post.return_value = _mock_resp(200, text="", content=b"")
ok, msgs = client.syntax_check("ztest", "/uri")
self.assertTrue(ok)
def test_with_errors(self):
client = _make_client()
xml_body = (
f''
f''
f''
f'Syntax error at line 1'
f''
f''
)
client.session.post.return_value = _mock_resp(200, text=xml_body, content=xml_body.encode())
ok, msgs = client.syntax_check("ztest", "/uri")
self.assertFalse(ok)
def test_http_error(self):
client = _make_client()
client.session.post.return_value = _mock_resp(500)
with self.assertRaises(SyntaxCheckError):
client.syntax_check("ztest", "/uri")
# ═══════════════════════════════════════════
# 删除对象(内部调 lock)
# ═══════════════════════════════════════════
class TestDeleteObject(unittest.TestCase):
def _mock_lock_ok(self, client):
client.session.post.return_value = _mock_resp(200, content=(
f'lh_del'
).encode())
def test_success_200(self):
client = _make_client()
self._mock_lock_ok(client)
client.session.delete.return_value = _mock_resp(200)
ok, msg = client.delete_object("/uri")
self.assertTrue(ok)
def test_success_204(self):
client = _make_client()
self._mock_lock_ok(client)
client.session.delete.return_value = _mock_resp(204)
ok, msg = client.delete_object("/uri")
self.assertTrue(ok)
def test_failure(self):
client = _make_client()
self._mock_lock_ok(client)
client.session.delete.return_value = _mock_resp(500, text="Cannot delete")
with self.assertRaises(DeleteError):
client.delete_object("/uri")
def test_ddic_lock_uses_special_accept_header(self):
"""delete 调用 lock 时传 DDIC lock 专用 Accept 头(缺陷4)。
DDIC 对象(domain/dataelement/table/structure)的 lock 端点对默认
Accept: */* 返回 HTTP 406。参考 abap-adt-api objectcontents.ts 的 lock
实现,需用 application/*,application/vnd.sap.as+xml;charset=UTF-8;
dataname=com.sap.adt.lock.result。
"""
client = _make_client()
self._mock_lock_ok(client)
client.session.delete.return_value = _mock_resp(204)
client.delete_object("/uri")
# lock 的 POST 请求必须携带 DDIC lock 专用 Accept 头。
# 注意:delete 成功后会再发一次 unlock 的 POST,故不能用 call_args
# (那是最后一次调用 = unlock),须从调用列表中挑出 _action=LOCK 的请求。
lock_calls = [
kw for _, kw in client.session.post.call_args_list
if kw.get("params", {}).get("_action") == "LOCK"
]
self.assertEqual(len(lock_calls), 1, "delete 应恰好发起一次 LOCK 请求")
accept = lock_calls[0]["headers"]["Accept"]
self.assertIn("com.sap.adt.lock.result", accept)
self.assertNotEqual(accept, "*/*")
# ═══════════════════════════════════════════
# 创建对象
# ═══════════════════════════════════════════
class TestCreateObject(unittest.TestCase):
def test_report_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(201, headers={
"Location": "/sap/bc/adt/programs/programs/ztest"
})
obj_uri, src_uri = client.create_object("report", "ZTEST", "Test report")
self.assertIn("ztest", obj_uri)
def test_class_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(201, headers={
"Location": "/sap/bc/adt/oo/classes/zcl_test"
})
obj_uri, src_uri = client.create_object("class", "ZCL_TEST", "Test class")
self.assertIn("zcl_test", obj_uri)
def test_with_source(self):
"""有 source → lock → set_source → unlock → activate。"""
client = _make_client()
lock_resp = _mock_resp(200, content=(
f'lh'
).encode())
create_resp = _mock_resp(201, headers={"Location": "/sap/bc/adt/programs/programs/ztest"})
unlock_resp = _mock_resp(200)
activate_resp = _mock_resp(200, text="", content=b"")
client.session.post.side_effect = [create_resp, lock_resp, unlock_resp, activate_resp]
client.session.put.return_value = _mock_resp(200)
obj_uri, src_uri = client.create_object("report", "ZTEST", "Test", source="REPORT ztest.")
self.assertIn("ztest", obj_uri)
def test_failure(self):
client = _make_client()
client.session.post.return_value = _mock_resp(409, text="Already exists")
with self.assertRaises(CreateError):
client.create_object("report", "ZTEST", "Test")
class TestBuildCreateBody(unittest.TestCase):
def test_report(self):
body = _make_client()._build_create_body("report", "ZTEST", "Desc")
self.assertIn("ZTEST", body)
def test_class(self):
body = _make_client()._build_create_body("class", "ZCL_TEST", "Desc")
self.assertIn("ZCL_TEST", body)
def test_unsupported(self):
with self.assertRaises(ValueError):
_make_client()._build_create_body("unknown", "ZTEST", "Desc")
# ═══════════════════════════════════════════
# DDIC 对象 — definition_body 是 XML 字符串
# ═══════════════════════════════════════════
_DDIC_DOMAIN_XML = (
f''
f''
f''
f''
)
class TestCreateDDICObject(unittest.TestCase):
def test_domain_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(201)
# mock activate(create_ddic_object 内部调用)
client.session.post.side_effect = [
_mock_resp(201), # create POST
_mock_resp(200, text="", content=b""), # activate POST
]
obj_uri, src_uri = client.create_ddic_object("domain", "ZTEST_DOM", _DDIC_DOMAIN_XML)
self.assertIn("ztest_dom", obj_uri)
def test_table_success(self):
"""table 类型先 POST 创建实体,再 PUT 写入 DDL(缺陷3 修复后)。"""
client = _make_client()
client.session.post.return_value = _mock_resp(201)
# mock _put_ddl_source
with patch.object(client, "_put_ddl_source") as mock_ddl, \
patch.object(client, "activate", return_value=(True, [])):
obj_uri, src_uri = client.create_ddic_object(
"table", "ZTEST_TAB", ""
)
# 先 POST 创建实体,再 PUT DDL
self.assertTrue(client.session.post.called)
mock_ddl.assert_called_once()
self.assertIn("ztest_tab", obj_uri)
def test_create_failure(self):
client = _make_client()
client.session.post.return_value = _mock_resp(500, text="Error")
with self.assertRaises(CreateError):
client.create_ddic_object("domain", "ZTEST", _DDIC_DOMAIN_XML)
# ═══════════════════════════════════════════
# 对象状态
# ═══════════════════════════════════════════
class TestGetObjectStatus(unittest.TestCase):
def test_active(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=(
f''
f''
f''
f''
).encode())
self.assertTrue(client.get_object_status("/uri")["exists"])
def test_not_exists(self):
client = _make_client()
client.session.get.return_value = _mock_resp(404)
self.assertFalse(client.get_object_status("/uri")["exists"])
# ═══════════════════════════════════════════
# 函数组
# ═══════════════════════════════════════════
class TestFunctionGroup(unittest.TestCase):
def test_exists_true(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200)
self.assertTrue(client.function_group_exists("ZGROUP"))
def test_exists_false(self):
client = _make_client()
client.session.get.return_value = _mock_resp(404)
self.assertFalse(client.function_group_exists("ZGROUP"))
def test_create_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(201)
self.assertTrue(client.create_function_group("ZGROUP"))
def test_create_failure(self):
client = _make_client()
client.session.post.return_value = _mock_resp(500, text="Error")
with self.assertRaises(CreateError):
client.create_function_group("ZGROUP")
# ═══════════════════════════════════════════
# 搜索浏览
# ═══════════════════════════════════════════
_LIST_TWO = (
f''
f''
f''
f''
f''
f''
f''
).encode()
_LIST_EMPTY = (
f''
f''
).encode()
class TestListObjects(unittest.TestCase):
def test_found(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=_LIST_TWO)
result = client.list_objects(obj_type="report")
self.assertEqual(len(result), 2)
self.assertEqual(result[0]["name"], "Z_TEST1")
def test_empty(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=_LIST_EMPTY)
self.assertEqual(client.list_objects(), [])
def test_quickssearch_params(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=_LIST_TWO)
client.list_objects(obj_type="PROG/P", prefix="Z_TEST")
_, kwargs = client.session.get.call_args
params = kwargs["params"]
self.assertEqual(params["operation"], "quickSearch")
self.assertEqual(params["query"], "Z_TEST")
self.assertEqual(params["maxResults"], "200")
self.assertEqual(params["objectType"], "PROG/P")
for bad in ("maxrow", "name", "type"):
self.assertNotIn(bad, params)
def test_strips_type_label_from_name(self):
client = _make_client()
xml = (
f''
f''
f''
f''
).encode()
client.session.get.return_value = _mock_resp(200, content=xml)
result = client.list_objects()
self.assertEqual(result[0]["name"], "ZMM_BIP_001_HEADER")
class TestWhereUsed(unittest.TestCase):
def test_found(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200, content=(
f''
f''
f''
f''
).encode())
result = client.where_used("Z_TEST", "/uri", "PROG/P")
self.assertEqual(len(result), 1)
def test_empty(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200, content=_LIST_EMPTY)
self.assertEqual(client.where_used("Z", "/uri"), [])
class TestSearchCode(unittest.TestCase):
def test_found(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200, content=(
f''
f''
f''
f''
).encode())
self.assertEqual(len(client.search_code("SELECT", obj_type="report")), 1)
def test_empty(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200, content=_LIST_EMPTY)
self.assertEqual(client.search_code("NONEXISTENT"), [])
class TestReadSourceForDiff(unittest.TestCase):
def test_success(self):
client = _make_client()
with patch.object(client, "get_source", return_value="REPORT ztest."):
self.assertEqual(client.read_source_for_diff("ZTEST", "report"), "REPORT ztest.")
def test_no_src_uri_raises(self):
client = _make_client()
with self.assertRaises(SapCliError):
client.read_source_for_diff("ZTEST", "functiongroup")
# ═══════════════════════════════════════════
# CDS
# ═══════════════════════════════════════════
class TestGetCdsSource(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, text="define view Z_TEST as select from mara { mara.matnr };")
self.assertIn("define view", client.get_cds_source("Z_TEST"))
def test_not_found(self):
client = _make_client()
mock = _mock_resp(404)
mock.raise_for_status.side_effect = Exception("404")
client.session.get.return_value = mock
with self.assertRaises(Exception):
client.get_cds_source("NONEXISTENT")
class TestCreateCDS(unittest.TestCase):
def test_success(self):
client = _make_client()
lock_resp = _mock_resp(200, content=(
f'lh_cds'
).encode())
create_resp = _mock_resp(201, headers={"Location": "/sap/bc/adt/dds/ddl/sources/z_test"})
unlock_resp = _mock_resp(200)
client.session.post.side_effect = [create_resp, lock_resp, unlock_resp]
client.session.put.return_value = _mock_resp(200)
obj_uri, src_uri = client.create_cds("Z_TEST", "Test", "define view Z_TEST as select from mara { mara.matnr };")
self.assertIn("z_test", obj_uri)
def test_failure(self):
client = _make_client()
client.session.post.return_value = _mock_resp(500, text="Error")
with self.assertRaises(CreateError):
client.create_cds("Z_TEST", "Test", "ddl source")
# ═══════════════════════════════════════════
# 包管理
# ═══════════════════════════════════════════
class TestPackage(unittest.TestCase):
def test_create_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(201)
self.assertTrue(client.create_package("Z_TEST_PKG"))
def test_create_failure(self):
client = _make_client()
client.session.post.return_value = _mock_resp(500, text="Error")
with self.assertRaises(CreateError):
client.create_package("Z_TEST_PKG")
def test_get_info_success(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=(
f''
f''
f'Test'
).encode())
self.assertIsInstance(client.get_package_info("Z_TEST"), dict)
def test_get_info_not_found(self):
client = _make_client()
mock = _mock_resp(404)
mock.raise_for_status.side_effect = Exception("404")
client.session.get.return_value = mock
with self.assertRaises(Exception):
client.get_package_info("NONEXISTENT")
# ═══════════════════════════════════════════
# 传输管理
# ═══════════════════════════════════════════
class TestTransportInfo(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=(
f''
f''
f''
f''
).encode())
result = client.transport_info("DEVK001")
self.assertEqual(result["number"], "DEVK001")
def test_not_found(self):
client = _make_client()
mock = _mock_resp(404)
mock.raise_for_status.side_effect = Exception("404")
client.session.get.return_value = mock
with self.assertRaises(Exception):
client.transport_info("NONEXISTENT")
class TestTransportRelease(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200)
self.assertTrue(client.transport_release("DEVK001"))
def test_failure(self):
client = _make_client()
client.session.post.return_value = _mock_resp(500, text="Error")
with self.assertRaises(SapCliError):
client.transport_release("DEVK001")
class TestTransportObjects(unittest.TestCase):
def test_found(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=(
f''
f''
f''
f''
).encode())
self.assertIsInstance(client.transport_objects("DEVK001"), list)
def test_empty(self):
client = _make_client()
client.session.get.return_value = _mock_resp(200, content=(
f''
f''
).encode())
self.assertEqual(client.transport_objects("DEVK001"), [])
# ═══════════════════════════════════════════
# 代码质量
# ═══════════════════════════════════════════
class TestATCCheck(unittest.TestCase):
def test_clean(self):
client = _make_client()
xml_body = (
f''
f''
)
client.session.post.return_value = _mock_resp(200, text=xml_body, content=xml_body.encode())
ok, findings = client.atc_check("ZTEST", "/uri")
self.assertTrue(ok)
def test_with_findings(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200, content=(
f''
f''
f''
f'Unused variable'
f''
f''
).encode())
ok, findings = client.atc_check("ZTEST", "/uri")
self.assertFalse(ok)
class TestPrettyPrint(unittest.TestCase):
def test_success(self):
client = _make_client()
client.session.post.return_value = _mock_resp(200, text="FORMATTED CODE")
self.assertEqual(client.pretty_print("raw code"), "FORMATTED CODE")
def test_error(self):
"""pretty_print 用 raise_for_status() 抛 HTTPError。"""
client = _make_client()
mock = _mock_resp(500, text="Error")
mock.raise_for_status.side_effect = Exception("500 Server Error")
client.session.post.return_value = mock
with self.assertRaises(Exception):
client.pretty_print("raw code")
# ═══════════════════════════════════════════
# _build_create_body 各类型
# ═══════════════════════════════════════════
class TestBuildCreateBodyTypes(unittest.TestCase):
def _check(self, obj_type, name):
body = _make_client()._build_create_body(obj_type, name, f"Test {obj_type}")
self.assertIn(name, body)
def test_report(self): self._check("report", "ZTEST")
def test_class(self): self._check("class", "ZCL_TEST")
def test_interface(self): self._check("interface", "ZIF_TEST")
def test_domain(self): self._check("domain", "ZTEST_DOM")
def test_dataelement(self): self._check("dataelement", "ZTEST_DE")
def test_table(self): self._check("table", "ZTEST_TAB")
def test_structure(self): self._check("structure", "ZTEST_STR")
def test_tabletype(self): self._check("tabletype", "ZTEST_TT")
def test_unsupported(self):
with self.assertRaises(ValueError):
_make_client()._build_create_body("bogus_type", "ZTEST", "Test")
# ═══════════════════════════════════════════
# set_source 错误重试路径(行 59-69)
# ═══════════════════════════════════════════
class TestSetSourceRetry(unittest.TestCase):
def _error_body(self, corrnr):
return f'{corrnr}'
def test_retry_with_extracted_corrnr_succeeds(self):
"""首次 400 + 错误体含不同 corrNr → 用该 corrNr 重试成功。"""
client = _make_client()
client.session.put.side_effect = [
_mock_resp(400, text=self._error_body("DEVK999")),
_mock_resp(200),
]
self.assertTrue(client.set_source("/src", "code", "lh", corr_nr="DEVK001"))
self.assertEqual(client.session.put.call_count, 2)
def test_retry_fails_raises(self):
"""重试后仍 400 → raise_for_status。"""
client = _make_client()
first = _mock_resp(400, text=self._error_body("DEVK999"))
retry = _mock_resp(400)
retry.raise_for_status.side_effect = Exception("still 400")
client.session.put.side_effect = [first, retry]
with self.assertRaises(Exception):
client.set_source("/src", "code", "lh", corr_nr="DEVK001")
def test_no_corrnr_in_error_raises(self):
"""错误体无 corrNr → 不重试,直接 raise。"""
client = _make_client()
resp = _mock_resp(400, text="boom")
resp.raise_for_status.side_effect = Exception("400")
client.session.put.return_value = resp
with self.assertRaises(Exception):
client.set_source("/src", "code", "lh")
self.assertEqual(client.session.put.call_count, 1)
# ═══════════════════════════════════════════
# lock 高级路径(行 94, 101-107)
# ═══════════════════════════════════════════
class TestLockAdvanced(unittest.TestCase):
def test_lock_with_corr_nr(self):
"""传 corr_nr → 写入 params 并原样返回(不提取)。"""
client = _make_client()
client.session.post.return_value = _mock_resp(200, content=(
f'lh_123'
).encode())
handle, corr = client.lock("/uri", corr_nr="DEVK001")
self.assertEqual(handle, "lh_123")
self.assertEqual(corr, "DEVK001")
def test_lock_500_retry_with_existing_corrnr(self):
"""首次 500 + 错误体含 corrNr → 用该 corrNr 重试成功。"""
client = _make_client()
error_body = 'DEVK999'
client.session.post.side_effect = [
_mock_resp(500, text=error_body),
_mock_resp(200, content=(
f'lh'
).encode()),
]
handle, corr = client.lock("/uri", corr_nr="DEVK001")
self.assertEqual(handle, "lh")
self.assertEqual(corr, "DEVK999")
self.assertEqual(client.session.post.call_count, 2)
# ═══════════════════════════════════════════
# _extract_locked_corrnr(行 135-149)
# ═══════════════════════════════════════════
class TestExtractLockedCorrnr(unittest.TestCase):
def test_key_corrnr(self):
client = _make_client()
body = 'DEVK001'
self.assertEqual(client._extract_locked_corrnr(body), "DEVK001")
def test_star_skipped_no_message(self):
client = _make_client()
body = '*'
self.assertIsNone(client._extract_locked_corrnr(body))
def test_message_branch(self):
client = _make_client()
body = 'Object is locked in request DEVK888'
self.assertEqual(client._extract_locked_corrnr(body), "DEVK888")
def test_invalid_xml(self):
client = _make_client()
self.assertIsNone(client._extract_locked_corrnr("not valid xml <<"))
# ═══════════════════════════════════════════
# _extract_corrnr_from_lock_response(行 166, 172-175, 183-185)
# ═══════════════════════════════════════════
class TestExtractCorrnrFromLockResponse(unittest.TestCase):
def _extract(self, xml):
return _make_client()._extract_corrnr_from_lock_response(ET.fromstring(xml))
def test_strategy1_cornn_tag(self):
self.assertEqual(
self._extract('DEVK001'),
"DEVK001",
)
def test_strategy1_star_skipped(self):
self.assertIsNone(
self._extract('*'),
)
def test_strategy2_attr(self):
self.assertEqual(
self._extract(''),
"DEVK003",
)
def test_strategy2_tagname(self):
self.assertEqual(
self._extract('DEVK004'),
"DEVK004",
)
def test_strategy3_property_key(self):
self.assertEqual(
self._extract('DEVK005'),
"DEVK005",
)
def test_no_match_returns_none(self):
self.assertIsNone(self._extract('val'))
# ═══════════════════════════════════════════
# activate — inactivectsobjects 路径(行 235-265)
# ═══════════════════════════════════════════
class TestActivateInactiveCtsObjects(unittest.TestCase):
IOC_NS = "http://www.sap.com/abapxml/inactiveCtsObjects"
def _activate_resp(self, xml):
return _mock_resp(
200, text=xml, content=xml.encode(),
headers={"content-type": "application/vnd.sap.adt.inactivectsobjects+xml"},
)
def test_with_objects_via_fallback_ref(self):
"""ref 不在 adtcore ns → 走 endswith}ref 回退路径。"""
xml = (
f''
f''
f''
f''
f''
f''
)
client = _make_client()
client.session.post.return_value = self._activate_resp(xml)
ok, msgs = client.activate("ztest", "/uri")
self.assertFalse(ok)
self.assertTrue(any("Z_INACTIVE" in m["text"] for m in msgs))
def test_with_objects_via_adtcore_ref(self):
"""ref 在 adtcore ns → obj_elem.find 直接命中。"""
xml = (
f''
f''
f''
f''
f''
f''
)
client = _make_client()
client.session.post.return_value = self._activate_resp(xml)
ok, msgs = client.activate("ztest", "/uri")
self.assertFalse(ok)
self.assertTrue(any("Z_ADTC" in m["text"] for m in msgs))
def test_empty_no_objects(self):
"""inactivectsobjects 响应但无 entry → 视为成功。"""
xml = (
f''
f''
f''
)
client = _make_client()
client.session.post.return_value = self._activate_resp(xml)
ok, msgs = client.activate("ztest", "/uri")
self.assertTrue(ok)
self.assertEqual(msgs, [])
# ═══════════════════════════════════════════
# activate — corr_nr / msg 回退 / verify 异常(行 219, 316-317, 334-337)
# ═══════════════════════════════════════════
class TestActivateMisc(unittest.TestCase):
def test_activate_with_corr_nr(self):
"""传 corr_nr → 写入 activation params。"""
client = _make_client()
xml = f''
client.session.post.return_value = _mock_resp(200, text=xml, content=xml.encode())
ok, msgs = client.activate("ztest", "/uri", corr_nr="DEVK001")
self.assertTrue(ok)
self.assertEqual(msgs, [])
def test_msg_without_txt_uses_fallback_text(self):
"""msg 无 }txt 子元素 → 用第一个长文本后代(行 334-337)。"""
client = _make_client()
xml = (
f''
f''
f''
f'Some long activation failure text'
f''
f''
)
client.session.post.return_value = _mock_resp(200, text=xml, content=xml.encode())
ok, msgs = client.activate("ztest", "/uri")
self.assertFalse(ok)
self.assertTrue(msgs)
self.assertIn("Some long", msgs[0]["text"])
def test_empty_response_verify_raises(self):
"""空响应 + verify GET 抛异常 → 捕获后视为成功(行 316-317)。"""
client = _make_client()
client.session.post.return_value = _mock_resp(200, text="", content=b"")
client.session.get.side_effect = Exception("verify boom")
ok, msgs = client.activate("ztest", "/uri")
self.assertTrue(ok)
self.assertEqual(msgs, [])
# ═══════════════════════════════════════════
# syntax_check — msg 回退文本(行 390-393)
# ═══════════════════════════════════════════
class TestSyntaxCheckMsgFallback(unittest.TestCase):
def test_msg_without_txt_uses_fallback_text(self):
client = _make_client()
xml = (
f''
f''
f''
f'Another long syntax failure note'
f''
f''
)
client.session.post.return_value = _mock_resp(200, text=xml, content=xml.encode())
ok, msgs = client.syntax_check("ztest", "/uri")
self.assertFalse(ok)
self.assertTrue(msgs)
self.assertIn("Another long", msgs[0]["text"])
if __name__ == "__main__":
unittest.main(verbosity=2)