已合并
【UBSE】补充HA和CLI相关IT用例 #1468
【UBSE】补充HA和CLI相关IT用例 #1468
已合并
weaver创建于 8 天前
18 个文件变更+770-9
@@ -189,6 +189,7 @@ set(UBSE_IT_SCENARIOS
189 tongsuan_1d_single_node189 tongsuan_1d_single_node
190 tongsuan_1d_two_nodes190 tongsuan_1d_two_nodes
191 tongsuan_1d_two_nodes_election_candidate_false191 tongsuan_1d_two_nodes_election_candidate_false
192+ tongsuan_1d_two_nodes_election_disabled
192 tongsuan_1d_two_nodes_ub_feature_fault193 tongsuan_1d_two_nodes_ub_feature_fault
193 tongsuan_1d_two_nodes_runtime_fault194 tongsuan_1d_two_nodes_runtime_fault
194 tongsuan_1d_two_nodes_lender_false195 tongsuan_1d_two_nodes_lender_false
@@ -201,6 +202,7 @@ set(UBSE_IT_SCENARIOS
201 tongsuan_1d_four_nodes202 tongsuan_1d_four_nodes
202 tongsuan_1d_four_nodes_two_group203 tongsuan_1d_four_nodes_two_group
203 tongsuan_1d_four_nodes_boundary_hostname204 tongsuan_1d_four_nodes_boundary_hostname
205+ tongsuan_1d_four_nodes_candidate_nodes
204 zhisuan_npu_single_node206 zhisuan_npu_single_node
205)207)
206 208 
@@ -127,6 +127,11 @@ UbseResult NodeProcessManager::Start()
127 return UBSE_ERROR_DEF(9);127 return UBSE_ERROR_DEF(9);
128 }128 }
129 129 
130+ // 清理上次进程残留的 UDS socket 文件(例如节点被 SIGKILL 后遗留)。
131+ // 若不清除,WaitForStartup 会将残留文件误判为本次进程已就绪,
132+ // 导致 API server 尚未监听时 CLI 连接即失败(IPC error 20)。
133+ unlink(udsSocketPath_.c_str());
134+ 
130 std::vector<std::string> environment = BuildChildEnvironment();135 std::vector<std::string> environment = BuildChildEnvironment();
131 std::vector<char*> envp;136 std::vector<char*> envp;
132 envp.reserve(environment.size() + 1);137 envp.reserve(environment.size() + 1);
@@ -332,4 +337,4 @@ UbseResult NodeProcessManager::InjectAlarmEvent(unsigned short alarmId, const st
332 return ItXalarmHelper::InjectEvent(xalarmFifoPath_, alarmId, paras);337 return ItXalarmHelper::InjectEvent(xalarmFifoPath_, alarmId, paras);
333}338}
334 339 
335-} // namespace ubse::it::infra340+} // namespace ubse::it::infra
@@ -241,6 +241,13 @@ TEST_F(Tongsuan1dFullMeshFourNodesScenario, MasterRestartStandbyTakesOver)
241 ubse::it::tests::election::RunFourNodeMasterRestartTest(Cluster());241 ubse::it::tests::election::RunFourNodeMasterRestartTest(Cluster());
242}242}
243 243 
244+// 错峰启动:节点2先启动并独自成为主,随后节点1、3、4同时启动。
245+// 验证已存在主节点时其余节点同时启动,集群最终收敛出唯一主(仍为节点2)+ 唯一备。
246+TEST_F(Tongsuan1dFullMeshFourNodesScenario, ExistingMasterUniqueStandby)
247+{
248+ ubse::it::tests::election::RunFourNodeExistingMasterUniqueStandbyTest(Cluster());
249+}
250+ 
244// ====================================================================251// ====================================================================
245// P1 测试 — Fault Log 校验252// P1 测试 — Fault Log 校验
246// ====================================================================253// ====================================================================
@@ -0,0 +1,3 @@
1+add_it_scene(four_nodes_candidate_nodes
2+ LINK_LIBS it_election_cases
3+)
@@ -0,0 +1,23 @@
1+/*
2+ * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved.
3+ * ubs-engine is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, NON-INFRINGEMENT, MERCHANTABILITY OR A PARTICULAR PURPOSE.
9+ * See the Mulan PSL v2 for more details.
10+ */
11+ 
12+#include "scenario.h"
13+#include "tests/election/election_cases.h"
14+ 
15+using ubse::it::infra::Tongsuan1dFourNodesCandidateNodesScenario;
16+ 
17+// 仅 election.candidateNodes 中配置的节点可以成为主:
18+// 先保留非候选节点1、2(预期不升主),启动候选节点3(升主并选备);
19+// 停止节点3(备/agent 为非候选,预期不升主),启动候选节点4(升主并选备)。
20+TEST_F(Tongsuan1dFourNodesCandidateNodesScenario, CandidateNodesOnlyMaster)
21+{
22+ ubse::it::tests::election::RunFourNodeCandidateNodesOnlyMasterTest(Cluster());
23+}
@@ -0,0 +1,24 @@
1+/*
2+ * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved.
3+ * ubs-engine is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, NON-INFRINGEMENT, MERCHANTABILITY OR A PARTICULAR PURPOSE.
9+ * See the Mulan PSL v2 for more details.
10+ */
11+ 
12+#include "it_scenario_fixture.h"
13+ 
14+// 通算1D四节点候选主节点列表场景:四节点均配置 election.candidateNodes=3,4,
15+// 仅节点"3"、"4"可以作为主节点,节点"1"、"2"为非候选节点(不能升主)。
16+IT_DEFINE_SCENARIO(Tongsuan1dFourNodesCandidateNodesScenario,
17+ MakeBuilder()
18+ .Tongsuan()
19+ .FourNode()
20+ .WithNodeConfig("1", "ubse.election", "election.candidateNodes", "3,4")
21+ .WithNodeConfig("2", "ubse.election", "election.candidateNodes", "3,4")
22+ .WithNodeConfig("3", "ubse.election", "election.candidateNodes", "3,4")
23+ .WithNodeConfig("4", "ubse.election", "election.candidateNodes", "3,4")
24+ .Start(cluster_))
@@ -211,6 +211,18 @@ TEST_F(Tongsuan1dFullMeshSingleNodeScenario, P0CliNodeBadParam02)
211 ubse::it::tests::topo::RunP0CliNodeBadParam02(Cluster());211 ubse::it::tests::topo::RunP0CliNodeBadParam02(Cluster());
212}212}
213 213 
214+// P0-CliHelpInfo-Ok-01: ubsectl -h / --help 输出全量已注册指令和参数信息, 且内容一致
215+TEST_F(Tongsuan1dFullMeshSingleNodeScenario, P0CliHelpInfoOk01)
216+{
217+ ubse::it::tests::topo::RunP0CliHelpInfoOk01(Cluster());
218+}
219+ 
220+// P0-CliCompletion-Ok-01: ubsectl 命令自动补齐功能
221+TEST_F(Tongsuan1dFullMeshSingleNodeScenario, P0CliCompletionOk01)
222+{
223+ ubse::it::tests::topo::RunP0CliCompletionOk01(Cluster());
224+}
225+ 
214// ==================== CLI Mem P0 ====================226// ==================== CLI Mem P0 ====================
215 227 
216// P0-CliCreateShare-OverLen-01: CLI create share name超长228// P0-CliCreateShare-OverLen-01: CLI create share name超长
@@ -335,4 +347,4 @@ TEST_F(Tongsuan1dFullMeshSingleNodeScenario, ElectionConvergence)
335TEST_F(Tongsuan1dFullMeshSingleNodeScenario, BmcFaultSingleNode)347TEST_F(Tongsuan1dFullMeshSingleNodeScenario, BmcFaultSingleNode)
336{348{
337 ubse::it::tests::fault::RunBmcFaultSingleNodeTest(Cluster(), "1");349 ubse::it::tests::fault::RunBmcFaultSingleNodeTest(Cluster(), "1");
338-}350+}
@@ -848,6 +848,13 @@ TEST_F(Tongsuan1dFullMeshTwoNodesScenario, P0CliCheckMemOk01)
848 ubse::it::tests::mem_borrow::RunP0CliCheckMemOk01(Cluster());848 ubse::it::tests::mem_borrow::RunP0CliCheckMemOk01(Cluster());
849}849}
850 850 
851+// P0-CliCheckMem-StatusChange-01: 内存状态巡检(节点启停联动)
852+// 双节点均启动时节点2状态ok;停止节点2后变nok;恢复节点2后恢复ok
853+TEST_F(Tongsuan1dFullMeshTwoNodesScenario, P0CliCheckMemStatusChange01)
854+{
855+ ubse::it::tests::mem_borrow::RunMemCheckStatusChangeTest(Cluster());
856+}
857+ 
851// P0-CliNodeBorrow-Ok-01: 查询节点借入汇总858// P0-CliNodeBorrow-Ok-01: 查询节点借入汇总
852TEST_F(Tongsuan1dFullMeshTwoNodesScenario, P0CliNodeBorrowOk01)859TEST_F(Tongsuan1dFullMeshTwoNodesScenario, P0CliNodeBorrowOk01)
853{860{
@@ -1073,6 +1080,14 @@ TEST_F(Tongsuan1dFullMeshTwoNodesScenario, ElectionConvergence)
1073 ubse::it::tests::election::RunTwoNodeElectionTest(Cluster());1080 ubse::it::tests::election::RunTwoNodeElectionTest(Cluster());
1074}1081}
1075 1082 
1083+// 选举测试:验证主节点向集群其他节点周期发送心跳。
1084+// 观测主节点日志 "[ELECTION] ProcTimer MASTER send pkt id=<id>"(DEBUG级别),
1085+// 等待3个心跳周期后应看到至少3次发送,且确有发往对端(备节点)的心跳。
1086+TEST_F(Tongsuan1dFullMeshTwoNodesScenario, MasterPeriodicHeartbeat)
1087+{
1088+ ubse::it::tests::election::RunTwoNodeMasterPeriodicHeartbeatTest(Cluster());
1089+}
1090+ 
1076// ====================================================================1091// ====================================================================
1077// P1 测试 — 拓扑校验1092// P1 测试 — 拓扑校验
1078// ====================================================================1093// ====================================================================
@@ -1145,4 +1160,4 @@ TEST_F(Tongsuan1dFullMeshTwoNodesScenario, P1FaultLogShareDetachNotExist01)
1145TEST_F(Tongsuan1dFullMeshTwoNodesScenario, ITSeiDegradeLifecycle)1160TEST_F(Tongsuan1dFullMeshTwoNodesScenario, ITSeiDegradeLifecycle)
1146{1161{
1147 ubse::it::tests::sei_degrade::RunITSeiDegradeLifecycle(Cluster());1162 ubse::it::tests::sei_degrade::RunITSeiDegradeLifecycle(Cluster());
1148-}1163+}
@@ -0,0 +1,3 @@
1+add_it_scene(two_nodes_election_disabled
2+ LINK_LIBS it_election_cases
3+)
@@ -0,0 +1,22 @@
1+/*
2+ * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved.
3+ * ubs-engine is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, NON-INFRINGEMENT, MERCHANTABILITY OR A PARTICULAR PURPOSE.
9+ * See the Mulan PSL v2 for more details.
10+ */
11+ 
12+#include "scenario.h"
13+#include "tests/election/election_cases.h"
14+ 
15+using ubse::it::infra::Tongsuan1dTwoNodesElectionDisabledScenario;
16+ 
17+// 双节点均不参与选主与等待测试:两节点均配置 election.candidate=false 且 election.wait=false,
18+// 预期两节点均停留在 init 角色(不升主、不升备、不升 agent)。
19+TEST_F(Tongsuan1dTwoNodesElectionDisabledScenario, ElectionBothDisabledKeepInit)
20+{
21+ ubse::it::tests::election::RunTwoNodeBothElectionDisabledTest(Cluster());
22+}
@@ -0,0 +1,26 @@
1+/*
2+ * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved.
3+ * ubs-engine is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, NON-INFRINGEMENT, MERCHANTABILITY OR A PARTICULAR PURPOSE.
9+ * See the Mulan PSL v2 for more details.
10+ */
11+ 
12+#include "it_scenario_fixture.h"
13+ 
14+// 通算1D双节点均不参与选主与等待场景:节点"1"、"2"均配置 election.candidate=false
15+// 且 election.wait=false,预期两节点均停留在 init 角色,不会升主、升备或升 agent。
16+IT_DEFINE_SCENARIO(Tongsuan1dTwoNodesElectionDisabledScenario,
17+ MakeBuilder()
18+ .Tongsuan()
19+ .TwoNode()
20+ // 两节点均不参与选举,集群永不收敛,启动时不等待选举结果
21+ .NoElection()
22+ .WithNodeConfig("1", "ubse.election", "election.candidate", "false")
23+ .WithNodeConfig("1", "ubse.election", "election.wait", "false")
24+ .WithNodeConfig("2", "ubse.election", "election.candidate", "false")
25+ .WithNodeConfig("2", "ubse.election", "election.wait", "false")
26+ .Start(cluster_))
@@ -68,6 +68,17 @@ bool IsObmmDevicePath(const char* path)
68 return strncmp(path, UBSE_OBMM_DEV_PREFIX, strlen(UBSE_OBMM_DEV_PREFIX)) == 0;68 return strncmp(path, UBSE_OBMM_DEV_PREFIX, strlen(UBSE_OBMM_DEV_PREFIX)) == 0;
69}69}
70 70 
71+bool IsObmmModulePath(const char* path)
72+{
73+ if (path == nullptr) {
74+ return false;
75+ }
76+ // 生产机中 /sys/module/obmm 是 obmm 内核模块的加载目录,collector 通过
77+ // stat() 判断该目录是否存在来决定 obmmState。IT 环境无该内核模块,这里
78+ // 模拟目录存在,使在线节点的 check memory 健康状态可判定为 ok。
79+ return strcmp(path, "/sys/module/obmm") == 0;
80+}
81+ 
71bool CsvContainsIp(const char* csv, in_addr_t ip)82bool CsvContainsIp(const char* csv, in_addr_t ip)
72{83{
73 if (csv == nullptr || csv[0] == '\0') {84 if (csv == nullptr || csv[0] == '\0') {
@@ -446,6 +457,17 @@ extern "C" FILE* popen(const char* command, const char* mode)
446 }457 }
447 }458 }
448 459 
460+ // sysSentry IT 模拟:生产机上由 sentryctl 查询/配置 sysSentry 服务,
461+ // IT 环境无该命令。这里拦截以保证节点健康状态可判定为 ok:
462+ // - "sentryctl status ..." => 输出 RUNNING,使 isSentryMsgMonitorRunning=true
463+ // - "sentryctl set ..." => 返回成功(exit 0),使 sysSentry 配置成功
464+ if (command != nullptr && strstr(command, "sentryctl") != nullptr) {
465+ if (strstr(command, "status") != nullptr) {
466+ return real_popen_func("echo status: RUNNING", "r");
467+ }
468+ return real_popen_func("echo ok", "r");
469+ }
470+ 
449 return real_popen_func(command, mode);471 return real_popen_func(command, mode);
450}472}
451 473 
@@ -505,6 +527,17 @@ extern "C" int lstat(const char* path, struct stat* buf)
505 return 0;527 return 0;
506 }528 }
507 529 
530+ // 模拟 obmm 内核模块加载目录存在,使 obmmState 判定为 INSERTED
531+ if (IsObmmModulePath(path)) {
532+ if (buf != nullptr) {
533+ memset(buf, 0, sizeof(struct stat));
534+ buf->st_mode = S_IFDIR | 0755;
535+ buf->st_uid = getuid();
536+ buf->st_gid = getgid();
537+ }
538+ return 0;
539+ }
540+ 
508 init_real_conf_funcs();541 init_real_conf_funcs();
509 if (real_lstat == nullptr) {542 if (real_lstat == nullptr) {
510 errno = ENOSYS;543 errno = ENOSYS;
@@ -544,6 +577,17 @@ extern "C" int stat(const char* path, struct stat* buf)
544 return 0;577 return 0;
545 }578 }
546 579 
580+ // 模拟 obmm 内核模块加载目录存在,使 obmmState 判定为 INSERTED
581+ if (IsObmmModulePath(path)) {
582+ if (buf != nullptr) {
583+ memset(buf, 0, sizeof(struct stat));
584+ buf->st_mode = S_IFDIR | 0755;
585+ buf->st_uid = getuid();
586+ buf->st_gid = getgid();
587+ }
588+ return 0;
589+ }
590+ 
547 init_real_conf_funcs();591 init_real_conf_funcs();
548 if (real_stat == nullptr) {592 if (real_stat == nullptr) {
549 errno = ENOSYS;593 errno = ENOSYS;
@@ -811,4 +855,4 @@ extern "C" int getpwuid_r(uid_t uid, struct passwd* pwd, char* buf, size_t bufle
811 pwd->pw_name = buf;855 pwd->pw_name = buf;
812 *result = pwd;856 *result = pwd;
813 return 0;857 return 0;
814-}858+}
@@ -20,6 +20,7 @@
20 20 
21#include "ubse_common_def.h"21#include "ubse_common_def.h"
22#include "it_assertion.h"22#include "it_assertion.h"
23+#include "it_string_util.h"
23#include "it_wait_helper.h"24#include "it_wait_helper.h"
24 25 
25namespace ubse::it::tests::election {26namespace ubse::it::tests::election {
@@ -128,6 +129,35 @@ void CheckMasterPeriodicHeartbeat(ubse::it::infra::ItCluster& cluster, const std
128 EXPECT_GE(sendsToPeer, 1U) << "master should send heartbeat to peer node " << peerNodeId;129 EXPECT_GE(sendsToPeer, 1U) << "master should send heartbeat to peer node " << peerNodeId;
129}130}
130 131 
132+// 判断指定运行节点集合中是否已收敛为唯一主 + 唯一备;若 expectMaster 非空,还要求主节点为该节点。
133+bool HasUniqueMasterAndStandby(ubse::it::infra::ItCluster& cluster, const std::vector<std::string>& runningIds,
134+ const std::string& expectMaster)
135+{
136+ std::string masterId;
137+ std::string standbyId;
138+ for (const auto& id : runningIds) {
139+ std::string role;
140+ if (cluster.GetCliInvoker(id).GetRole(role) != UBS_SUCCESS) {
141+ return false;
142+ }
143+ if (role == ubse::election::ELECTION_ROLE_MASTER) {
144+ if (!masterId.empty()) {
145+ return false; // 存在多个主,尚未收敛
146+ }
147+ masterId = id;
148+ } else if (role == ubse::election::ELECTION_ROLE_STANDBY) {
149+ if (!standbyId.empty()) {
150+ return false; // 存在多个备,尚未收敛
151+ }
152+ standbyId = id;
153+ }
154+ }
155+ if (masterId.empty() || standbyId.empty()) {
156+ return false;
157+ }
158+ return expectMaster.empty() || masterId == expectMaster;
159+}
160+ 
131} // namespace161} // namespace
132 162 
133// 遍历集群所有节点,统计各选举角色的数量163// 遍历集群所有节点,统计各选举角色的数量
@@ -199,6 +229,25 @@ void RunTwoNodeElectionTest(ubse::it::infra::ItCluster& cluster)
199 CheckMasterPeriodicHeartbeat(cluster, roles.masterNodeId, peerNodeId);229 CheckMasterPeriodicHeartbeat(cluster, roles.masterNodeId, peerNodeId);
200}230}
201 231 
232+// 双节点主节点周期心跳测试:收敛为1主+1备后,验证主节点向集群其他节点周期发送心跳。
233+// 观测主节点日志中的 "[ELECTION] ProcTimer MASTER send pkt id=<id>"(DEBUG级别):
234+// 等待 3 个心跳周期后,主节点至少发出 3 次心跳(证明周期性),且确有发往对端节点的心跳。
235+void RunTwoNodeMasterPeriodicHeartbeatTest(ubse::it::infra::ItCluster& cluster)
236+{
237+ // 收敛为1主+1备,并确认主节点为节点"1"(双节点默认配置,最小节点升主)
238+ auto roles = CollectElectionRoles(cluster);
239+ ASSERT_EQ(roles.masterCount, 1U) << "集群应存在唯一主节点";
240+ ASSERT_EQ(roles.standbyCount, 1U) << "集群应存在唯一备节点";
241+ ASSERT_EQ(roles.masterNodeId, "1") << "双节点默认配置下主节点应为节点1";
242+ ASSERT_EQ(roles.standbyNodeId, "2") << "双节点默认配置下备节点应为节点2";
243+ 
244+ // 双节点环境中,主节点的对端节点即备节点(节点2)
245+ std::string peerNodeId = roles.standbyNodeId;
246+ 
247+ // 通过主节点日志校验周期心跳:周期发送(>=3次)且确实发往对端节点
248+ CheckMasterPeriodicHeartbeat(cluster, roles.masterNodeId, peerNodeId);
249+}
250+ 
202// 双节点选举候选约束测试:最小节点 candidate=false、另一节点 candidate=true,251// 双节点选举候选约束测试:最小节点 candidate=false、另一节点 candidate=true,
203// 收敛后主节点应为候选节点、备节点为最小节点,并验证主节点周期性心跳。252// 收敛后主节点应为候选节点、备节点为最小节点,并验证主节点周期性心跳。
204void RunTwoNodeElectionCandidateFalseTest(ubse::it::infra::ItCluster& cluster)253void RunTwoNodeElectionCandidateFalseTest(ubse::it::infra::ItCluster& cluster)
@@ -255,6 +304,60 @@ void RunTwoNodeElectionCandidateFalseTest(ubse::it::infra::ItCluster& cluster)
255 CheckMasterPeriodicHeartbeat(cluster, roles.masterNodeId, peerNodeId);304 CheckMasterPeriodicHeartbeat(cluster, roles.masterNodeId, peerNodeId);
256}305}
257 306 
307+// 双节点均不参与选主与等待测试:两节点均配置 election.candidate=false 且 election.wait=false。
308+// 预期两节点始终停留在 init(Initializer)角色:不升主、不升备、不升 agent。
309+// 通过日志校验:正面信号为角色初始化与选举定时器运行;反面信号为无任何角色切换(Master/Standby/Agent)。
310+void RunTwoNodeBothElectionDisabledTest(ubse::it::infra::ItCluster& cluster)
311+{
312+ const auto& nodeIds = cluster.GetNodeIds();
313+ ASSERT_EQ(nodeIds.size(), 2U) << "本用例要求双节点集群,实际=" << nodeIds.size();
314+ 
315+ // 1. 配置校验:两节点均 candidate=false 且 wait=false
316+ for (const auto& id : nodeIds) {
317+ const std::string cfg = cluster.GetNode(id).GetConfigFilePath();
318+ EXPECT_EQ(ReadConfigValue(cfg, "ubse.election", "election.candidate"), "false")
319+ << "node " << id << " should have election.candidate=false";
320+ EXPECT_EQ(ReadConfigValue(cfg, "ubse.election", "election.wait"), "false")
321+ << "node " << id << " should have election.wait=false";
322+ }
323+ 
324+ // 2. 等待选举超时:若节点会升主/升备,在数个心跳周期内即会发生;等待 3 个周期 + 余量
325+ uint32_t hbIntervalMs = 2000;
326+ auto intervalStr =
327+ ReadConfigValue(cluster.GetNode(nodeIds[0]).GetConfigFilePath(), "ubse.election", "heartbeat.timeInterval");
328+ if (!intervalStr.empty()) {
329+ try {
330+ hbIntervalMs = static_cast<uint32_t>(std::stoul(intervalStr));
331+ } catch (const std::exception&) {
332+ hbIntervalMs = 2000;
333+ }
334+ }
335+ if (hbIntervalMs == 0) {
336+ hbIntervalMs = 2000;
337+ }
338+ std::this_thread::sleep_for(std::chrono::milliseconds(3 * static_cast<int64_t>(hbIntervalMs) + 2000));
339+ 
340+ // 3. 日志校验:正面为角色初始化 + 选举定时器运行;反面为无任何角色切换
341+ for (const auto& id : nodeIds) {
342+ const std::string log = cluster.GetNode(id).GetLogFilePath();
343+ // 正面:角色初始化日志说明节点停留在 init 角色
344+ EXPECT_GE(CountLogLines(log, "[ELECTION] Initializer:"), 1U)
345+ << "node " << id << " should stay in init role, log=" << log;
346+ // 正面:选举定时器持续运行
347+ EXPECT_GE(CountLogLines(log, "[ELECTION] local node state is ready, start to elect."), 1U)
348+ << "node " << id << " should keep election timer running, log=" << log;
349+ // 反面:不应升主
350+ EXPECT_EQ(CountLogLines(log, "[ELECTION] SwitchRole Master"), 0U)
351+ << "node " << id << " should NOT switch to master";
352+ // 反面:不应升备
353+ EXPECT_EQ(CountLogLines(log, "[ELECTION] SwitchRole Standby"), 0U)
354+ << "node " << id << " should NOT switch to standby";
355+ // 反面:不应升 agent
356+ EXPECT_EQ(CountLogLines(log, "[ELECTION] SwitchRole Agent"), 0U)
357+ << "node " << id << " should NOT switch to agent";
358+ }
359+}
360+ 
258// 四节点选举测试:验证集群收敛为1主+1备+2代理361// 四节点选举测试:验证集群收敛为1主+1备+2代理
259void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster)362void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster)
260{363{
@@ -312,4 +415,173 @@ void RunFourNodeMasterRestartTest(ubse::it::infra::ItCluster& cluster)
312 EXPECT_EQ(after.agentCount, 2U);415 EXPECT_EQ(after.agentCount, 2U);
313}416}
314 417 
418+// 四节点错峰启动测试:节点2先启动并独自成为主节点,随后节点1、3、4同时启动。
419+// 验证已存在主节点时,其余节点同时启动后集群最终收敛出唯一主(仍为节点2)+ 唯一备。
420+//
421+// 场景先全部启动并收敛,再复用现有故障注入接口模拟错峰启动:
422+// 1) 前置:集群已全部启动并收敛为1主+1备+2agent(默认主=节点1、备=节点2)
423+// 2) KillNode 停掉节点1、3、4 -> 仅剩节点2(相当于只有节点2先启动)
424+// 3) 节点2检测主节点1下线后于心跳超时升主,等待其角色成为 master(CLI 查询)
425+// 4) RestartNode 同时恢复节点1、3、4(新节点以 init 身份加入,被主节点2收编)
426+// 5) 等待收敛后通过 CLI display node / display cluster 验证:
427+// 唯一主(节点2)+ 唯一备 + 2 agent
428+void RunFourNodeExistingMasterUniqueStandbyTest(ubse::it::infra::ItCluster& cluster)
429+{
430+ const auto& nodeIds = cluster.GetNodeIds();
431+ ASSERT_EQ(nodeIds.size(), 4U) << "本用例要求4节点集群,实际=" << nodeIds.size();
432+ 
433+ // 1. 前置:集群已全部启动并收敛为1主+1备+2agent
434+ auto roles = CollectElectionRoles(cluster);
435+ ASSERT_EQ(roles.masterCount, 1U);
436+ ASSERT_EQ(roles.standbyCount, 1U);
437+ 
438+ // 2. 停掉节点1、3、4,仅保留节点2
439+ ASSERT_IT_OK(cluster.KillNode("1"));
440+ ASSERT_IT_OK(cluster.KillNode("3"));
441+ ASSERT_IT_OK(cluster.KillNode("4"));
442+ ASSERT_FALSE(cluster.IsNodeRunning("1"));
443+ ASSERT_FALSE(cluster.IsNodeRunning("3"));
444+ ASSERT_FALSE(cluster.IsNodeRunning("4"));
445+ ASSERT_TRUE(cluster.IsNodeRunning("2"));
446+ 
447+ // 3. 等待节点2独自成为主节点(主节点1下线,节点2心跳超时后升主)
448+ auto ret = ubse::it::infra::ItWaitHelper::WaitForCondition(
449+ [&]() -> bool {
450+ std::string role;
451+ return cluster.GetCliInvoker("2").GetRole(role) == UBS_SUCCESS &&
452+ role == ubse::election::ELECTION_ROLE_MASTER;
453+ },
454+ 30000);
455+ EXPECT_IT_OK(ret) << "仅剩节点2时应由节点2升为主节点";
456+ 
457+ // 4. 同时恢复节点1、3、4(不逐个等待收敛,最后统一收敛)
458+ ASSERT_IT_OK(cluster.RestartNode("1", false));
459+ ASSERT_IT_OK(cluster.RestartNode("3", false));
460+ ASSERT_IT_OK(cluster.RestartNode("4", false));
461+ ASSERT_TRUE(cluster.IsNodeRunning("1"));
462+ ASSERT_TRUE(cluster.IsNodeRunning("3"));
463+ ASSERT_TRUE(cluster.IsNodeRunning("4"));
464+ 
465+ // 5. 等待集群选举收敛(1主+1备+2agent)
466+ ASSERT_IT_OK(cluster.WaitForElectionConvergence(30000));
467+ 
468+ // 6. 通过 CLI display node 统计角色:唯一主、唯一备,主节点仍为节点2
469+ auto finalRoles = CollectElectionRoles(cluster);
470+ EXPECT_EQ(finalRoles.masterCount, 1U) << "集群应存在唯一主节点";
471+ EXPECT_EQ(finalRoles.standbyCount, 1U) << "集群应存在唯一备节点";
472+ EXPECT_EQ(finalRoles.agentCount, 2U) << "集群应存在2个agent节点";
473+ EXPECT_EQ(finalRoles.agentNodeIds.size(), 2U);
474+ EXPECT_EQ(finalRoles.masterNodeId, "2") << "主节点应保持为节点2";
475+ 
476+ // 7. 通过 CLI display cluster 从主节点2视角再次确认唯一主备
477+ std::vector<ubse::it::infra::ItNodeInfo> nodeInfos;
478+ EXPECT_EQ(cluster.GetCliInvoker("2").QueryClusterInfo(nodeInfos), UBS_SUCCESS);
479+ uint32_t masterCount = 0;
480+ uint32_t standbyCount = 0;
481+ uint32_t agentCount = 0;
482+ std::string masterInView;
483+ for (const auto& info : nodeInfos) {
484+ if (info.role == ubse::election::ELECTION_ROLE_MASTER) {
485+ ++masterCount;
486+ masterInView = ubse::it::infra::util::ExtractNodeId(info.node);
487+ } else if (info.role == ubse::election::ELECTION_ROLE_STANDBY) {
488+ ++standbyCount;
489+ } else if (info.role == ubse::election::ELECTION_ROLE_AGENT) {
490+ ++agentCount;
491+ }
492+ }
493+ EXPECT_EQ(masterCount, 1U) << "display cluster 应显示唯一主节点";
494+ EXPECT_EQ(standbyCount, 1U) << "display cluster 应显示唯一备节点";
495+ EXPECT_EQ(agentCount, 2U) << "display cluster 应显示2个agent节点";
496+ EXPECT_EQ(masterInView, "2") << "display cluster 中主节点应仍为节点2";
497+}
498+ 
499+// 四节点候选主节点列表约束测试:仅 election.candidateNodes 中配置的节点可以成为主。
500+// 场景配置 candidateNodes=3,4,非候选节点(1、2)始终不能升主。
501+// 步骤:① 仅保留非候选节点1、2,等待超时预期不升主;② 启动候选节点3,预期3升主并选出唯一备;
502+// ③ 停止节点3,非候选备/agent 预期不升主;④ 启动候选节点4,预期4升主并选出唯一备。
503+void RunFourNodeCandidateNodesOnlyMasterTest(ubse::it::infra::ItCluster& cluster)
504+{
505+ const auto& nodeIds = cluster.GetNodeIds();
506+ ASSERT_EQ(nodeIds.size(), 4U) << "本用例要求四节点集群,实际=" << nodeIds.size();
507+ 
508+ // 读取心跳配置,用于计算"不升主"验证的等待时长
509+ const std::string cfgPath = cluster.GetNode("1").GetConfigFilePath();
510+ uint32_t hbIntervalMs = 2000;
511+ uint32_t hbLost = 3;
512+ auto intervalStr = ReadConfigValue(cfgPath, "ubse.election", "heartbeat.timeInterval");
513+ if (!intervalStr.empty()) {
514+ try {
515+ hbIntervalMs = static_cast<uint32_t>(std::stoul(intervalStr));
516+ } catch (const std::exception&) {
517+ hbIntervalMs = 2000;
518+ }
519+ }
520+ auto lostStr = ReadConfigValue(cfgPath, "ubse.election", "heartbeat.lostThreshold");
521+ if (!lostStr.empty()) {
522+ try {
523+ hbLost = static_cast<uint32_t>(std::stoul(lostStr));
524+ } catch (const std::exception&) {
525+ hbLost = 3;
526+ }
527+ }
528+ if (hbIntervalMs == 0) {
529+ hbIntervalMs = 2000;
530+ }
531+ if (hbLost == 0) {
532+ hbLost = 3;
533+ }
534+ 
535+ // 0. 配置校验:所有节点均应配置 candidateNodes=3,4
536+ for (const auto& id : nodeIds) {
537+ EXPECT_EQ(ReadConfigValue(cluster.GetNode(id).GetConfigFilePath(), "ubse.election", "election.candidateNodes"),
538+ "3,4")
539+ << "node " << id << " should have election.candidateNodes=3,4";
540+ }
541+ 
542+ // 1. 前置:集群已收敛,存在唯一主节点(候选节点)
543+ auto preRoles = CollectElectionRoles(cluster);
544+ ASSERT_EQ(preRoles.masterCount, 1U) << "前置收敛后应存在唯一主节点";
545+ 
546+ // 2. 停掉候选节点3、4,仅保留非候选节点1、2
547+ ASSERT_IT_OK(cluster.KillNode("3"));
548+ ASSERT_IT_OK(cluster.KillNode("4"));
549+ ASSERT_FALSE(cluster.IsNodeRunning("3"));
550+ ASSERT_FALSE(cluster.IsNodeRunning("4"));
551+ 
552+ // 3. 非候选节点1、2 不会升主:等待超过选举周期(4个心跳周期+余量)后仍无 SwitchRole Master
553+ std::this_thread::sleep_for(std::chrono::milliseconds(4 * static_cast<int64_t>(hbIntervalMs) + 2000));
554+ for (const auto& id : {std::string("1"), std::string("2")}) {
555+ EXPECT_EQ(CountLogLines(cluster.GetNode(id).GetLogFilePath(), "[ELECTION] SwitchRole Master"), 0U)
556+ << "non-candidate node " << id << " should NOT become master";
557+ }
558+ 
559+ // 4. 启动候选节点3:3 应升为主并选出唯一备节点
560+ ASSERT_IT_OK(cluster.RestartNode("3", false));
561+ auto ret3 = ubse::it::infra::ItWaitHelper::WaitForCondition(
562+ [&]() -> bool {
563+ return HasUniqueMasterAndStandby(cluster, {"1", "2", "3"}, "3");
564+ },
565+ 30000);
566+ EXPECT_IT_OK(ret3) << "candidate node 3 should become master and appoint a unique standby";
567+ 
568+ // 5. 停止节点3(主):非候选备/agent 心跳超时后不应升主
569+ ASSERT_IT_OK(cluster.KillNode("3"));
570+ ASSERT_FALSE(cluster.IsNodeRunning("3"));
571+ std::this_thread::sleep_for(std::chrono::milliseconds((static_cast<int64_t>(hbLost) + 2) * hbIntervalMs + 2000));
572+ for (const auto& id : {std::string("1"), std::string("2")}) {
573+ EXPECT_EQ(CountLogLines(cluster.GetNode(id).GetLogFilePath(), "[ELECTION] SwitchRole Master"), 0U)
574+ << "non-candidate node " << id << " should NOT become master after master(3) down";
575+ }
576+ 
577+ // 6. 启动候选节点4:4 应升为主并选出唯一备节点
578+ ASSERT_IT_OK(cluster.RestartNode("4", false));
579+ auto ret4 = ubse::it::infra::ItWaitHelper::WaitForCondition(
580+ [&]() -> bool {
581+ return HasUniqueMasterAndStandby(cluster, {"1", "2", "4"}, "4");
582+ },
583+ 30000);
584+ EXPECT_IT_OK(ret4) << "candidate node 4 should become master and appoint a unique standby";
585+}
586+ 
315} // namespace ubse::it::tests::election587} // namespace ubse::it::tests::election
@@ -40,16 +40,32 @@ void RunSingleNodeElectionTest(ubse::it::infra::ItCluster& cluster);
40// 双节点选举测试:验证集群收敛为1主+1备(默认候选配置)40// 双节点选举测试:验证集群收敛为1主+1备(默认候选配置)
41void RunTwoNodeElectionTest(ubse::it::infra::ItCluster& cluster);41void RunTwoNodeElectionTest(ubse::it::infra::ItCluster& cluster);
42 42 
43+// 双节点主节点周期心跳测试:收敛为1主+1备后,通过主节点日志
44+// "[ELECTION] ProcTimer MASTER send pkt id=<id>" 验证主节点周期性地向对端节点发送心跳。
45+void RunTwoNodeMasterPeriodicHeartbeatTest(ubse::it::infra::ItCluster& cluster);
46+ 
43// 双节点选举候选约束测试:最小节点 candidate=false、另一节点 candidate=true,47// 双节点选举候选约束测试:最小节点 candidate=false、另一节点 candidate=true,
44// 收敛后主节点应为候选节点、备节点为最小节点,并验证主节点周期性心跳。48// 收敛后主节点应为候选节点、备节点为最小节点,并验证主节点周期性心跳。
45void RunTwoNodeElectionCandidateFalseTest(ubse::it::infra::ItCluster& cluster);49void RunTwoNodeElectionCandidateFalseTest(ubse::it::infra::ItCluster& cluster);
46 50 
51+// 双节点均不参与选主与等待测试:两节点均配置 election.candidate=false 且 election.wait=false。
52+// 预期两节点均停留在 init 角色,不升主、不升备、不升 agent。
53+void RunTwoNodeBothElectionDisabledTest(ubse::it::infra::ItCluster& cluster);
54+ 
47// 四节点选举测试:验证集群收敛为1主+1备+2代理55// 四节点选举测试:验证集群收敛为1主+1备+2代理
48void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster);56void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster);
49 57 
50// 四节点主节点重启测试:收敛后重启主节点,备节点应接管成为新主,集群最终重新收敛58// 四节点主节点重启测试:收敛后重启主节点,备节点应接管成为新主,集群最终重新收敛
51void RunFourNodeMasterRestartTest(ubse::it::infra::ItCluster& cluster);59void RunFourNodeMasterRestartTest(ubse::it::infra::ItCluster& cluster);
52 60 
61+// 四节点错峰启动测试:节点2先启动并独自成为主节点,随后节点1、3、4同时启动。
62+// 验证已存在主节点时其余节点同时启动,集群最终收敛出唯一主(仍为节点2)+ 唯一备。
63+void RunFourNodeExistingMasterUniqueStandbyTest(ubse::it::infra::ItCluster& cluster);
64+ 
65+// 四节点候选主节点列表约束测试:仅 election.candidateNodes 中配置的节点可以成为主。
66+// 配置 candidateNodes=3,4:非候选节点(1、2)始终不能升主;候选节点3、4可先后升主并选出唯一备。
67+void RunFourNodeCandidateNodesOnlyMasterTest(ubse::it::infra::ItCluster& cluster);
68+ 
53} // namespace ubse::it::tests::election69} // namespace ubse::it::tests::election
54 70 
55-#endif // IT_ELECTION_CASES_H71+#endif // IT_ELECTION_CASES_H
@@ -19,6 +19,7 @@
19#include <unistd.h>19#include <unistd.h>
20 20 
21#include <future>21#include <future>
22+#include <sstream>
22#include <thread>23#include <thread>
23 24 
24#include "ubse_common_def.h"25#include "ubse_common_def.h"
@@ -60,6 +61,35 @@ void CreateShmWithLenderAndVerify(ubse::it::infra::ItSdkClient& sdk, const char*
60 61 
61 EXPECT_IT_OK(sdk.MemShmDelete(name)) << "清理共享内存应成功";62 EXPECT_IT_OK(sdk.MemShmDelete(name)) << "清理共享内存应成功";
62}63}
64+ 
65+// 从 check memory 表格输出中解析指定 slotId 节点的状态 (ok/nok)
66+// 表格格式与 display node 一致,列间以空白分隔,行间以 "---" 分隔
67+bool ParseMemCheckStatus(const std::string& output, const std::string& slotId, std::string& status)
68+{
69+ std::istringstream iss(output);
70+ std::string line;
71+ int separatorCount = 0;
72+ while (std::getline(iss, line)) {
73+ if (line.find("---") != std::string::npos) {
74+ ++separatorCount;
75+ continue;
76+ }
77+ // 表头及之前的内容跳过
78+ if (separatorCount < 2) {
79+ continue;
80+ }
81+ std::istringstream lineStream(line);
82+ std::string node;
83+ std::string st;
84+ lineStream >> node >> st;
85+ // 节点单元格格式为 hostname(slotId),下线时显示 -(slotId)
86+ if (node.find("(" + slotId + ")") != std::string::npos) {
87+ status = st;
88+ return true;
89+ }
90+ }
91+ return false;
92+}
63} // namespace93} // namespace
64 94 
65// CLI查询节点内存状态测试95// CLI查询节点内存状态测试
@@ -95,6 +125,54 @@ void RunP0CliCheckMemOk01(ubse::it::infra::ItCluster& cluster)
95 IT_LOG_INFO << "CLI check memory test passed";125 IT_LOG_INFO << "CLI check memory test passed";
96}126}
97 127 
128+// 内存状态巡检(节点启停联动):
129+// 1) 双节点均启动时,check memory 中节点2状态应为 ok;
130+// 2) 停止节点2后,等待节点1检测到节点2下线,节点2状态应变为 nok;
131+// 3) 恢复节点2并等待选举收敛后,节点2状态应恢复为 ok。
132+void RunMemCheckStatusChangeTest(ubse::it::infra::ItCluster& cluster)
133+{
134+ auto& cliInvoker = cluster.GetCliInvoker("1");
135+ const std::string slotId = "2";
136+ 
137+ // 1. 双节点均在线:等待节点2状态为 ok
138+ // (check memory 的 ok 依赖 sysSentry/obmm 状态,二者由后台定时器刷新,
139+ // 故需轮询等待,避免首次查询时状态尚未刷新导致误判)
140+ auto ret = ubse::it::infra::ItWaitHelper::WaitForCondition(
141+ [&cliInvoker, &slotId]() -> bool {
142+ std::string cur = cliInvoker.ExecCli("check memory");
143+ std::string s;
144+ return ParseMemCheckStatus(cur, slotId, s) && s == "ok";
145+ },
146+ ubse::it::infra::ItWaitHelper::DEFAULT_ELECTION_TIMEOUT_MS);
147+ ASSERT_IT_OK(ret) << "双节点均启动时,check memory 中节点2状态应为 ok";
148+ 
149+ // 2. 停止节点2:等待节点1检测到节点2下线,节点2状态应变为 nok
150+ ASSERT_IT_OK(cluster.KillNode("2")) << "停止节点2应成功";
151+ 
152+ ret = ubse::it::infra::ItWaitHelper::WaitForCondition(
153+ [&cliInvoker, &slotId]() -> bool {
154+ std::string cur = cliInvoker.ExecCli("check memory");
155+ std::string s;
156+ return ParseMemCheckStatus(cur, slotId, s) && s == "nok";
157+ },
158+ ubse::it::infra::ItWaitHelper::DEFAULT_ELECTION_TIMEOUT_MS);
159+ ASSERT_IT_OK(ret) << "停止节点2后,check memory 中节点2状态应变为 nok";
160+ 
161+ // 3. 恢复节点2:重启并等待选举收敛,节点2状态应恢复为 ok
162+ ASSERT_IT_OK(cluster.RestartNode("2", true, 30000)) << "恢复节点2应成功";
163+ 
164+ ret = ubse::it::infra::ItWaitHelper::WaitForCondition(
165+ [&cliInvoker, &slotId]() -> bool {
166+ std::string cur = cliInvoker.ExecCli("check memory");
167+ std::string s;
168+ return ParseMemCheckStatus(cur, slotId, s) && s == "ok";
169+ },
170+ ubse::it::infra::ItWaitHelper::DEFAULT_ELECTION_TIMEOUT_MS);
171+ ASSERT_IT_OK(ret) << "恢复节点2后,check memory 中节点2状态应恢复为 ok";
172+ 
173+ IT_LOG_INFO << "Mem check status change test passed";
174+}
175+ 
98// CLI节点借入汇总查询测试176// CLI节点借入汇总查询测试
99void RunP0CliNodeBorrowOk01(ubse::it::infra::ItCluster& cluster)177void RunP0CliNodeBorrowOk01(ubse::it::infra::ItCluster& cluster)
100{178{
@@ -4666,4 +4744,4 @@ void RunP0CliDetachMemOverLen01(ubse::it::infra::ItCluster& cluster)
4666 IT_LOG_INFO << "P0-CliDetachMem-OverLen-01 passed";4744 IT_LOG_INFO << "P0-CliDetachMem-OverLen-01 passed";
4667}4745}
4668 4746 
4669-} // namespace ubse::it::tests::mem_borrow4747+} // namespace ubse::it::tests::mem_borrow
@@ -24,6 +24,9 @@ namespace ubse::it::tests::mem_borrow {
24// CLI查询节点内存状态测试:调用check memory命令,验证返回包含两个节点的状态信息24// CLI查询节点内存状态测试:调用check memory命令,验证返回包含两个节点的状态信息
25void RunP0CliCheckMemOk01(ubse::it::infra::ItCluster& cluster);25void RunP0CliCheckMemOk01(ubse::it::infra::ItCluster& cluster);
26 26 
27+// 内存状态巡检(节点启停联动):双节点均启动时节点2状态ok;停止节点2后变nok;恢复后恢复ok
28+void RunMemCheckStatusChangeTest(ubse::it::infra::ItCluster& cluster);
29+ 
27// CLI节点借入汇总查询测试30// CLI节点借入汇总查询测试
28void RunP0CliNodeBorrowOk01(ubse::it::infra::ItCluster& cluster);31void RunP0CliNodeBorrowOk01(ubse::it::infra::ItCluster& cluster);
29 32 
@@ -324,4 +327,4 @@ void RunP0CliDetachMemOverLen01(ubse::it::infra::ItCluster& cluster);
324 327 
325} // namespace ubse::it::tests::mem_borrow328} // namespace ubse::it::tests::mem_borrow
326 329 
327-#endif // IT_MEM_BORROW_CASES_H330+#endif // IT_MEM_BORROW_CASES_H
@@ -22,11 +22,18 @@
22#include "it_string_util.h"22#include "it_string_util.h"
23#include "ubs_engine_topo.h"23#include "ubs_engine_topo.h"
24 24 
25+#include <unistd.h>
25#include <algorithm>26#include <algorithm>
27+#include <array>
28+#include <climits>
29+#include <cstdio>
30+#include <cstring>
26#include <map>31#include <map>
27#include <memory>32#include <memory>
28#include <regex>33#include <regex>
29#include <set>34#include <set>
35+#include <sstream>
36+#include <vector>
30 37 
31namespace ubse::it::tests::topo {38namespace ubse::it::tests::topo {
32namespace util = ubse::it::infra::util;39namespace util = ubse::it::infra::util;
@@ -120,6 +127,85 @@ bool HasValidIp(const ubs_topo_node_t& node)
120 return false;127 return false;
121}128}
122 129 
130+// 统计子串 text 中子串 sub 出现的次数
131+size_t CountOccurrences(const std::string& text, const std::string& sub)
132+{
133+ size_t count = 0;
134+ size_t pos = 0;
135+ while ((pos = text.find(sub, pos)) != std::string::npos) {
136+ ++count;
137+ pos += sub.length();
138+ }
139+ return count;
140+}
141+ 
142+// 定位 ubsectl 的 Bash 补全脚本: <build>/scripts/cli_commands.sh
143+// 基于测试二进制 /proc/self/exe 推导构建目录(二进制位于 <build>/bin/ 下)
144+std::string LocateCliCompletionScript()
145+{
146+ char exePath[PATH_MAX];
147+ ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
148+ if (len <= 0) {
149+ return "";
150+ }
151+ exePath[len] = '\0';
152+ std::string exe(exePath); // <build>/bin/<binary>
153+ size_t binSlash = exe.find_last_of('/');
154+ if (binSlash == std::string::npos) {
155+ return "";
156+ }
157+ std::string buildDir = exe.substr(0, binSlash); // <build>/bin
158+ size_t buildSlash = buildDir.find_last_of('/');
159+ if (buildSlash == std::string::npos) {
160+ return "";
161+ }
162+ buildDir = buildDir.substr(0, buildSlash); // <build>
163+ return buildDir + "/scripts/cli_commands.sh";
164+}
165+ 
166+// 执行一段 bash 脚本并返回 stdout+stderr(不经过 ItCliInvoker,避免 CLI 超时/env 前缀干扰)
167+std::string RunBashScript(const std::string& script)
168+{
169+ std::string cmd = "bash -c '" + script + "' 2>&1";
170+ FILE* pipe = popen(cmd.c_str(), "r");
171+ if (pipe == nullptr) {
172+ IT_LOG_ERROR << "popen failed for bash script: " << cmd;
173+ return "";
174+ }
175+ std::ostringstream oss;
176+ std::array<char, 4096> buffer{};
177+ while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
178+ oss << buffer.data();
179+ }
180+ pclose(pipe);
181+ return oss.str();
182+}
183+ 
184+// 从输出中提取 "TAG=" 到行尾的内容
185+std::string ExtractTagValue(const std::string& output, const std::string& tag)
186+{
187+ size_t pos = output.find(tag + "=");
188+ if (pos == std::string::npos) {
189+ return "";
190+ }
191+ pos += tag.size() + 1;
192+ size_t eol = output.find('\n', pos);
193+ return output.substr(pos, eol == std::string::npos ? std::string::npos : eol - pos);
194+}
195+ 
196+// 判断空格分隔的候选词文本是否包含指定单词
197+bool ContainsWord(const std::string& text, const std::string& word)
198+{
199+ std::istringstream iss(text);
200+ std::string token;
201+ while (iss >> token) {
202+ if (token == word) {
203+ return true;
204+ }
205+ }
206+ return false;
207+}
208+ 
123} // namespace209} // namespace
124 210 
125// ==================== P0 用例 ====================211// ==================== P0 用例 ====================
@@ -631,4 +717,118 @@ void RunP1CliTopoCpuLinkOneNodeOnline01(ubse::it::infra::ItCluster& cluster)
631 EXPECT_EQ(topoLinks.size(), 2);717 EXPECT_EQ(topoLinks.size(), 2);
632 EXPECT_FALSE(topoLinks[0].linkId == "-" || topoLinks[1].linkId == "-") << "1-2 link should be up";718 EXPECT_FALSE(topoLinks[0].linkId == "-" || topoLinks[1].linkId == "-") << "1-2 link should be up";
633}719}
634-} // namespace ubse::it::tests::topo720+ 
721+// P0-CliHelpInfo-Ok-01: ubsectl -h / --help 输出全量已注册指令和参数信息,且 -h 与 --help 内容一致
722+void RunP0CliHelpInfoOk01(ubse::it::infra::ItCluster& cluster)
723+{
724+ auto& cli = cluster.GetCliInvoker("1");
725+ 
726+ // 1. ubsectl -h:指令下发成功,输出全量已注册指令和参数信息
727+ std::string shortHelp = cli.ExecCli("-h");
728+ EXPECT_FALSE(shortHelp.empty()) << "ubsectl -h should produce output";
729+ EXPECT_EQ(shortHelp.find("ERROR:"), std::string::npos) << "ubsectl -h should not return error";
730+ 
731+ // 帮助信息应包含已注册命令的 Usage 描述(全量指令)
732+ EXPECT_NE(shortHelp.find("Usage: ubsectl"), std::string::npos)
733+ << "help should contain 'Usage: ubsectl' for registered commands";
734+ EXPECT_GT(CountOccurrences(shortHelp, "Usage: ubsectl"), 1) << "help should list all registered commands";
735+ 
736+ // 参数信息:每个命令应包含 OPTIONS 及选项描述
737+ EXPECT_NE(shortHelp.find("OPTIONS:"), std::string::npos) << "help should list options for registered commands";
738+ EXPECT_NE(shortHelp.find("--"), std::string::npos) << "help should show long option descriptions";
739+ 
740+ // 2. ubsectl --help:指令下发成功,内容与 -h 完全一致
741+ std::string longHelp = cli.ExecCli("--help");
742+ EXPECT_FALSE(longHelp.empty()) << "ubsectl --help should produce output";
743+ EXPECT_EQ(longHelp.find("ERROR:"), std::string::npos) << "ubsectl --help should not return error";
744+ EXPECT_EQ(shortHelp, longHelp) << "ubsectl -h and --help should print identical help content";
745+ 
746+ IT_LOG_INFO << "P0-CliHelpInfo-Ok-01 passed";
747+}
748+ 
749+// P0-CliCompletion-Ok-01: ubsectl 命令自动补齐功能(Bash Tab 补全候选)
750+// 验证步骤:
751+// 1. ubsec + Tab -> 补全为 ubsectl(bash 命令名补全)
752+// 2. ubsectl dis + Tab -> ubsectl display
753+// 3. ubsectl display m + Tab -> ubsectl display memory
754+// ubsectl display t + Tab -> ubsectl display topo
755+// 4. ubsectl display memory --t + Tab -> ubsectl display memory --type
756+// 5. ubsectl display memory --n + Tab -> ubsectl display memory --name
757+// 6. ubsectl display memory --b + Tab -> ubsectl display memory --borrow-type
758+void RunP0CliCompletionOk01(ubse::it::infra::ItCluster& cluster)
759+{
760+ (void)cluster; // 补全用例不依赖集群节点,仅验证 CLI 自动补齐功能
761+ // 1. 定位补全脚本:<build>/scripts/cli_commands.sh
762+ std::string scriptPath = LocateCliCompletionScript();
763+ ASSERT_FALSE(scriptPath.empty()) << "failed to locate cli_commands.sh via /proc/self/exe";
764+ 
765+ // 由补全脚本路径推导 ubsectl 所在目录 <build>/bin,供步骤1的命令名补全使用
766+ const std::string suffix = "/scripts/cli_commands.sh";
767+ std::string binDir;
768+ if (scriptPath.size() >= suffix.size() &&
769+ scriptPath.compare(scriptPath.size() - suffix.size(), suffix.size(), suffix) == 0) {
770+ binDir = scriptPath.substr(0, scriptPath.size() - suffix.size()) + "/bin";
771+ }
772+ ASSERT_FALSE(binDir.empty()) << "failed to derive <build>/bin from completion script path";
773+ 
774+ // 2. 在 bash 中 source 补全脚本,模拟 Tab 补全(设置 COMP_WORDS/COMP_CWORD 后调用补全函数),收集候选词
775+ std::string script =
776+ "source \"" + scriptPath +
777+ "\"\n"
778+ "export PATH=\"" +
779+ binDir +
780+ ":$PATH\"\n"
781+ "echo \"REGISTER=$(complete -p ubsectl)\"\n"
782+ // 步骤1:ubsec + Tab,bash 命令名补全应得到 ubsectl
783+ // 注意:脚本由 RunBashScript 以 bash -c '<script>' 执行,脚本内禁止出现单引号,
784+ // 故用 xargs 将候选命令拼接为单行(不能用 tr '\n' ' ')。
785+ // bin 目录含 ubse_it_daemon/ubse_it_node_launcher 等,前缀必须用 ubsec 才能唯一补全为 ubsectl
786+ "echo \"S1_CMD=$(compgen -c ubsec | xargs)\"\n"
787+ // 步骤2:ubsectl dis + Tab,object 部分字符补齐为完整 object
788+ "COMP_WORDS=(ubsectl dis); COMP_CWORD=1; _ubse_commond_completion; echo \"S2_DIS=${COMPREPLY[*]}\"\n"
789+ // 步骤3:ubsectl display m + Tab / display t + Tab,action 部分字符补齐为完整 action
790+ "COMP_WORDS=(ubsectl display m); COMP_CWORD=2; _ubse_commond_completion; echo \"S3_M=${COMPREPLY[*]}\"\n"
791+ "COMP_WORDS=(ubsectl display t); COMP_CWORD=2; _ubse_commond_completion; echo \"S3_T=${COMPREPLY[*]}\"\n"
792+ // 步骤4:ubsectl display memory --t + Tab,长名参数补齐为 --type
793+ "COMP_WORDS=(ubsectl display memory --t); COMP_CWORD=3; _ubse_commond_completion; echo "
794+ "\"S4_T=${COMPREPLY[*]}\"\n"
795+ // 步骤5:ubsectl display memory --n + Tab,长名参数补齐为 --name
796+ "COMP_WORDS=(ubsectl display memory --n); COMP_CWORD=3; _ubse_commond_completion; echo "
797+ "\"S5_N=${COMPREPLY[*]}\"\n"
798+ // 步骤6:ubsectl display memory --b + Tab,长名参数补齐为 --borrow-type
799+ "COMP_WORDS=(ubsectl display memory --b); COMP_CWORD=3; _ubse_commond_completion; echo "
800+ "\"S6_B=${COMPREPLY[*]}\"\n";
801+ std::string output = RunBashScript(script);
802+ ASSERT_FALSE(output.empty()) << "bash completion script produced no output";
803+ 
804+ // 前置:补全函数已注册到 ubsectl(自动补齐已启用)
805+ std::string registered = ExtractTagValue(output, "REGISTER");
806+ EXPECT_NE(registered.find("_ubse_commond_completion"), std::string::npos)
807+ << "complete -p ubsectl should register _ubse_commond_completion, got: " << registered;
808+ EXPECT_NE(registered.find("ubsectl"), std::string::npos)
809+ << "completion should be bound to ubsectl, got: " << registered;
810+ 
811+ // 步骤1:ubsec + Tab 应补全出 ubsectl
812+ std::string s1Cmd = ExtractTagValue(output, "S1_CMD");
813+ EXPECT_TRUE(ContainsWord(s1Cmd, "ubsectl")) << "step1: 'ubsec'+Tab should complete to 'ubsectl', got: " << s1Cmd;
814+ 
815+ // 步骤2:ubsectl dis + Tab 应补齐为 ubsectl display
816+ EXPECT_EQ(ExtractTagValue(output, "S2_DIS"), "display") << "step2: prefix 'dis' should complete to 'display'";
817+ 
818+ // 步骤3:ubsectl display m + Tab 应补齐为 memory;display t + Tab 应补齐为 topo
819+ EXPECT_EQ(ExtractTagValue(output, "S3_M"), "memory") << "step3: prefix 'm' should complete to 'memory'";
820+ EXPECT_EQ(ExtractTagValue(output, "S3_T"), "topo") << "step3: prefix 't' should complete to 'topo'";
821+ 
822+ // 步骤4:ubsectl display memory --t + Tab 应补齐为 --type
823+ EXPECT_EQ(ExtractTagValue(output, "S4_T"), "--type") << "step4: prefix '--t' should complete to '--type'";
824+ 
825+ // 步骤5:ubsectl display memory --n + Tab 应补齐为 --name
826+ EXPECT_EQ(ExtractTagValue(output, "S5_N"), "--name") << "step5: prefix '--n' should complete to '--name'";
827+ 
828+ // 步骤6:ubsectl display memory --b + Tab 应补齐为 --borrow-type
829+ EXPECT_EQ(ExtractTagValue(output, "S6_B"), "--borrow-type")
830+ << "step6: prefix '--b' should complete to '--borrow-type'";
831+ 
832+ IT_LOG_INFO << "P0-CliCompletion-Ok-01 passed";
833+}
834+} // namespace ubse::it::tests::topo
@@ -68,6 +68,12 @@ void RunP0CliTopoCpuLinkChange01(ubse::it::infra::ItCluster& cluster);
68 68 
69// P1-CliTopoCpu-LinkOneNodeOnline-01: 链路一端节点在线, 另一侧节点离线69// P1-CliTopoCpu-LinkOneNodeOnline-01: 链路一端节点在线, 另一侧节点离线
70void RunP1CliTopoCpuLinkOneNodeOnline01(ubse::it::infra::ItCluster& cluster);70void RunP1CliTopoCpuLinkOneNodeOnline01(ubse::it::infra::ItCluster& cluster);
71+ 
72+// P0-CliHelpInfo-Ok-01: ubsectl -h / --help 输出全量已注册指令和参数信息, 且内容一致
73+void RunP0CliHelpInfoOk01(ubse::it::infra::ItCluster& cluster);
74+ 
75+// P0-CliCompletion-Ok-01: ubsectl 命令自动补齐功能 (Bash Tab 补全候选)
76+void RunP0CliCompletionOk01(ubse::it::infra::ItCluster& cluster);
71} // namespace ubse::it::tests::topo77} // namespace ubse::it::tests::topo
72 78 
73-#endif // IT_TOPO_CASES_H79+#endif // IT_TOPO_CASES_H