openubmc-bingo 0.6.45__py3-none-any.whl → 0.6.99__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. bmcgo/__init__.py +1 -1
  2. bmcgo/bmcgo.py +9 -3
  3. bmcgo/bmcgo_config.py +16 -0
  4. bmcgo/cli/cli.py +72 -21
  5. bmcgo/codegen/__init__.py +1 -1
  6. bmcgo/codegen/lua/codegen.py +2 -2
  7. bmcgo/codegen/lua/script/check_intfs.py +1 -0
  8. bmcgo/codegen/lua/script/dto/options.py +1 -0
  9. bmcgo/codegen/lua/script/gen_db_json.py +4 -3
  10. bmcgo/codegen/lua/script/gen_rpc_msg_json.py +78 -11
  11. bmcgo/codegen/lua/script/model_consistency_check.py +1 -1
  12. bmcgo/codegen/lua/script/render_utils/db_lua.py +5 -6
  13. bmcgo/codegen/lua/script/render_utils/model_lua.py +5 -1
  14. bmcgo/codegen/lua/script/template.py +5 -0
  15. bmcgo/codegen/lua/script/utils.py +50 -8
  16. bmcgo/codegen/lua/templates/apps/Makefile +2 -2
  17. bmcgo/codegen/lua/templates/apps/client.lua.mako +1 -1
  18. bmcgo/codegen/lua/templates/apps/model.lua.mako +4 -3
  19. bmcgo/codegen/lua/templates/apps/service.lua.mako +1 -1
  20. bmcgo/codegen/lua/templates/apps/utils/mdb_intf.lua.mako +4 -0
  21. bmcgo/codegen/lua/templates/new_app_v2/CMakeLists.txt.mako +26 -0
  22. bmcgo/codegen/lua/templates/new_app_v2/conanfile.py.mako +9 -0
  23. bmcgo/codegen/lua/v1/script/render_utils/db_lua.py +5 -6
  24. bmcgo/codegen/lua/v1/script/render_utils/model_lua.py +13 -1
  25. bmcgo/codegen/lua/v1/templates/apps/client.lua.mako +1 -1
  26. bmcgo/codegen/lua/v1/templates/apps/local_db.lua.mako +0 -4
  27. bmcgo/codegen/lua/v1/templates/apps/message.lua.mako +3 -0
  28. bmcgo/codegen/lua/v1/templates/apps/model.lua.mako +3 -0
  29. bmcgo/codegen/lua/v1/templates/apps/utils/mdb_intf.lua.mako +6 -4
  30. bmcgo/component/analysis/analysis.py +9 -4
  31. bmcgo/component/analysis/dep-rules.json +20 -8
  32. bmcgo/component/analysis/dep_node.py +2 -0
  33. bmcgo/component/analysis/intf_validation.py +8 -7
  34. bmcgo/component/analysis/sr_validation.py +5 -4
  35. bmcgo/component/busctl_log_parse/busctl_log_parser.py +809 -0
  36. bmcgo/component/busctl_log_parse/mock_data_save.py +170 -0
  37. bmcgo/component/busctl_log_parse/test_data_save.py +49 -0
  38. bmcgo/component/component_helper.py +29 -0
  39. bmcgo/component/coverage/incremental_cov.py +5 -0
  40. bmcgo/component/fixture/__init__.py +29 -0
  41. bmcgo/component/fixture/auto_case_generator.py +490 -0
  42. bmcgo/component/fixture/busctl_type_converter.py +1081 -0
  43. bmcgo/component/fixture/common_config.py +15 -0
  44. bmcgo/component/fixture/dbus_gateway.py +669 -0
  45. bmcgo/component/fixture/dbus_library.py +250 -0
  46. bmcgo/component/fixture/dbus_mock_utils.py +514 -0
  47. bmcgo/component/fixture/dbus_response_handler.py +138 -0
  48. bmcgo/component/fixture/dbus_signature.py +110 -0
  49. bmcgo/component/template_v2/conanbase.py.mako +1 -5
  50. bmcgo/component/test.py +69 -10
  51. bmcgo/error_analyzer/__init__.py +0 -0
  52. bmcgo/error_analyzer/case_matcher.py +114 -0
  53. bmcgo/error_analyzer/log_parser.py +128 -0
  54. bmcgo/error_analyzer/unified_error_analyzer.py +359 -0
  55. bmcgo/error_cases/cases.yml +59 -0
  56. bmcgo/error_cases/cases_template_valid.json +71 -0
  57. bmcgo/error_cases/conanfile.py +58 -0
  58. bmcgo/frame.py +0 -4
  59. bmcgo/functional/analysis.py +18 -12
  60. bmcgo/functional/bmc_studio_action.py +21 -10
  61. bmcgo/functional/check.py +86 -42
  62. bmcgo/functional/conan_index_build.py +1 -1
  63. bmcgo/functional/config.py +22 -18
  64. bmcgo/functional/csr_build.py +63 -34
  65. bmcgo/functional/deploy.py +4 -3
  66. bmcgo/functional/diff.py +51 -34
  67. bmcgo/functional/full_component.py +16 -5
  68. bmcgo/functional/hpm_signer.py +484 -0
  69. bmcgo/functional/new.py +8 -2
  70. bmcgo/functional/schema_valid.py +111 -15
  71. bmcgo/functional/upgrade.py +6 -6
  72. bmcgo/misc.py +1 -0
  73. bmcgo/tasks/task_build_conan.py +27 -6
  74. bmcgo/tasks/task_build_rootfs_img.py +120 -83
  75. bmcgo/tasks/task_buildgppbin.py +30 -13
  76. bmcgo/tasks/task_buildhpm_ext4.py +5 -3
  77. bmcgo/tasks/task_download_buildtools.py +20 -11
  78. bmcgo/tasks/task_download_dependency.py +29 -20
  79. bmcgo/tasks/task_hpm_envir_prepare.py +32 -53
  80. bmcgo/tasks/task_packet_to_supporte.py +12 -4
  81. bmcgo/tasks/task_prepare.py +1 -1
  82. bmcgo/tasks/task_sign_and_pack_hpm.py +15 -7
  83. bmcgo/utils/component_version_check.py +4 -4
  84. bmcgo/utils/config.py +3 -0
  85. bmcgo/utils/fetch_component_code.py +148 -17
  86. bmcgo/utils/install_manager.py +2 -2
  87. bmcgo/utils/installations/base_installer.py +10 -27
  88. bmcgo/utils/installations/install_plans/studio.yml +3 -0
  89. bmcgo/utils/mapping_config_patch.py +5 -4
  90. bmcgo/utils/tools.py +49 -7
  91. {openubmc_bingo-0.6.45.dist-info → openubmc_bingo-0.6.99.dist-info}/METADATA +1 -1
  92. {openubmc_bingo-0.6.45.dist-info → openubmc_bingo-0.6.99.dist-info}/RECORD +95 -74
  93. bmcgo/tasks/download_buildtools_hm.py +0 -124
  94. {openubmc_bingo-0.6.45.dist-info → openubmc_bingo-0.6.99.dist-info}/WHEEL +0 -0
  95. {openubmc_bingo-0.6.45.dist-info → openubmc_bingo-0.6.99.dist-info}/entry_points.txt +0 -0
  96. {openubmc_bingo-0.6.45.dist-info → openubmc_bingo-0.6.99.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env python3
2
+ # coding: utf-8
3
+ # Copyright (c) 2024 Huawei Technologies Co., Ltd.
4
+ # openUBMC is licensed under Mulan PSL v2.
5
+ # You can use this software according to the terms and conditions of the Mulan PSL v2.
6
+ # You may obtain a copy of Mulan PSL v2 at:
7
+ # http://license.coscl.org.cn/MulanPSL2
8
+ # THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
9
+ # EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
10
+ # MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
11
+ # See the Mulan PSL v2 for more details.
12
+ """
13
+ 根据 busctl 录制的 test_data.json 自动生成 Robot 测试用例。
14
+
15
+ 当前版本采用"数据-用例分离"策略:
16
+ - 录制数据始终保留在 test_data.json 中;
17
+ - 生成的 Python 关键字只包含回放逻辑,执行时按 case 序号动态解析数据;
18
+ - Robot 用例仅串联关键字,便于维护与扩展。
19
+
20
+ 使用方式:python auto_case_generator.py \
21
+ --bmc-test-db-dir /opt/code/network_adapter/temp/opt/bmc/it_test/bmc_test_db \
22
+ --test-db-name network_adapter_y \
23
+ --fixture-dir /opt/code/network_adapter/temp/opt/bmc/it_test/fixture
24
+
25
+ 生成的用例文件会输出到当前执行命令的目录(工作目录)。
26
+ """
27
+ from __future__ import annotations
28
+ import argparse
29
+ import json
30
+ import re
31
+ import logging
32
+ from dataclasses import dataclass
33
+ from datetime import datetime, timezone
34
+ from pathlib import Path
35
+ from typing import List, Sequence
36
+ from bmcgo.errors import BmcGoException
37
+
38
+ REPO_ROOT = Path(__file__).resolve().parents[2]
39
+ # 默认输出文件名(不含路径),实际路径在运行时基于当前工作目录计算
40
+ DEFAULT_PY_OUT_NAME = "generated_keywords.py"
41
+ DEFAULT_ROBOT_OUT_NAME = "generated_cases.robot"
42
+
43
+
44
+ @dataclass
45
+ class CaseSpec:
46
+ index: int
47
+ keyword_name: str
48
+ robot_title: str
49
+ request_member: str
50
+ request_interface: str
51
+ timestamp: str
52
+
53
+
54
+ @dataclass
55
+ class GeneratorConfig:
56
+ input_path: Path
57
+ py_out: Path
58
+ robot_out: Path
59
+ bmc_test_db_dir: Path
60
+ test_db_name: str
61
+ fixture_dir: Path
62
+ repo_root: Path
63
+
64
+
65
+ def slugify(name: str) -> str:
66
+ slug = re.sub(r"[^0-9a-zA-Z]+", "_", name).strip("_").lower()
67
+ return slug or "call"
68
+
69
+
70
+ def build_cases(events: Sequence[dict]) -> List[CaseSpec]:
71
+ cases: List[CaseSpec] = []
72
+ counter = 1
73
+ trailing_signals = 0
74
+ for entry in events:
75
+ if entry.get("type") == "signal":
76
+ trailing_signals += 1
77
+ continue
78
+ if "request" not in entry:
79
+ continue
80
+ request_block = entry["request"]
81
+ keyword_name = f"case_{counter:04d}_{slugify(request_block['member'])}"
82
+ robot_title = f"Case {counter:04d} {request_block['member']}"
83
+ cases.append(
84
+ CaseSpec(
85
+ index=counter,
86
+ keyword_name=keyword_name,
87
+ robot_title=robot_title,
88
+ request_member=request_block["member"],
89
+ request_interface=request_block["interface"],
90
+ timestamp=request_block.get("timestamp", ""),
91
+ )
92
+ )
93
+ trailing_signals = 0
94
+ counter += 1
95
+ if trailing_signals:
96
+ logging.info(f"[INFO] 有 {trailing_signals} 条末尾 signal 未消费,已忽略。")
97
+ return cases
98
+
99
+
100
+ def build_python_module(
101
+ cases: Sequence[CaseSpec],
102
+ bmc_test_db_dir: Path,
103
+ test_db_name: str,
104
+ fixture_dir: Path,
105
+ repo_root: Path,
106
+ ) -> str:
107
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
108
+ meta = f'"""Auto-generated keywords @ {timestamp}."""'
109
+ # 构建数据路径:bmc_test_db_dir / test_db_name / test_data / test_data.json
110
+ data_path = bmc_test_db_dir / test_db_name / "test_data" / "test_data.json"
111
+ data_path_expr = f"Path(r'{data_path}')"
112
+ # fixture 路径
113
+ fixture_path_expr = f"Path(r'{fixture_dir}')"
114
+ header = "\n".join(
115
+ [
116
+ meta,
117
+ "from __future__ import annotations",
118
+ "",
119
+ "import json",
120
+ "import logging",
121
+ "import sys",
122
+ "import time",
123
+ "from datetime import datetime",
124
+ "from functools import lru_cache",
125
+ "from pathlib import Path",
126
+ "from typing import Any, Iterable, List",
127
+ "",
128
+ "THIS_DIR = Path(__file__).resolve().parent",
129
+ f"DATA_PATH = {data_path_expr}",
130
+ f"FIXTURE_DIR = {fixture_path_expr}",
131
+ "if str(FIXTURE_DIR) not in sys.path:",
132
+ " sys.path.insert(0, str(FIXTURE_DIR))",
133
+ "",
134
+ "from bmcgo.component.fixture.busctl_type_converter import BusCtlTypeConverter",
135
+ "from dbus_next import Variant",
136
+ "",
137
+ "if str(THIS_DIR) not in sys.path:",
138
+ " sys.path.insert(0, str(THIS_DIR))",
139
+ "",
140
+ "from bmcgo.component.fixture.dbus_library import DBusLibrary",
141
+ "from bmcgo.component.fixture.dbus_mock_utils import set_mock_response, clear_mock",
142
+ "",
143
+ "_DBUS = DBusLibrary()",
144
+ "logger = logging.getLogger(__name__)",
145
+ "",
146
+ "DEPENDENCY_SERVICE = 'bmc.kepler.MockControl'",
147
+ "DEPENDENCY_OBJECT_PATH = '/bmc/kepler/MockControl'",
148
+ "DEPENDENCY_INTERFACE = 'bmc.kepler.MockControl'",
149
+ "DEFAULT_DEPENDENCY_TIMEOUT_MS = 10000",
150
+ "_TIMESTAMP_FORMATS = (\"%a %Y-%m-%d %H:%M:%S.%f %Z\", \"%a %Y-%m-%d %H:%M:%S %Z\")",
151
+ "",
152
+ "# 全局配置:是否按时间戳计算延迟(True=启用,False=禁用,但仍会执行 sleep_ms 和依赖等待)",
153
+ "USE_TIMESTAMP_DELAY = True",
154
+ "",
155
+ "",
156
+ "def _parse_busctl(text: str):",
157
+ " return BusCtlTypeConverter.dbus_string_to_type(text)",
158
+ "",
159
+ "",
160
+ "@lru_cache(maxsize=1)",
161
+ "def _load_events():",
162
+ " with DATA_PATH.open(encoding=\"utf-8\") as fh:",
163
+ " return json.load(fh)",
164
+ "",
165
+ "",
166
+ "def _parse_timestamp_value(value: str):",
167
+ " if not value:",
168
+ " return None",
169
+ " for fmt in _TIMESTAMP_FORMATS:",
170
+ " try:",
171
+ " return datetime.strptime(value, fmt)",
172
+ " except ValueError:",
173
+ " continue",
174
+ " logger.debug(\"无法解析 timestamp: %s\", value)",
175
+ " return None",
176
+ "",
177
+ "",
178
+ "def _prepare_signal_schedule(signal_specs: List[dict]):",
179
+ " prev_ts = None",
180
+ " for spec in signal_specs:",
181
+ " wait_cfg = dict(spec.get(\"wait\") or {})",
182
+ " delay_override = wait_cfg.get(\"delay_seconds\")",
183
+ " if delay_override is None and \"delay_ms\" in wait_cfg:",
184
+ " delay_override = wait_cfg.get(\"delay_ms\") / 1000.0",
185
+ " if delay_override is not None:",
186
+ " spec[\"delay_seconds\"] = max(float(delay_override), 0.0)",
187
+ " continue",
188
+ " if not USE_TIMESTAMP_DELAY:",
189
+ " spec[\"delay_seconds\"] = 0.0",
190
+ " continue",
191
+ " current_ts = _parse_timestamp_value(spec.get(\"timestamp\"))",
192
+ " if prev_ts and current_ts:",
193
+ " delta = max((current_ts - prev_ts).total_seconds(), 0.0)",
194
+ " else:",
195
+ " delta = 0.0",
196
+ " spec[\"delay_seconds\"] = delta",
197
+ " if current_ts:",
198
+ " prev_ts = current_ts",
199
+ "",
200
+ "",
201
+ "def _convert_signal_entry(entry: dict):",
202
+ " wait_spec = entry.get(\"wait\")",
203
+ " if isinstance(wait_spec, dict):",
204
+ " wait_spec = dict(wait_spec)",
205
+ " else:",
206
+ " wait_spec = {}",
207
+ " sleep_ms = entry.get(\"sleep_ms\")",
208
+ " if sleep_ms is None and \"sleep_seconds\" in entry:",
209
+ " sleep_ms = float(entry.get(\"sleep_seconds\", 0)) * 1000",
210
+ " return {",
211
+ " \"path\": entry[\"path\"],",
212
+ " \"interface\": entry[\"interface\"],",
213
+ " \"member\": entry[\"member\"],",
214
+ " \"signature\": entry.get(\"signature\", \"\"),",
215
+ " \"args\": [_parse_busctl(item) for item in entry.get(\"content\", [])],",
216
+ " \"timestamp\": entry.get(\"timestamp\"),",
217
+ " \"wait\": wait_spec,",
218
+ " \"sleep_ms\": sleep_ms,",
219
+ " }",
220
+ "",
221
+ "",
222
+ "def _convert_request_response(entry: dict):",
223
+ " request_block = entry[\"request\"]",
224
+ " response_block = entry.get(\"response\")",
225
+ " if response_block is None:",
226
+ " raise AssertionError(\"录制数据缺少 response 字段\")",
227
+ " request = {",
228
+ " \"destination\": request_block[\"destination\"],",
229
+ " \"path\": request_block[\"path\"],",
230
+ " \"interface\": request_block[\"interface\"],",
231
+ " \"member\": request_block[\"member\"],",
232
+ " \"args\": [_parse_busctl(arg) for arg in request_block.get(\"args\", [])],",
233
+ " }",
234
+ " expected = [_parse_busctl(value) for value in response_block.get(\"values\", [])]",
235
+ " return request, expected",
236
+ "",
237
+ "",
238
+ "def _load_case_payload(case_index: int):",
239
+ " events = _load_events()",
240
+ " pending: List[dict] = []",
241
+ " counter = 1",
242
+ " for entry in events:",
243
+ " if entry.get(\"type\") == \"signal\":",
244
+ " pending.append(entry)",
245
+ " continue",
246
+ " if \"request\" in entry:",
247
+ " if counter == case_index:",
248
+ " signals = [_convert_signal_entry(sig) for sig in pending]",
249
+ " _prepare_signal_schedule(signals)",
250
+ " request, expected = _convert_request_response(entry)",
251
+ " return signals, request, expected",
252
+ " pending = []",
253
+ " counter += 1",
254
+ " raise AssertionError(f\"未在录制数据中找到序号为 {case_index} 的请求\")",
255
+ "",
256
+ "",
257
+ "def _normalize(value: Any):",
258
+ " if isinstance(value, Variant):",
259
+ " return (\"variant\", value.signature, _normalize(value.value))",
260
+ " if isinstance(value, dict):",
261
+ " return {key: _normalize(val) for key, val in value.items()}",
262
+ " if isinstance(value, (list, tuple)):",
263
+ " return [_normalize(item) for item in value]",
264
+ " return value",
265
+ "",
266
+ "",
267
+ "def _calculate_delay_seconds(spec: dict) -> float:",
268
+ " base = float(spec.get(\"delay_seconds\") or 0.0)",
269
+ " wait_cfg = spec.get(\"wait\") or {}",
270
+ " if \"extra_delay_ms\" in wait_cfg:",
271
+ " base += max(wait_cfg[\"extra_delay_ms\"], 0) / 1000.0",
272
+ " if \"post_delay_ms\" in wait_cfg:",
273
+ " base += max(wait_cfg[\"post_delay_ms\"], 0) / 1000.0",
274
+ " return max(base, 0.0)",
275
+ "",
276
+ "",
277
+ "def _wait_for_dependency(spec: dict, idx: int):",
278
+ " wait_cfg = spec.get(\"wait\") or {}",
279
+ " dep_cfg = wait_cfg.get(\"dependency\")",
280
+ " if not dep_cfg:",
281
+ " return",
282
+ " lookup_key = dep_cfg.get(\"lookup_key\")",
283
+ " if not lookup_key:",
284
+ " raise AssertionError(f\"信号[{idx}] 缺少 dependency.lookup_key\")",
285
+ " target = int(dep_cfg.get(\"count\", 1))",
286
+ " timeout_ms = int(dep_cfg.get(\"timeout_ms\", DEFAULT_DEPENDENCY_TIMEOUT_MS))",
287
+ " timeout_seconds = timeout_ms / 1000.0",
288
+ " start_time = time.time()",
289
+ " check_interval = 0.1 # 每100ms查询一次",
290
+ " current_count = 0",
291
+ " ",
292
+ " while True:",
293
+ " try:",
294
+ " current_count = _DBUS.call_dbus_method(",
295
+ " DEPENDENCY_SERVICE,",
296
+ " DEPENDENCY_OBJECT_PATH,",
297
+ " DEPENDENCY_INTERFACE,",
298
+ " \"get_dependency_count\",",
299
+ " lookup_key,",
300
+ " )",
301
+ " if current_count >= target:",
302
+ " logger.info(",
303
+ " f\"依赖 {lookup_key} 已满足:当前调用次数 {current_count} >= 目标 {target}\"",
304
+ " )",
305
+ " return",
306
+ " except Exception as exc:",
307
+ " logger.warning(",
308
+ " f\"查询依赖 {lookup_key} 调用次数失败: {exc},继续等待\"",
309
+ " )",
310
+ " ",
311
+ " elapsed = time.time() - start_time",
312
+ " if elapsed >= timeout_seconds:",
313
+ " logger.warning(",
314
+ " f\"等待依赖 {lookup_key} 第 {target} 次调用超时 ({timeout_ms} ms),当前调用次数: {current_count},继续发送信号\"",
315
+ " )",
316
+ " return",
317
+ " ",
318
+ " time.sleep(check_interval)",
319
+ "",
320
+ "",
321
+ "def _replay_signals(signal_specs: Iterable[dict]):",
322
+ " for idx, spec in enumerate(signal_specs, start=1):",
323
+ " _wait_for_dependency(spec, idx)",
324
+ " delay = _calculate_delay_seconds(spec)",
325
+ " if delay > 0:",
326
+ " time.sleep(delay)",
327
+ " path = spec[\"path\"]",
328
+ " interface = spec[\"interface\"]",
329
+ " member = spec[\"member\"]",
330
+ " signature = spec.get(\"signature\") or \"\"",
331
+ " args = spec.get(\"args\", [])",
332
+ " sleep_ms = spec.get(\"sleep_ms\")",
333
+ " if sleep_ms is not None and sleep_ms > 0:",
334
+ " time.sleep(sleep_ms / 1000.0)",
335
+ " try:",
336
+ " if signature:",
337
+ " _DBUS.send_signal_with_signature(",
338
+ " path, interface, member, signature, *args",
339
+ " )",
340
+ " else:",
341
+ " _DBUS.send_signal(path, interface, member, *args)",
342
+ " except Exception as exc:",
343
+ " raise AssertionError(",
344
+ " f\"重放信号失败[{idx}] {interface}.{member} ({path}): {exc}\"",
345
+ " ) from exc",
346
+ "",
347
+ "",
348
+ "def _call_and_assert(request: dict, expected_values: List[Any]):",
349
+ " raw = _DBUS.call_dbus_method(",
350
+ " request[\"destination\"],",
351
+ " request[\"path\"],",
352
+ " request[\"interface\"],",
353
+ " request[\"member\"],",
354
+ " *request[\"args\"],",
355
+ " )",
356
+ " if raw is None:",
357
+ " normalized_raw = []",
358
+ " elif isinstance(raw, tuple):",
359
+ " normalized_raw = [_normalize(item) for item in raw]",
360
+ " else:",
361
+ " normalized_raw = _normalize(raw)",
362
+ " normalized_expected = _normalize(",
363
+ " expected_values if len(expected_values) != 1 else expected_values[0]",
364
+ " )",
365
+ " if normalized_raw != normalized_expected:",
366
+ " raise AssertionError(",
367
+ " f\"期望 {normalized_expected!r},实际 {normalized_raw!r}\"",
368
+ " )",
369
+ " return raw",
370
+ ]
371
+ )
372
+ body_lines: List[str] = [header]
373
+ runner_entries: List[str] = []
374
+ for case in cases:
375
+ body_lines.append(f"\n\ndef {case.keyword_name}():\n")
376
+ doc = (
377
+ f"\"\"\"{case.request_member} ({case.request_interface}) recorded at {case.timestamp}\"\"\""
378
+ )
379
+ body_lines.append(f" {doc}\n")
380
+ body_lines.append(f" signals, request, expected = _load_case_payload({case.index})\n")
381
+ body_lines.append(" _replay_signals(signals)\n")
382
+ body_lines.append(" return _call_and_assert(request, expected)\n")
383
+ runner_entries.append(f" {case.index}: {case.keyword_name},")
384
+ body_lines.append("\n\n_CASE_RUNNERS = {\n")
385
+ body_lines.extend(line + "\n" for line in runner_entries)
386
+ body_lines.append("}\n")
387
+ body_lines.append(
388
+ """
389
+
390
+ def run_case(case_index: int):
391
+ try:
392
+ func = _CASE_RUNNERS[case_index]
393
+ except KeyError as exc:
394
+ raise KeyError(f"未知 case 序号 {case_index}") from exc
395
+ return func()
396
+
397
+
398
+ if __name__ == "__main__":
399
+ import argparse
400
+
401
+ parser = argparse.ArgumentParser(description="运行录制用例")
402
+ parser.add_argument("case", type=int, help="case 序号(从 1 开始)")
403
+ args = parser.parse_args()
404
+ run_case(args.case)
405
+ """
406
+ )
407
+ return "".join(body_lines).rstrip() + "\n"
408
+
409
+
410
+ def build_robot_suite(cases: Sequence[CaseSpec], keywords_file: str) -> str:
411
+ lines = [
412
+ "*** Settings ***",
413
+ f"Library {keywords_file}",
414
+ "",
415
+ "*** Test Cases ***",
416
+ ]
417
+ for case in cases:
418
+ lines.append(f"{case.robot_title}")
419
+ lines.append(f" {case.keyword_name}")
420
+ lines.append("")
421
+ return "\n".join(lines).rstrip() + "\n"
422
+
423
+
424
+ def generate(config: GeneratorConfig) -> None:
425
+ input_path = config.input_path
426
+ py_out = config.py_out
427
+ robot_out = config.robot_out
428
+ bmc_test_db_dir = config.bmc_test_db_dir
429
+ test_db_name = config.test_db_name
430
+ fixture_dir = config.fixture_dir
431
+ repo_root = config.repo_root
432
+ with input_path.open(encoding="utf-8") as fh:
433
+ events = json.load(fh)
434
+ cases = build_cases(events)
435
+ if not cases:
436
+ raise BmcGoException("未在录制数据中找到 request/response 对。")
437
+ py_out.write_text(
438
+ build_python_module(cases, bmc_test_db_dir, test_db_name, fixture_dir, repo_root),
439
+ encoding="utf-8",
440
+ )
441
+ keywords_file = py_out.name
442
+ robot_out.write_text(build_robot_suite(cases, keywords_file), encoding="utf-8")
443
+ logging.info(
444
+ f"[OK] 生成 {len(cases)} 个用例。Python: {py_out} Robot: {robot_out}"
445
+ )
446
+
447
+
448
+ def parse_args() -> argparse.Namespace:
449
+ parser = argparse.ArgumentParser(description="根据录制数据生成 Robot 用例")
450
+ parser.add_argument(
451
+ "--bmc-test-db-dir",
452
+ required=True,
453
+ help="bmc_test_db 目录路径(例如:/opt/code/network_adapter/temp/opt/bmc/it_test/bmc_test_db)",
454
+ )
455
+ parser.add_argument(
456
+ "--test-db-name",
457
+ required=True,
458
+ help="测试数据库名称,即 bmc_test_db 下的第一层文件夹名(例如:network_adapter_y)",
459
+ )
460
+ parser.add_argument(
461
+ "--fixture-dir",
462
+ required=True,
463
+ help="fixture 目录路径(例如:/opt/code/network_adapter/temp/opt/bmc/it_test/fixture)",
464
+ )
465
+ return parser.parse_args()
466
+
467
+
468
+ def main() -> None:
469
+ args = parse_args()
470
+ # 不 resolve,直接使用传入路径,避免符号链接解析问题
471
+ bmc_test_db_dir = Path(args.bmc_test_db_dir)
472
+ if not bmc_test_db_dir.is_absolute():
473
+ bmc_test_db_dir = bmc_test_db_dir.resolve()
474
+ fixture_dir = Path(args.fixture_dir)
475
+ if not fixture_dir.is_absolute():
476
+ fixture_dir = fixture_dir.resolve()
477
+ test_db_name = args.test_db_name
478
+ # 自动构建 test_data.json 路径:bmc_test_db_dir / test_db_name / test_data / test_data.json
479
+ input_path = bmc_test_db_dir / test_db_name / "test_data" / "test_data.json"
480
+ if not input_path.exists():
481
+ raise SystemExit(f"错误:找不到 test_data.json 文件: {input_path}")
482
+ py_out = bmc_test_db_dir / test_db_name / f"{test_db_name}_keywords.py"
483
+ robot_out = bmc_test_db_dir / test_db_name / f"{test_db_name}_cases.robot"
484
+ repo_root = REPO_ROOT.resolve()
485
+ config = GeneratorConfig(input_path, py_out, robot_out, bmc_test_db_dir, test_db_name, fixture_dir, repo_root)
486
+ generate(config)
487
+
488
+
489
+ if __name__ == "__main__":
490
+ main()