"""batch.py / analyze.py 深入单元测试 — 失败/边界场景。 运行: python tests/unit/test_batch_analyze.py """ from __future__ import annotations import argparse import json import os import sys import tempfile import unittest from unittest.mock import MagicMock, patch, call sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "assets")) from sapcli.manifest import Manifest, ManifestEntry # ─── Helpers ─── def _make_client(): """创建完整 mock 的 ADTClient。""" client = MagicMock() client.host = "https://sap.example.com" client.sap_client = "100" client.csrf_token = "test-csrf-token" client.user = "TESTUSER" client.session = MagicMock() return client def _args(**kwargs): defaults = {"type": "report", "name": "ZTEST", "path": ".", "config": None} defaults.update(kwargs) return argparse.Namespace(**defaults) def _write_manifest(td, entries, version=1): """在 td 下写 manifest.json。entries: list[ManifestEntry]。""" data = { "version": version, "objects": {e.name: e.to_dict() for e in entries}, } path = os.path.join(td, "manifest.json") with open(path, "w", encoding="utf-8") as f: json.dump(data, f) return path def _make_entry(name="ZTEST", type_="report", file_="reports/ztest.abap", status="not_exists", corr_nr=None, depends_on=None, last_sync=None, last_sync_result="pending"): return ManifestEntry( name=name, type=type_, file=file_, system_status=status, corr_nr=corr_nr, depends_on=depends_on or [], last_sync=last_sync, last_sync_result=last_sync_result, ) # ═══════════════════════════════════════════ # batch.py — cmd_init # ═══════════════════════════════════════════ class TestCmdInitEmptyDir(unittest.TestCase): """cmd_init 无 .abap 文件的空目录。""" def test_no_abap_files_returns_early(self): from sapcli.commands.batch import cmd_init client = _make_client() with tempfile.TemporaryDirectory() as td: # 空目录,没有任何子目录或 .abap 文件 cmd_init(_args(path=td), client) # 不应创建 manifest.json self.assertFalse(os.path.isfile(os.path.join(td, "manifest.json"))) # client 不应被调用 client.get_object_status.assert_not_called() def test_no_abap_files_with_subdirs(self): """有标准子目录但无 .abap 文件。""" from sapcli.commands.batch import cmd_init client = _make_client() with tempfile.TemporaryDirectory() as td: for d in ("reports", "classes", "interfaces"): os.makedirs(os.path.join(td, d)) cmd_init(_args(path=td), client) self.assertFalse(os.path.isfile(os.path.join(td, "manifest.json"))) client.get_object_status.assert_not_called() class TestCmdInitDirNotExists(unittest.TestCase): """cmd_init 目录不存在。""" def test_nonexistent_dir_raises_config_error(self): from sapcli.commands.batch import cmd_init from sapcli.exceptions import ConfigError client = _make_client() with self.assertRaises(ConfigError) as ctx: cmd_init(_args(path="/nonexistent/path/xyz"), client) self.assertIn("不存在", str(ctx.exception)) # ═══════════════════════════════════════════ # batch.py — cmd_sync_all # ═══════════════════════════════════════════ class TestCmdSyncAllNoManifest(unittest.TestCase): """cmd_sync_all 无清单文件报错。""" def test_missing_manifest_raises_file_not_found(self): from sapcli.commands.batch import cmd_sync_all client = _make_client() with tempfile.TemporaryDirectory() as td: # 不创建 manifest.json with self.assertRaises(FileNotFoundError): cmd_sync_all(_args(path=td), client) class TestCmdSyncAllDryRun(unittest.TestCase): """cmd_sync_all dry-run 模式:只打印计划,不实际同步。""" def test_dry_run_does_not_sync(self): from sapcli.commands.batch import cmd_sync_all client = _make_client() with tempfile.TemporaryDirectory() as td: # 创建源文件 report_dir = os.path.join(td, "reports") os.makedirs(report_dir) src_file = os.path.join(report_dir, "ztest.abap") with open(src_file, "w", encoding="utf-8") as f: f.write("REPORT ztest.") entry = _make_entry(status="inactive", last_sync_result="failed") _write_manifest(td, [entry]) with patch("builtins.print") as mock_print: cmd_sync_all(_args(path=td, dry_run=True), client) # _sync_single 不应被调用 # 检查打印了 dry-run 标题 printed = "\n".join(str(c) for c in mock_print.call_args_list) self.assertIn("dry-run", printed) def test_dry_run_shows_pending_objects(self): from sapcli.commands.batch import cmd_sync_all client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) with open(os.path.join(report_dir, "ztest.abap"), "w", encoding="utf-8") as f: f.write("REPORT ztest.") with open(os.path.join(report_dir, "zother.abap"), "w", encoding="utf-8") as f: f.write("REPORT zother.") e1 = _make_entry(name="ZTEST", file_="reports/ztest.abap", status="not_exists") e2 = _make_entry(name="ZOTHER", file_="reports/zother.abap", status="inactive") _write_manifest(td, [e1, e2]) with patch("builtins.print") as mock_print: cmd_sync_all(_args(path=td, dry_run=True), client) printed = "\n".join(str(c) for c in mock_print.call_args_list) self.assertIn("ZTEST", printed) self.assertIn("ZOTHER", printed) class TestCmdSyncAllFailFast(unittest.TestCase): """cmd_sync_all 有对象同步失败(fail-fast)。""" @patch("sapcli.commands.batch._sync_single") def test_fail_fast_stops_on_first_error(self, mock_sync): from sapcli.commands.batch import cmd_sync_all mock_sync.return_value = (False, "同步失败: 语法错误", None) client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) e1 = _make_entry(name="ZFIRST", file_="reports/zfirst.abap", status="inactive") e2 = _make_entry(name="ZSECOND", file_="reports/zsecond.abap", status="inactive") _write_manifest(td, [e1, e2]) for fname in ("zfirst.abap", "zsecond.abap"): with open(os.path.join(report_dir, fname), "w", encoding="utf-8") as f: f.write("REPORT ztest.") with patch("builtins.print"): cmd_sync_all(_args(path=td, fail_fast=True), client) # fail_fast → 第一个失败后 break,_sync_single 最多调用 1 次 self.assertLessEqual(mock_sync.call_count, 1) @patch("sapcli.commands.batch._sync_single") def test_no_fail_fast_continues_on_error(self, mock_sync): from sapcli.commands.batch import cmd_sync_all # 第一次失败,第二次成功 mock_sync.side_effect = [ (False, "同步失败", None), (True, None, "DEVK001"), ] client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) e1 = _make_entry(name="ZFIRST", file_="reports/zfirst.abap", status="inactive") e2 = _make_entry(name="ZSECOND", file_="reports/zsecond.abap", status="inactive") _write_manifest(td, [e1, e2]) for fname in ("zfirst.abap", "zsecond.abap"): with open(os.path.join(report_dir, fname), "w", encoding="utf-8") as f: f.write("REPORT ztest.") with patch("builtins.print"): cmd_sync_all(_args(path=td, fail_fast=False), client) # 没有 fail_fast → 继续同步第二个 self.assertEqual(mock_sync.call_count, 2) @patch("sapcli.commands.batch._sync_single") def test_cascading_skip_on_dep_failure(self, mock_sync): """依赖失败时,后续依赖它的对象被级联跳过。""" from sapcli.commands.batch import cmd_sync_all mock_sync.return_value = (False, "同步失败", None) client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) e1 = _make_entry(name="ZBASE", file_="reports/zbase.abap", status="inactive") e2 = _make_entry(name="ZCHILD", file_="reports/zchild.abap", status="inactive", depends_on=["ZBASE"]) _write_manifest(td, [e1, e2]) for fname in ("zbase.abap", "zchild.abap"): with open(os.path.join(report_dir, fname), "w", encoding="utf-8") as f: f.write("REPORT ztest.") with patch("builtins.print") as mock_print: cmd_sync_all(_args(path=td, fail_fast=False), client) printed = "\n".join(str(c) for c in mock_print.call_args_list) # ZCHILD 应被跳过(依赖失败) self.assertIn("ZCHILD", printed) self.assertIn("跳过", printed) class TestCmdSyncAllMissingFile(unittest.TestCase): """cmd_sync_all 清单中对象对应的本地文件缺失。""" @patch("sapcli.commands.batch._sync_single") def test_missing_file_is_skipped(self, mock_sync): from sapcli.commands.batch import cmd_sync_all mock_sync.return_value = (True, None, "DEVK001") client = _make_client() with tempfile.TemporaryDirectory() as td: # 不创建源文件 → 清单中 file 指向不存在的文件 e1 = _make_entry(name="ZMISSING", file_="reports/zmissing.abap", status="inactive") _write_manifest(td, [e1]) with patch("builtins.print"): cmd_sync_all(_args(path=td), client) # _sync_single 不应被调用(文件缺失,对象被跳过) mock_sync.assert_not_called() class TestCmdSyncAllAlreadySynced(unittest.TestCase): """cmd_sync_all 跳过已同步且激活的对象。""" @patch("sapcli.commands.batch._sync_single") def test_active_success_is_skipped(self, mock_sync): from sapcli.commands.batch import cmd_sync_all client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) with open(os.path.join(report_dir, "zactive.abap"), "w", encoding="utf-8") as f: f.write("REPORT zactive.") e1 = _make_entry( name="ZACTIVE", file_="reports/zactive.abap", status="active", last_sync_result="success", last_sync="2026-01-01T00:00:00+00:00", ) _write_manifest(td, [e1]) with patch("builtins.print"): cmd_sync_all(_args(path=td), client) mock_sync.assert_not_called() class TestCmdSyncAllCyclicDependency(unittest.TestCase): """cmd_sync_all 拓扑排序检测到循环依赖时抛异常。""" def test_cyclic_raises(self): from sapcli.commands.batch import cmd_sync_all from sapcli.exceptions import CyclicDependencyError client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) for fname in ("za.abap", "zb.abap"): with open(os.path.join(report_dir, fname), "w", encoding="utf-8") as f: f.write("REPORT ztest.") e1 = _make_entry(name="ZA", file_="reports/za.abap", status="inactive", depends_on=["ZB"]) e2 = _make_entry(name="ZB", file_="reports/zb.abap", status="inactive", depends_on=["ZA"]) _write_manifest(td, [e1, e2]) with self.assertRaises(CyclicDependencyError): cmd_sync_all(_args(path=td), client) class TestCmdSyncAllNotExistsAutoCreate(unittest.TestCase): """cmd_sync_all 对象不存在时自动创建,创建失败则 fail-fast 或 continue。""" def test_auto_create_failure_fail_fast(self): from sapcli.commands.batch import cmd_sync_all client = _make_client() client.create_object.side_effect = Exception("SAP 连接超时") with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) with open(os.path.join(report_dir, "znew.abap"), "w", encoding="utf-8") as f: f.write("REPORT znew.") e1 = _make_entry(name="ZNEW", file_="reports/znew.abap", status="not_exists") _write_manifest(td, [e1]) with patch("builtins.print"): cmd_sync_all(_args(path=td, fail_fast=True), client) client.create_object.assert_called_once() def test_auto_create_failure_continue(self): from sapcli.commands.batch import cmd_sync_all client = _make_client() client.create_object.side_effect = Exception("SAP 连接超时") with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) with open(os.path.join(report_dir, "znew.abap"), "w", encoding="utf-8") as f: f.write("REPORT znew.") e1 = _make_entry(name="ZNEW", file_="reports/znew.abap", status="not_exists") _write_manifest(td, [e1]) with patch("builtins.print"): # 不应抛出异常,只是标记为 failed cmd_sync_all(_args(path=td, fail_fast=False), client) # 验证 manifest 被更新了(即使失败也保存) self.assertTrue(os.path.isfile(os.path.join(td, "manifest.json"))) class TestCmdSyncAllFunctionAutoGroup(unittest.TestCase): """cmd_sync_all function 类型自动创建函数组。""" def test_auto_creates_function_group(self): from sapcli.commands.batch import cmd_sync_all client = _make_client() client.function_group_exists.return_value = False client.create_function_group.return_value = True # create_object for the function module itself → succeed client.create_object.return_value = ("/uri/zgrp/zfunc", "/uri/zgrp/zfunc/source/main") with tempfile.TemporaryDirectory() as td: func_dir = os.path.join(td, "functions", "zgrp") os.makedirs(func_dir) with open(os.path.join(func_dir, "zfunc.abap"), "w", encoding="utf-8") as f: f.write("FUNCTION zfunc.") e1 = _make_entry( name="ZGRP/ZFUNC", type_="function", file_="functions/zgrp/zfunc.abap", status="not_exists", ) _write_manifest(td, [e1]) with patch("sapcli.commands.batch._sync_single") as mock_sync, \ patch("builtins.print"): # _sync_single succeeds mock_sync.return_value = (True, None, "DEVK001") cmd_sync_all(_args(path=td), client) client.create_function_group.assert_called_once_with("ZGRP") # ═══════════════════════════════════════════ # batch.py — cmd_init SAP 查询失败 # ═══════════════════════════════════════════ class TestCmdInitSAPQueryFailure(unittest.TestCase): """cmd_init 查询 SAP 状态失败时使用 fallback 状态。""" def test_query_failure_defaults_to_not_exists(self): from sapcli.commands.batch import cmd_init client = _make_client() client.get_object_status.side_effect = Exception("网络超时") with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) with open(os.path.join(report_dir, "ztest.abap"), "w", encoding="utf-8") as f: f.write("REPORT ztest.") with patch("builtins.print"): cmd_init(_args(path=td), client) # 应仍然创建 manifest.json manifest_path = os.path.join(td, "manifest.json") self.assertTrue(os.path.isfile(manifest_path)) with open(manifest_path, "r", encoding="utf-8") as f: data = json.load(f) # 失败时 fallback 到 not_exists self.assertEqual(data["objects"]["ZTEST"]["system_status"], "not_exists") # ═══════════════════════════════════════════ # batch.py — cmd_refresh 查询失败 # ═══════════════════════════════════════════ class TestCmdRefreshQueryFailure(unittest.TestCase): """cmd_refresh 单个对象查询失败时跳过。""" def test_query_failure_skips_entry(self): from sapcli.commands.batch import cmd_refresh client = _make_client() client.get_object_status.side_effect = Exception("SAP 不可用") with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) entry = _make_entry(status="active") _write_manifest(td, [entry]) with patch("builtins.print"): cmd_refresh(_args(path=td), client) # 不抛异常,只跳过失败的对象 client.get_object_status.assert_called() # ═══════════════════════════════════════════ # analyze.py — cmd_analyze # ═══════════════════════════════════════════ class TestCmdAnalyzeDirNotExists(unittest.TestCase): """cmd_analyze 目录不存在。""" def test_nonexistent_dir_prints_error(self): from sapcli.commands.analyze import cmd_analyze client = _make_client() with patch("builtins.print") as mock_print: cmd_analyze(_args(path="/nonexistent/path/xyz"), client) printed = "\n".join(str(c) for c in mock_print.call_args_list) self.assertIn("不存在", printed) class TestCmdAnalyzeNoAbapFiles(unittest.TestCase): """cmd_analyze 项目无 .abap 文件(空项目)。""" def test_empty_project_no_crash(self): from sapcli.commands.analyze import cmd_analyze client = _make_client() with tempfile.TemporaryDirectory() as td: with patch("builtins.print") as mock_print: cmd_analyze(_args(path=td), client) printed = "\n".join(str(c) for c in mock_print.call_args_list) self.assertIn("未找到", printed) class TestCmdAnalyzeNoDependencies(unittest.TestCase): """cmd_analyze 项目中 .abap 文件无外部依赖。""" def test_no_z_deps_shows_no_dependencies(self): from sapcli.commands.analyze import cmd_analyze client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) with open(os.path.join(report_dir, "zsimple.abap"), "w", encoding="utf-8") as f: f.write("REPORT zsimple.\nWRITE: / 'hello'.\n") with patch("builtins.print") as mock_print: cmd_analyze(_args(path=td), client) printed = "\n".join(str(c) for c in mock_print.call_args_list) self.assertIn("无外部依赖", printed) class TestCmdAnalyzeWithDependencies(unittest.TestCase): """cmd_analyze 正确检测 Z/Y 开头的外部依赖。""" def test_detects_type_ref_to(self): from sapcli.commands.analyze import cmd_analyze, _analyze_dependencies source = "DATA: lo_obj TYPE REF TO ZCL_MY_CLASS." deps = _analyze_dependencies(source) self.assertIn("ZCL_MY_CLASS", deps) def test_detects_call_function(self): from sapcli.commands.analyze import _analyze_dependencies source = "CALL FUNCTION 'Z_MY_FUNC'." deps = _analyze_dependencies(source) self.assertIn("Z_MY_FUNC", deps) def test_detects_create_object(self): from sapcli.commands.analyze import _analyze_dependencies source = "CREATE OBJECT lo_obj TYPE ZCL_FACTORY." deps = _analyze_dependencies(source) self.assertIn("ZCL_FACTORY", deps) def test_detects_type_reference(self): from sapcli.commands.analyze import _analyze_dependencies source = "DATA: lv_val TYPE ZCUSTOM_TYPE." deps = _analyze_dependencies(source) self.assertIn("ZCUSTOM_TYPE", deps) def test_ignores_builtin_types(self): from sapcli.commands.analyze import _analyze_dependencies source = "DATA: lv_str TYPE STRING.\nDATA: lv_int TYPE INT4." deps = _analyze_dependencies(source) self.assertEqual(deps, []) def test_ignores_short_names(self): from sapcli.commands.analyze import _analyze_dependencies source = "DATA: lv_val TYPE AB." deps = _analyze_dependencies(source) self.assertEqual(deps, []) def test_detects_y_prefix(self): from sapcli.commands.analyze import _analyze_dependencies source = "DATA: lo TYPE REF TO YCL_SERVICE." deps = _analyze_dependencies(source) self.assertIn("YCL_SERVICE", deps) def test_multiple_deps_deduplicated(self): from sapcli.commands.analyze import _analyze_dependencies source = ( "DATA: lo1 TYPE REF TO ZCL_A.\n" "DATA: lo2 TYPE REF TO ZCL_A.\n" "CALL FUNCTION 'ZCL_A'.\n" ) deps = _analyze_dependencies(source) # ZCL_A 只出现一次 self.assertEqual(deps.count("ZCL_A"), 1) class TestCmdAnalyzeFileIntegration(unittest.TestCase): """cmd_analyze 完整文件分析集成测试。""" def test_analyze_with_deps(self): from sapcli.commands.analyze import cmd_analyze client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) with open(os.path.join(report_dir, "zrpt.abap"), "w", encoding="utf-8") as f: f.write("REPORT zrpt.\nDATA: lo TYPE REF TO ZCL_HELPER.\n") with patch("builtins.print") as mock_print: cmd_analyze(_args(path=td), client) printed = "\n".join(str(c) for c in mock_print.call_args_list) self.assertIn("ZCL_HELPER", printed) def test_analyze_multiple_files(self): from sapcli.commands.analyze import cmd_analyze client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) # 文件 1: 有依赖 with open(os.path.join(report_dir, "za.abap"), "w", encoding="utf-8") as f: f.write("REPORT za.\nDATA lo TYPE REF TO ZCL_DEP.\n") # 文件 2: 无依赖 with open(os.path.join(report_dir, "zb.abap"), "w", encoding="utf-8") as f: f.write("REPORT zb.\nWRITE: / 'hello'.\n") with patch("builtins.print") as mock_print: cmd_analyze(_args(path=td), client) printed = "\n".join(str(c) for c in mock_print.call_args_list) self.assertIn("2", printed) # 2 个文件 self.assertIn("ZCL_DEP", printed) class TestCmdAnalyzeMissingFiles(unittest.TestCase): """cmd_analyze 处理缺少文件的对象。""" def test_analyze_only_reads_existing_files(self): """cmd_analyze 通过 os.walk 扫描,只读存在的 .abap 文件,不会因 manifest 中 引用的缺失文件而崩溃。""" from sapcli.commands.analyze import cmd_analyze client = _make_client() with tempfile.TemporaryDirectory() as td: # 创建一个有文件的对象和一个空目录 report_dir = os.path.join(td, "reports") os.makedirs(report_dir) with open(os.path.join(report_dir, "zexists.abap"), "w", encoding="utf-8") as f: f.write("REPORT zexists.\n") # 只分析到实际存在的文件 with patch("builtins.print") as mock_print: cmd_analyze(_args(path=td), client) printed = "\n".join(str(c) for c in mock_print.call_args_list) self.assertIn("1", printed) # 1 个文件 class TestCmdAnalyzeEmptySource(unittest.TestCase): """_analyze_dependencies 输入空源码。""" def test_empty_source_no_deps(self): from sapcli.commands.analyze import _analyze_dependencies deps = _analyze_dependencies("") self.assertEqual(deps, []) def test_whitespace_only_no_deps(self): from sapcli.commands.analyze import _analyze_dependencies deps = _analyze_dependencies(" \n \n ") self.assertEqual(deps, []) # ═══════════════════════════════════════════ # sorter.py — 环形依赖检测(补充) # ═══════════════════════════════════════════ class TestTopologicalSortCyclic(unittest.TestCase): """topological_sort 检测循环依赖。""" def test_simple_two_node_cycle(self): from sapcli.sorter import topological_sort from sapcli.exceptions import CyclicDependencyError e1 = _make_entry(name="ZA", depends_on=["ZB"]) e2 = _make_entry(name="ZB", depends_on=["ZA"]) with self.assertRaises(CyclicDependencyError) as ctx: topological_sort([e1, e2]) self.assertTrue(len(ctx.exception.cycle_members) > 0) def test_three_node_cycle(self): from sapcli.sorter import topological_sort from sapcli.exceptions import CyclicDependencyError e1 = _make_entry(name="ZA", depends_on=["ZC"]) e2 = _make_entry(name="ZB", depends_on=["ZA"]) e3 = _make_entry(name="ZC", depends_on=["ZB"]) with self.assertRaises(CyclicDependencyError): topological_sort([e1, e2, e3]) def test_self_dependency(self): from sapcli.sorter import topological_sort from sapcli.exceptions import CyclicDependencyError e1 = _make_entry(name="ZA", depends_on=["ZA"]) with self.assertRaises(CyclicDependencyError): topological_sort([e1]) def test_no_deps_returns_all(self): from sapcli.sorter import topological_sort e1 = _make_entry(name="ZA") e2 = _make_entry(name="ZB") result = topological_sort([e1, e2]) names = [e.name for e in result] self.assertEqual(set(names), {"ZA", "ZB"}) def test_external_dep_ignored(self): """depends_on 引用不在列表中的外部对象,不报错。""" from sapcli.sorter import topological_sort e1 = _make_entry(name="ZA", depends_on=["Z_EXTERNAL"]) result = topological_sort([e1]) self.assertEqual(len(result), 1) def test_empty_list_returns_empty(self): from sapcli.sorter import topological_sort self.assertEqual(topological_sort([]), []) # ═══════════════════════════════════════════ # batch.py — cmd_sync_all 成功路径 # ═══════════════════════════════════════════ class TestCmdSyncAllSuccess(unittest.TestCase): """cmd_sync_all 正常同步成功。""" @patch("sapcli.commands.batch._sync_single") def test_sync_updates_manifest(self, mock_sync): from sapcli.commands.batch import cmd_sync_all mock_sync.return_value = (True, None, "DEVK999") client = _make_client() with tempfile.TemporaryDirectory() as td: report_dir = os.path.join(td, "reports") os.makedirs(report_dir) with open(os.path.join(report_dir, "ztest.abap"), "w", encoding="utf-8") as f: f.write("REPORT ztest.") e1 = _make_entry(status="inactive", last_sync_result="pending") _write_manifest(td, [e1]) with patch("builtins.print"): cmd_sync_all(_args(path=td), client) # 验证 manifest 更新 with open(os.path.join(td, "manifest.json"), "r", encoding="utf-8") as f: data = json.load(f) self.assertEqual(data["objects"]["ZTEST"]["system_status"], "active") self.assertEqual(data["objects"]["ZTEST"]["last_sync_result"], "success") self.assertEqual(data["objects"]["ZTEST"]["corr_nr"], "DEVK999") if __name__ == "__main__": unittest.main(verbosity=2)