"""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'' b'') # ═══════════════════════════════════════════════════════════════ # crud.py — cmd_download # ═══════════════════════════════════════════════════════════════ class TestCmdDownloadObjectNotFound(unittest.TestCase): """cmd_download 对象不存在时报错。""" def test_object_not_found_raises(self): from sapcli.commands.crud import cmd_download from sapcli.exceptions import ObjectNotFoundError client = _make_client() client.object_exists.return_value = False with tempfile.TemporaryDirectory() as td: with self.assertRaises(ObjectNotFoundError) as ctx: cmd_download(_args(path=td), client) self.assertIn("ZTEST", str(ctx.exception)) def test_object_not_found_prints_hint(self): from sapcli.commands.crud import cmd_download from sapcli.exceptions import ObjectNotFoundError client = _make_client() client.object_exists.return_value = False with tempfile.TemporaryDirectory() as td: with patch("builtins.print") as mock_print: with self.assertRaises(ObjectNotFoundError): cmd_download(_args(path=td), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("不存在", printed) class TestCmdDownloadFileWriteFailure(unittest.TestCase): """cmd_download 文件写入失败(权限/路径不可写)。""" def test_write_permission_error(self): from sapcli.commands.crud import cmd_download client = _make_client() client.object_exists.return_value = True client.get_source.return_value = "REPORT ztest." # 令 open() 抛出 PermissionError with patch("builtins.open", side_effect=PermissionError("拒绝访问")): with self.assertRaises(PermissionError): cmd_download(_args(path="/tmp/some_dir"), client) # ═══════════════════════════════════════════════════════════════ # crud.py — cmd_create # ═══════════════════════════════════════════════════════════════ class TestCmdCreateWithSourceFile(unittest.TestCase): """cmd_create 有 --source 文件时。""" @patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001") @patch("sapcli.commands.crud.Manifest") def test_create_reads_source_file(self, MockManifest, mock_transport): from sapcli.commands.crud import cmd_create client = _make_client() client.object_exists.return_value = False client.create_object.return_value = ("/uri/ztest", "/uri/ztest/source/main") source_code = "REPORT ztest.\nWRITE: / 'hello'." with tempfile.NamedTemporaryFile( mode="w", suffix=".abap", delete=False, encoding="utf-8" ) as f: f.write(source_code) tmpfile = f.name try: cmd_create( _args(source=tmpfile, description="Test", package="$TMP", corr_nr=None), client, ) # create_object 的 source 参数应该来自文件内容 call_args = client.create_object.call_args self.assertEqual(call_args[0][4], source_code) finally: os.unlink(tmpfile) @patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001") @patch("sapcli.commands.crud.Manifest") def test_create_with_class_source(self, MockManifest, mock_transport): from sapcli.commands.crud import cmd_create client = _make_client() client.object_exists.return_value = False client.create_object.return_value = ("/uri/zcl_test", "/uri/zcl_test/source/main") source_code = "CLASS zcl_test DEFINITION PUBLIC. ENDCLASS." with tempfile.NamedTemporaryFile( mode="w", suffix=".abap", delete=False, encoding="utf-8" ) as f: f.write(source_code) tmpfile = f.name try: cmd_create( _args( type="class", name="ZCL_TEST", source=tmpfile, description="Test Class", package="$TMP", corr_nr=None, ), client, ) call_args = client.create_object.call_args self.assertEqual(call_args[0][4], source_code) finally: os.unlink(tmpfile) class TestCmdCreateWithDefinitionFile(unittest.TestCase): """cmd_create 有 --definition 文件时(DDIC 类型)。""" @patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001") @patch("sapcli.commands.crud.Manifest") def test_create_domain_with_definition(self, MockManifest, mock_transport): from sapcli.commands.crud import cmd_create client = _make_client() client.object_exists.return_value = False client.create_ddic_object.return_value = ("/uri/ztest_dom", "/uri/ztest_dom/source/main") # 创建一个临时 JSON 定义文件 definition = {"datatype": "CHAR", "length": 10, "decimals": 0} with tempfile.NamedTemporaryFile( mode="w", suffix=".json", delete=False, encoding="utf-8" ) as f: json.dump(definition, f) def_file = f.name try: cmd_create( _args( type="domain", name="ZTEST_DOM", source=None, definition=def_file, description="Test Domain", package="$TMP", corr_nr=None, ), client, ) client.create_ddic_object.assert_called_once() finally: os.unlink(def_file) @patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001") @patch("sapcli.commands.crud.Manifest") def test_create_dataelement_with_definition(self, MockManifest, mock_transport): from sapcli.commands.crud import cmd_create client = _make_client() client.object_exists.return_value = False client.create_ddic_object.return_value = ("/uri/ztest_de", "/uri/ztest_de/source/main") definition = {"datatype": "CHAR", "length": 20, "domain_name": "ZTEST_DOM"} with tempfile.NamedTemporaryFile( mode="w", suffix=".json", delete=False, encoding="utf-8" ) as f: json.dump(definition, f) def_file = f.name try: cmd_create( _args( type="dataelement", name="ZTEST_DE", source=None, definition=def_file, description="Test DE", package="$TMP", corr_nr=None, ), client, ) client.create_ddic_object.assert_called_once() finally: os.unlink(def_file) @patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001") @patch("sapcli.commands.crud.Manifest") def test_create_table_with_definition(self, MockManifest, mock_transport): from sapcli.commands.crud import cmd_create client = _make_client() client.object_exists.return_value = False client.create_ddic_object.return_value = ("/uri/ztest_tab", "/uri/ztest_tab/source/main") definition = { "fields": [ {"name": "KEY_FIELD", "type": "char10", "key": True, "not_null": True}, {"name": "VALUE", "type": "char20"}, ], "table_category": "#TRANSPARENT", "delivery_class": "#A", } with tempfile.NamedTemporaryFile( mode="w", suffix=".json", delete=False, encoding="utf-8" ) as f: json.dump(definition, f) def_file = f.name try: cmd_create( _args( type="table", name="ZTEST_TAB", source=None, definition=def_file, description="Test Table", package="$TMP", corr_nr=None, ), client, ) client.create_ddic_object.assert_called_once() finally: os.unlink(def_file) @patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001") def test_create_tabletype_requires_definition(self, mock_transport): """tabletype 没有 definition 时应报错。""" from sapcli.commands.crud import cmd_create from sapcli.exceptions import CreateError client = _make_client() client.object_exists.return_value = False with self.assertRaises(CreateError): cmd_create( _args( type="tabletype", name="ZTEST_TT", source=None, definition=None, description="Test TT", package="$TMP", corr_nr=None, ), client, ) @patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001") @patch("sapcli.commands.crud.Manifest") def test_create_functiongroup_success(self, MockManifest, mock_transport): from sapcli.commands.crud import cmd_create client = _make_client() client.function_group_exists.return_value = False client.create_function_group.return_value = "/uri/ztest_fg" cmd_create( _args( type="functiongroup", name="ZTEST_FG", source=None, description="Test FG", package="$TMP", corr_nr=None, ), client, ) client.create_function_group.assert_called_once() @patch("sapcli.commands.crud._select_transport_request", return_value="DEVK001") def test_create_functiongroup_already_exists(self, mock_transport): from sapcli.commands.crud import cmd_create from sapcli.exceptions import ObjectAlreadyExistsError client = _make_client() client.function_group_exists.return_value = True with self.assertRaises(ObjectAlreadyExistsError): cmd_create( _args( type="functiongroup", name="ZTEST_FG", source=None, description="Test FG", package="$TMP", corr_nr=None, ), client, ) # ═══════════════════════════════════════════════════════════════ # crud.py — cmd_info # ═══════════════════════════════════════════════════════════════ class TestCmdInfoObjectNotFound(unittest.TestCase): """cmd_info 对象不存在(HTTP 404)。""" def test_info_404_raises(self): from sapcli.commands.crud import cmd_info from sapcli.exceptions import ObjectNotFoundError client = _make_client() client.session.get.return_value = _mock_resp(404) with self.assertRaises(ObjectNotFoundError) as ctx: cmd_info(_args(), client) self.assertIn("ZTEST", str(ctx.exception)) def test_info_500_raises(self): from sapcli.commands.crud import cmd_info from sapcli.exceptions import SapCliError client = _make_client() client.session.get.return_value = _mock_resp(500, text="Internal Server Error") with self.assertRaises(SapCliError) as ctx: cmd_info(_args(), client) self.assertIn("500", str(ctx.exception)) def test_info_name_mismatch(self): """返回的名称与请求名称不匹配。""" from sapcli.commands.crud import cmd_info from sapcli.exceptions import SapCliError mismatched_xml = ( b'' b'' ) client = _make_client() client.session.get.return_value = _mock_resp(200, content=mismatched_xml) with self.assertRaises(SapCliError) as ctx: cmd_info(_args(), client) self.assertIn("不匹配", str(ctx.exception)) def test_info_http_406_fallback(self): """HTTP 406 时回退到 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'' b'' ) def test_request_found(self): from sapcli.commands.crud import cmd_info client = _make_client() client.session.get.return_value = _mock_resp(200, content=self._xml()) client.query_table_data.return_value = { "rows": [["DEVK901362"], ["DEVK901400"]] } with patch("builtins.print") as mock_print: cmd_info(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("DEVK901362", printed) self.assertIn("DEVK901400", printed) def test_no_request_shows_none(self): from sapcli.commands.crud import cmd_info client = _make_client() client.session.get.return_value = _mock_resp(200, content=self._xml()) client.query_table_data.return_value = {"rows": []} with patch("builtins.print") as mock_print: cmd_info(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("传输请求: 无", printed) def test_query_failure_graceful(self): from sapcli.commands.crud import cmd_info client = _make_client() client.session.get.return_value = _mock_resp(200, content=self._xml()) client.query_table_data.side_effect = Exception("boom") # 不应抛异常 with patch("builtins.print") as mock_print: cmd_info(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("(无法获取)", printed) def test_non_r3tr_type_skips_query(self): """include 非独立 R3TR 对象,不查 E071。""" from sapcli.commands.crud import cmd_info client = _make_client() client.session.get.return_value = _mock_resp( 200, content=self._xml(type_code="PROG/I") ) with patch("builtins.print") as mock_print: cmd_info(_args(type="include", name="ZTEST"), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("非独立传输对象", printed) client.query_table_data.assert_not_called() # ═══════════════════════════════════════════════════════════════ # crud.py — cmd_list(来自 search.py) # ═══════════════════════════════════════════════════════════════ class TestCmdListNoResults(unittest.TestCase): """cmd_list 无结果。""" def test_list_empty_results(self): from sapcli.commands.search import cmd_list client = _make_client() client.list_objects.return_value = [] with patch("builtins.print") as mock_print: cmd_list(argparse.Namespace(type="report", package=None, prefix=None, config=None), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("未找到", printed) def test_list_api_error(self): from sapcli.commands.search import cmd_list client = _make_client() client.list_objects.side_effect = Exception("Network error") # 应不抛异常,只打印错误 with patch("builtins.print") as mock_print: cmd_list(argparse.Namespace(type="report", package=None, prefix=None, config=None), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_list_with_results(self): from sapcli.commands.search import cmd_list client = _make_client() client.list_objects.return_value = [ {"name": "Z_PROG1", "type": "PROG/P", "package": "$TMP", "description": "Test 1"}, {"name": "Z_PROG2", "type": "PROG/P", "package": "$TMP", "description": "Test 2"}, ] with patch("builtins.print") as mock_print: cmd_list(argparse.Namespace(type="report", package=None, prefix=None, config=None), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("2", printed) def test_list_with_package_filter(self): from sapcli.commands.search import cmd_list client = _make_client() client.list_objects.return_value = [] cmd_list( argparse.Namespace(type="report", package="Z_MY_PKG", prefix=None, config=None), client, ) # 检查 list_objects 被调用时包含 package 参数 call_kwargs = client.list_objects.call_args[1] self.assertEqual(call_kwargs.get("package"), "Z_MY_PKG") # ═══════════════════════════════════════════════════════════════ # crud.py — cmd_whereused(来自 search.py) # ═══════════════════════════════════════════════════════════════ class TestCmdWhereUsedNoReferences(unittest.TestCase): """cmd_whereused 无引用。""" def test_whereused_empty(self): from sapcli.commands.search import cmd_whereused client = _make_client() client.where_used.return_value = [] with patch("builtins.print") as mock_print: cmd_whereused(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("未找到引用", printed) def test_whereused_api_error(self): from sapcli.commands.search import cmd_whereused client = _make_client() client.where_used.side_effect = Exception("Connection refused") with patch("builtins.print") as mock_print: cmd_whereused(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_whereused_with_results(self): from sapcli.commands.search import cmd_whereused client = _make_client() client.where_used.return_value = [ {"name": "Z_USER1", "type": "PROG/P", "package": "$TMP", "uri": "/prog/z_user1"}, ] with patch("builtins.print") as mock_print: cmd_whereused(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("1", printed) # ═══════════════════════════════════════════════════════════════ # crud.py — cmd_search(来自 search.py) # ═══════════════════════════════════════════════════════════════ class TestCmdSearchNoResults(unittest.TestCase): """cmd_search 无结果。""" def test_search_empty(self): from sapcli.commands.search import cmd_search client = _make_client() client.search_code.return_value = [] with patch("builtins.print") as mock_print: cmd_search(argparse.Namespace(query="SELECT", type=None, config=None), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("未找到", printed) def test_search_api_error(self): from sapcli.commands.search import cmd_search client = _make_client() client.search_code.side_effect = Exception("Timeout") with patch("builtins.print") as mock_print: cmd_search(argparse.Namespace(query="SELECT", type=None, config=None), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_search_with_type_filter(self): from sapcli.commands.search import cmd_search client = _make_client() client.search_code.return_value = [ {"name": "Z_RESULT", "type": "PROG/P", "description": "found"}, ] cmd_search( argparse.Namespace(query="WRITE", type="report", config=None), client, ) client.search_code.assert_called_once() # search_code 被调用时传入了 obj_type 参数 call_args = client.search_code.call_args # 它是 (query, obj_type=adt_type) 形式 obj_type_arg = call_args[1].get("obj_type") if obj_type_arg is None and len(call_args[0]) > 1: obj_type_arg = call_args[0][1] self.assertIsNotNone(obj_type_arg) # ═══════════════════════════════════════════════════════════════ # crud.py — cmd_format(来自 quality.py) # ═══════════════════════════════════════════════════════════════ class TestCmdFormatSuccess(unittest.TestCase): """cmd_format 格式化成功(完整流程)。""" def test_format_full_success(self): from sapcli.commands.quality import cmd_format client = _make_client() client.get_source.return_value = "REPORT ztest.\nWRITE: / 'hello'." client.pretty_print.return_value = "REPORT ztest.\n WRITE: / 'hello'." client.lock.return_value = ("lh_1", "DEVK001") client.set_source.return_value = True client.unlock.return_value = True client.activate.return_value = (True, []) with patch("builtins.print") as mock_print: cmd_format( argparse.Namespace(name="ZTEST", type="report", path=".", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("格式化完成", printed) client.pretty_print.assert_called_once() client.set_source.assert_called_once() client.activate.assert_called_once() def test_format_no_change(self): """格式化结果与原始一致,不写回。""" from sapcli.commands.quality import cmd_format client = _make_client() original = "REPORT ztest.\nWRITE: / 'hello'." client.get_source.return_value = original client.pretty_print.return_value = original # 无变化 with patch("builtins.print") as mock_print: cmd_format( argparse.Namespace(name="ZTEST", type="report", path=".", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("无需修改", printed) client.set_source.assert_not_called() def test_format_get_source_fails(self): from sapcli.commands.quality import cmd_format client = _make_client() client.get_source.side_effect = Exception("Read error") with patch("builtins.print") as mock_print: cmd_format( argparse.Namespace(name="ZTEST", type="report", path=".", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_format_unsupported_type(self): """functiongroup 没有 src_uri,应提前返回。""" from sapcli.commands.quality import cmd_format client = _make_client() with patch("builtins.print") as mock_print: cmd_format( argparse.Namespace(name="ZTEST_FG", type="functiongroup", path=".", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("不支持", printed) client.get_source.assert_not_called() # ═══════════════════════════════════════════════════════════════ # cds.py — cds download # ═══════════════════════════════════════════════════════════════ class TestCdsDownload(unittest.TestCase): """cds download 成功。""" def test_cds_download_success(self): from sapcli.commands.cds import cmd_cds client = _make_client() ddl_source = "define view z_test as select from sflight { carrid, connid, fldate };" client.get_cds_source.return_value = ddl_source with tempfile.TemporaryDirectory() as td: cmd_cds( argparse.Namespace(cds_action="download", name="Z_TEST", path=td, config=None), client, ) expected_file = os.path.join(td, "z_test.ddl") self.assertTrue(os.path.isfile(expected_file)) with open(expected_file, "r", encoding="utf-8") as f: content = f.read() self.assertEqual(content, ddl_source) def test_cds_download_api_error(self): from sapcli.commands.cds import cmd_cds client = _make_client() client.get_cds_source.side_effect = Exception("Not found") with patch("builtins.print") as mock_print: cmd_cds( argparse.Namespace(cds_action="download", name="Z_MISSING", path=".", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_cds_download_creates_dir(self): from sapcli.commands.cds import cmd_cds client = _make_client() client.get_cds_source.return_value = "define view z_test as select from mara { matnr };" with tempfile.TemporaryDirectory() as td: subdir = os.path.join(td, "new_dir") cmd_cds( argparse.Namespace(cds_action="download", name="Z_TEST", path=subdir, config=None), client, ) self.assertTrue(os.path.isfile(os.path.join(subdir, "z_test.ddl"))) # ═══════════════════════════════════════════════════════════════ # cds.py — cds create # ═══════════════════════════════════════════════════════════════ class TestCdsCreate(unittest.TestCase): """cds create 有 ddl_path 时。""" def test_cds_create_with_ddl_file(self): from sapcli.commands.cds import cmd_cds client = _make_client() client.create_cds.return_value = ("/uri/z_test", "/uri/z_test/source/main") ddl_content = "@AbapCatalog.sqlViewName: 'ZTEST'\ndefine view z_test as select from sflight { carrid };" with tempfile.NamedTemporaryFile( mode="w", suffix=".ddl", delete=False, encoding="utf-8" ) as f: f.write(ddl_content) ddl_file = f.name try: with tempfile.TemporaryDirectory() as td: cmd_cds( argparse.Namespace( cds_action="create", name="Z_TEST", description="Test CDS", ddl_path=ddl_file, path=td, config=None, ), client, ) client.create_cds.assert_called_once() # 验证 CDS 创建时传入了文件内容 call_args = client.create_cds.call_args self.assertIn(ddl_content, call_args[0]) finally: os.unlink(ddl_file) def test_cds_create_default_template(self): from sapcli.commands.cds import cmd_cds client = _make_client() client.create_cds.return_value = ("/uri/z_test", "/uri/z_test/source/main") with tempfile.TemporaryDirectory() as td: cmd_cds( argparse.Namespace( cds_action="create", name="Z_TEST", description="Test CDS", ddl_path=None, path=td, config=None, ), client, ) client.create_cds.assert_called_once() # 检查默认模板被使用 call_args = client.create_cds.call_args ddl_arg = call_args[0][2] self.assertIn("define view", ddl_arg) def test_cds_create_failure(self): from sapcli.commands.cds import cmd_cds client = _make_client() client.create_cds.side_effect = Exception("Already exists") with patch("builtins.print") as mock_print: cmd_cds( argparse.Namespace( cds_action="create", name="Z_DUP", description="Duplicate", ddl_path=None, path=".", config=None, ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) # ═══════════════════════════════════════════════════════════════ # cds.py — cds sync # ═══════════════════════════════════════════════════════════════ class TestCdsSync(unittest.TestCase): """cds sync 成功。""" def test_cds_sync_existing_object(self): from sapcli.commands.cds import cmd_cds client = _make_client() client.object_exists.return_value = True client.lock.return_value = ("lh_1", "DEVK001") client.set_source.return_value = True client.unlock.return_value = True client.activate.return_value = (True, []) ddl_content = "define view z_test as select from sflight { carrid };" with tempfile.NamedTemporaryFile( mode="w", suffix=".ddl", delete=False, encoding="utf-8" ) as f: f.write(ddl_content) ddl_file = f.name try: with patch("builtins.print") as mock_print: cmd_cds( argparse.Namespace( cds_action="sync", name="Z_TEST", path=ddl_file, config=None, ), client, ) client.lock.assert_called_once() client.set_source.assert_called_once() client.activate.assert_called_once() finally: os.unlink(ddl_file) def test_cds_sync_nonexistent_object_creates(self): """CDS 不存在时自动创建。""" from sapcli.commands.cds import cmd_cds client = _make_client() client.object_exists.return_value = False client.create_cds.return_value = ("/uri/z_test", "/uri/z_test/source/main") ddl_content = "define view z_test as select from sflight { carrid };" with tempfile.NamedTemporaryFile( mode="w", suffix=".ddl", delete=False, encoding="utf-8" ) as f: f.write(ddl_content) ddl_file = f.name try: with patch("builtins.print") as mock_print: cmd_cds( argparse.Namespace( cds_action="sync", name="Z_TEST", path=ddl_file, config=None, ), client, ) client.create_cds.assert_called_once() finally: os.unlink(ddl_file) def test_cds_sync_ddl_file_not_found(self): from sapcli.commands.cds import cmd_cds client = _make_client() with patch("builtins.print") as mock_print: cmd_cds( argparse.Namespace( cds_action="sync", name="Z_TEST", path="/nonexistent/file.ddl", config=None, ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("不存在", printed) def test_cds_sync_activation_failure(self): from sapcli.commands.cds import cmd_cds client = _make_client() client.object_exists.return_value = True client.lock.return_value = ("lh_1", "DEVK001") client.set_source.return_value = True client.unlock.return_value = True client.activate.return_value = ( False, [{"type": "E", "line": "5", "text": "Syntax error"}], ) ddl_content = "define view z_test as select from sflight { invalid_field };" with tempfile.NamedTemporaryFile( mode="w", suffix=".ddl", delete=False, encoding="utf-8" ) as f: f.write(ddl_content) ddl_file = f.name try: with patch("builtins.print") as mock_print: cmd_cds( argparse.Namespace( cds_action="sync", name="Z_TEST", path=ddl_file, config=None, ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("激活失败", printed) finally: os.unlink(ddl_file) def test_cds_unknown_action(self): from sapcli.commands.cds import cmd_cds client = _make_client() with patch("builtins.print") as mock_print: cmd_cds( argparse.Namespace(cds_action=None, name="Z_TEST", path=".", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("用法", printed) # ═══════════════════════════════════════════════════════════════ # transport.py — transport list # ═══════════════════════════════════════════════════════════════ class TestTransportList(unittest.TestCase): """transport list 成功。""" def test_transport_list_with_requests(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.list_transport_requests.return_value = [ {"number": "DEVK001", "description": "Dev request 1", "owner": "USER1"}, {"number": "DEVK002", "description": "Dev request 2", "owner": "USER2"}, ] with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace(transport_action="list", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("2", printed) self.assertIn("DEVK001", printed) def test_transport_list_empty(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.list_transport_requests.return_value = [] with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace(transport_action="list", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("未找到", printed) def test_transport_list_api_error(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.list_transport_requests.side_effect = Exception("Connection failed") with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace(transport_action="list", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) # ═══════════════════════════════════════════════════════════════ # transport.py — transport info # ═══════════════════════════════════════════════════════════════ class TestTransportInfo(unittest.TestCase): """transport info 成功。""" def test_transport_info_success(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.transport_info.return_value = { "number": "DEVK001", "description": "Transport for feature X", "status": "D", "owner": "TESTUSER", } with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace( transport_action="info", corr_nr="DEVK001", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("feature X", printed) self.assertIn("TESTUSER", printed) client.transport_info.assert_called_once_with("DEVK001") def test_transport_info_api_error(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.transport_info.side_effect = Exception("Not found") with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace( transport_action="info", corr_nr="DEVK999", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) # ═══════════════════════════════════════════════════════════════ # transport.py — transport release # ═══════════════════════════════════════════════════════════════ class TestTransportRelease(unittest.TestCase): """transport release 成功。""" def test_transport_release_success(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.transport_release.return_value = True with patch("builtins.input", return_value="yes"): with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace( transport_action="release", corr_nr="DEVK001", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("成功释放", printed) def test_transport_release_cancelled(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.transport_release.return_value = True with patch("builtins.input", return_value="no"): with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace( transport_action="release", corr_nr="DEVK001", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("取消", printed) client.transport_release.assert_not_called() def test_transport_release_failure(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.transport_release.return_value = False with patch("builtins.input", return_value="yes"): with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace( transport_action="release", corr_nr="DEVK001", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_transport_release_api_error(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.transport_release.side_effect = Exception("Timeout") with patch("builtins.input", return_value="yes"): with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace( transport_action="release", corr_nr="DEVK001", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) # ═══════════════════════════════════════════════════════════════ # transport.py — transport objects # ═══════════════════════════════════════════════════════════════ class TestTransportObjects(unittest.TestCase): """transport objects 成功。""" def test_transport_objects_with_results(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.transport_objects.return_value = [ {"name": "ZTEST_REPORT", "type": "PROG/P"}, {"name": "ZCL_MY_CLASS", "type": "CLAS/OC"}, ] with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace( transport_action="objects", corr_nr="DEVK001", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("2", printed) self.assertIn("ZTEST_REPORT", printed) def test_transport_objects_empty(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.transport_objects.return_value = [] with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace( transport_action="objects", corr_nr="DEVK001", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("无对象", printed) def test_transport_objects_api_error(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.transport_objects.side_effect = Exception("Server error") with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace( transport_action="objects", corr_nr="DEVK001", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_transport_unknown_action(self): from sapcli.commands.transport import cmd_transport client = _make_client() with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace(transport_action=None, config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("用法", printed) # ═══════════════════════════════════════════════════════════════ # package_cmd.py — package create # ═══════════════════════════════════════════════════════════════ class TestTransportCreate(unittest.TestCase): """transport create — 新建传输请求。 历史背景:该子命令曾只存在于分发副本(源码 v2.1 重构时未纳入), 刷新分发仓前先回迁到源码,避免同步时功能静默消失。 """ def test_transport_create_success(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.create_transport_request.return_value = "DEVK900123" with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace(transport_action="create", description="CEM BIP 传输请求", config=None), client, ) client.create_transport_request.assert_called_once_with("CEM BIP 传输请求") printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("DEVK900123", printed) def test_transport_create_client_error(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.create_transport_request.side_effect = Exception("HTTP 403") with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace(transport_action="create", description="X", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_transport_create_empty_number(self): from sapcli.commands.transport import cmd_transport client = _make_client() client.create_transport_request.return_value = "" with patch("builtins.print") as mock_print: cmd_transport( argparse.Namespace(transport_action="create", description="X", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_transport_usage_lists_create(self): from sapcli.commands.transport import cmd_transport client = _make_client() with patch("builtins.print") as mock_print: cmd_transport(argparse.Namespace(transport_action=None, config=None), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("create", printed) class TestPackageCreate(unittest.TestCase): """package create 成功。""" def test_package_create_success(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.create_package.return_value = True with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace( package_action="create", name="Z_TEST_PKG", description="Test Package", superpackage=None, config=None, ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("创建成功", printed) client.create_package.assert_called_once_with("Z_TEST_PKG", "Test Package", None) def test_package_create_failure(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.create_package.return_value = False with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace( package_action="create", name="Z_DUP_PKG", description="Duplicate", superpackage=None, config=None, ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_package_create_api_error(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.create_package.side_effect = Exception("Auth failed") with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace( package_action="create", name="Z_ERR_PKG", description="Error Pkg", superpackage=None, config=None, ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_package_create_with_superpackage(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.create_package.return_value = True cmd_package( argparse.Namespace( package_action="create", name="Z_SUB_PKG", description="Sub Package", superpackage="Z_PARENT", config=None, ), client, ) client.create_package.assert_called_once_with("Z_SUB_PKG", "Sub Package", "Z_PARENT") # ═══════════════════════════════════════════════════════════════ # package_cmd.py — package info # ═══════════════════════════════════════════════════════════════ class TestPackageInfo(unittest.TestCase): """package info 成功。""" def test_package_info_success(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.get_package_info.return_value = { "name": "Z_TEST", "description": "Test Package", "owner": "TESTUSER", "superpackage": "Z_PARENT", } with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace( package_action="info", name="Z_TEST", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("Test Package", printed) self.assertIn("Z_PARENT", printed) client.get_package_info.assert_called_once_with("Z_TEST") def test_package_info_no_superpackage(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.get_package_info.return_value = { "name": "Z_TOP", "description": "Top level", "owner": "ADMIN", "superpackage": "", } with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace( package_action="info", name="Z_TOP", config=None ), client, ) # info.get('superpackage', '(无)') returns '' when key exists but is empty printed = " ".join(str(c) for c, _ in mock_print.call_args_list) client.get_package_info.assert_called_once() def test_package_info_api_error(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.get_package_info.side_effect = Exception("Not found") with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace( package_action="info", name="Z_MISSING", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) # ═══════════════════════════════════════════════════════════════ # package_cmd.py — package list # ═══════════════════════════════════════════════════════════════ class TestPackageList(unittest.TestCase): """package list 成功。""" def test_package_list_with_objects(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.list_objects.return_value = [ {"name": "Z_REPORT1", "type": "PROG/P", "description": "Report 1"}, {"name": "Z_CLASS1", "type": "CLAS/OC", "description": "Class 1"}, ] with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace( package_action="list", name="Z_TEST_PKG", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("2", printed) self.assertIn("Z_REPORT1", printed) def test_package_list_empty(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.list_objects.return_value = [] with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace( package_action="list", name="Z_EMPTY_PKG", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("没有找到", printed) def test_package_list_api_error(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() client.list_objects.side_effect = Exception("Timeout") with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace( package_action="list", name="Z_ERR_PKG", config=None ), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_package_unknown_action(self): from sapcli.commands.package_cmd import cmd_package client = _make_client() with patch("builtins.print") as mock_print: cmd_package( argparse.Namespace(package_action=None, name="Z_TEST", config=None), client, ) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("用法", printed) # ═══════════════════════════════════════════════════════════════ # upload.py — cmd_upload(仅锁定→写入→解锁,不检查不激活) # ═══════════════════════════════════════════════════════════════ class TestUploadCommand(unittest.TestCase): """cmd_upload 仅执行锁定→写入→解锁,绝不触发语法检查和激活。""" def _write_tmp_source(self, source="REPORT ztest.\nWRITE: / 'hello'.\n"): """写入临时 .abap 源码文件,返回路径。""" f = tempfile.NamedTemporaryFile( mode="w", suffix=".abap", delete=False, encoding="utf-8" ) f.write(source) f.close() return f.name def test_upload_success(self): """成功上传:lock→set_source→unlock 被调用,syntax_check/activate 未被调用。""" from sapcli.commands.upload import cmd_upload client = _make_client() client.object_exists.return_value = True client.lock.return_value = ("lh_1", "DEVK901362") client.set_source.return_value = True client.unlock.return_value = True tmpfile = self._write_tmp_source() try: cmd_upload( _args(name="ZTEST", type="report", path=tmpfile, corr_nr=None), client, ) finally: os.unlink(tmpfile) client.object_exists.assert_called_once() client.lock.assert_called_once() client.set_source.assert_called_once() client.unlock.assert_called_once() # 关键:区分"真实现"和"假实现"——绝不能触发检查和激活 client.syntax_check.assert_not_called() client.activate.assert_not_called() def test_upload_nonexistent_object(self): """对象不存在时抛 ObjectNotFoundError(不自动创建,区别于 sync)。""" from sapcli.commands.upload import cmd_upload from sapcli.exceptions import ObjectNotFoundError client = _make_client() client.object_exists.return_value = False tmpfile = self._write_tmp_source() try: with self.assertRaises(ObjectNotFoundError) as ctx: cmd_upload( _args(name="ZMISSING", type="report", path=tmpfile, corr_nr=None), client, ) self.assertIn("ZMISSING", str(ctx.exception)) finally: os.unlink(tmpfile) # 对象不存在时不该尝试锁定/写入 client.lock.assert_not_called() client.set_source.assert_not_called() def test_upload_type_without_source(self): """functiongroup 无源码类型抛 InvalidNameError。""" from sapcli.commands.upload import cmd_upload from sapcli.exceptions import InvalidNameError client = _make_client() with self.assertRaises(InvalidNameError): cmd_upload( _args(name="ZTEST_FG", type="functiongroup", path=".", corr_nr=None), client, ) client.lock.assert_not_called() client.set_source.assert_not_called() def test_upload_with_corr_nr(self): """通过 --corr_nr 指定传输请求号,lock 使用该号。""" from sapcli.commands.upload import cmd_upload client = _make_client() client.object_exists.return_value = True client.lock.return_value = ("lh_1", "DEVK901362") client.set_source.return_value = True client.unlock.return_value = True tmpfile = self._write_tmp_source() try: cmd_upload( _args(name="ZTEST", type="class", path=tmpfile, corr_nr="DEVK901362"), client, ) finally: os.unlink(tmpfile) # corr_nr 可作为位置参数或关键字参数传入 call_args = client.lock.call_args passed_corr = ( call_args[1].get("corr_nr") if "corr_nr" in call_args[1] else call_args[0][-1] ) self.assertEqual(passed_corr, "DEVK901362") client.set_source.assert_called_once() def test_upload_file_not_found(self): """本地文件不存在时抛 ConfigError。""" from sapcli.commands.upload import cmd_upload from sapcli.exceptions import ConfigError client = _make_client() with self.assertRaises(ConfigError): cmd_upload( _args( name="ZTEST", type="report", path="/nonexistent/no_such_file.abap", corr_nr=None, ), client, ) client.lock.assert_not_called() client.set_source.assert_not_called() # ═══════════════════════════════════════════════════════════════ # syntax_check.py — cmd_syntax_check(远程对象语法检查,不上传不激活) # ═══════════════════════════════════════════════════════════════ class TestSyntaxCheckCommand(unittest.TestCase): """cmd_syntax_check 检查远程对象语法,不上传代码、不激活。""" def test_syntax_check_passed(self): """语法检查通过,输出含"通过"。""" from sapcli.commands.syntax_check import cmd_syntax_check client = _make_client() client.object_exists.return_value = True client.syntax_check.return_value = (True, []) with patch("builtins.print") as mock_print: cmd_syntax_check(_args(name="ZTEST", type="class"), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("通过", printed) client.syntax_check.assert_called_once() # 区分"真实现"和"假实现":绝不锁定/写入/激活 client.lock.assert_not_called() client.set_source.assert_not_called() client.activate.assert_not_called() def test_syntax_check_failed(self): """语法检查失败,输出含错误行号和描述。""" from sapcli.commands.syntax_check import cmd_syntax_check client = _make_client() client.object_exists.return_value = True client.syntax_check.return_value = ( False, [{"type": "E", "line": "42", "text": "Field UNKNOWN not found", "href": ""}], ) with patch("builtins.print") as mock_print: cmd_syntax_check(_args(name="ZTEST", type="report"), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("42", printed) self.assertIn("UNKNOWN", printed) def test_syntax_check_nonexistent(self): """对象不存在时抛 ObjectNotFoundError。""" from sapcli.commands.syntax_check import cmd_syntax_check from sapcli.exceptions import ObjectNotFoundError client = _make_client() client.object_exists.return_value = False with self.assertRaises(ObjectNotFoundError): cmd_syntax_check(_args(name="ZMISSING", type="report"), client) client.syntax_check.assert_not_called() def test_syntax_check_type_without_source(self): """无源码类型抛 InvalidNameError。""" from sapcli.commands.syntax_check import cmd_syntax_check from sapcli.exceptions import InvalidNameError client = _make_client() with self.assertRaises(InvalidNameError): cmd_syntax_check(_args(name="ZTEST_FG", type="functiongroup"), client) client.syntax_check.assert_not_called() # ═══════════════════════════════════════════════════════════════ # ddl_query.py — cmd_show_table / cmd_read_table # ═══════════════════════════════════════════════════════════════ class TestShowTableCommand(unittest.TestCase): """cmd_show_table 查询 DDIC 表字段结构。""" def test_show_table_success(self): from sapcli.commands.ddl_query import cmd_show_table client = _make_client() client.get_table_fields.return_value = [ {"name": "MANDT", "type": "CLNT", "length": "3", "key_attribute": "X", "description": "Client"}, {"name": "OBJ_ID", "type": "CHAR", "length": "20", "key_attribute": "X", "description": "Object ID"}, ] with patch("builtins.print") as mock_print: cmd_show_table(_args(name="ZMY_TABLE"), client) # 区分真/假实现:get_table_fields 被调用一次,且参数是大写表名 client.get_table_fields.assert_called_once_with("ZMY_TABLE") printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("ZMY_TABLE", printed) self.assertIn("OBJ_ID", printed) def test_show_table_no_fields(self): from sapcli.commands.ddl_query import cmd_show_table client = _make_client() client.get_table_fields.return_value = [] with patch("builtins.print") as mock_print: cmd_show_table(_args(name="ZEMPTY"), client) # 空字段列表 → 打印"无字段信息"并返回 printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("无字段信息", printed) client.get_table_fields.assert_called_once_with("ZEMPTY") def test_show_table_uppercase(self): """传入小写表名,client.get_table_fields 收到的应是大写。""" from sapcli.commands.ddl_query import cmd_show_table client = _make_client() client.get_table_fields.return_value = [ {"name": "F1", "type": "CHAR", "length": "10", "key_attribute": "", "description": "F"}, ] with patch("builtins.print"): cmd_show_table(_args(name="zmy_table"), client) # 大小写归一化在 cmd 内完成,client 收到 "ZMY_TABLE" client.get_table_fields.assert_called_once_with("ZMY_TABLE") class TestReadTableCommand(unittest.TestCase): """cmd_read_table 通过 freestyle SQL 查询表数据。""" def test_read_table_basic(self): from sapcli.commands.ddl_query import cmd_read_table client = _make_client() client.query_table_data.return_value = { "columns": ["MANDT", "OBJ_ID"], "rows": [["100", "OBJ1"], ["100", "OBJ2"]], "total_rows": 2, } with patch("builtins.print") as mock_print: cmd_read_table(_args(name="ZMY_TABLE", max_rows=50), client) # 区分真/假实现:query_table_data 收到的 SQL 含大写表名和 UP TO N ROWS sql_arg = client.query_table_data.call_args[0][0] self.assertIn("ZMY_TABLE", sql_arg) self.assertIn("UP TO 50 ROWS", sql_arg) # max_rows 也作为关键字参数传给 query_table_data self.assertEqual(client.query_table_data.call_args[1].get("max_rows"), 50) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("OBJ1", printed) def test_read_table_with_where(self): from sapcli.commands.ddl_query import cmd_read_table client = _make_client() client.query_table_data.return_value = { "columns": ["MANDT"], "rows": [["100"]], "total_rows": 1, } with patch("builtins.print"): cmd_read_table( _args(name="ZMY_TABLE", where="FIELD1 = 'X'", max_rows=200), client, ) sql_arg = client.query_table_data.call_args[0][0] self.assertIn("WHERE", sql_arg) self.assertIn("FIELD1 = 'X'", sql_arg) def test_read_table_with_fields(self): from sapcli.commands.ddl_query import cmd_read_table client = _make_client() client.query_table_data.return_value = { "columns": ["F1", "F2"], "rows": [["v1", "v2"]], "total_rows": 1, } with patch("builtins.print"): cmd_read_table( _args(name="ZMY_TABLE", fields="F1,F2", max_rows=200), client, ) # 指定字段 → SELECT F1,F2 而非 SELECT * sql_arg = client.query_table_data.call_args[0][0] self.assertIn("F1,F2", sql_arg) self.assertTrue(sql_arg.startswith("SELECT F1,F2")) def test_read_table_empty_data(self): from sapcli.commands.ddl_query import cmd_read_table client = _make_client() client.query_table_data.return_value = { "columns": ["MANDT"], "rows": [], "total_rows": 0, } with patch("builtins.print") as mock_print: cmd_read_table(_args(name="ZMY_TABLE", max_rows=200), client) # 空 rows → 打印"无数据"并返回 printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("无数据", printed) if __name__ == "__main__": unittest.main(verbosity=2)