已合并
【UBSE】补充HA和CLI相关IT用例 #1468
weaver创建于 8 天前
【UBSE】补充HA和CLI相关IT用例 #1468
已合并
共 18 个文件变更+770-9
| @@ -189,6 +189,7 @@ set(UBSE_IT_SCENARIOS | |||
| 189 | tongsuan_1d_single_node | 189 | tongsuan_1d_single_node |
| 190 | tongsuan_1d_two_nodes | 190 | tongsuan_1d_two_nodes |
| 191 | tongsuan_1d_two_nodes_election_candidate_false | 191 | tongsuan_1d_two_nodes_election_candidate_false |
| 192 | + tongsuan_1d_two_nodes_election_disabled | ||
| 192 | tongsuan_1d_two_nodes_ub_feature_fault | 193 | tongsuan_1d_two_nodes_ub_feature_fault |
| 193 | tongsuan_1d_two_nodes_runtime_fault | 194 | tongsuan_1d_two_nodes_runtime_fault |
| 194 | tongsuan_1d_two_nodes_lender_false | 195 | tongsuan_1d_two_nodes_lender_false |
| @@ -201,6 +202,7 @@ set(UBSE_IT_SCENARIOS | |||
| 201 | tongsuan_1d_four_nodes | 202 | tongsuan_1d_four_nodes |
| 202 | tongsuan_1d_four_nodes_two_group | 203 | tongsuan_1d_four_nodes_two_group |
| 203 | tongsuan_1d_four_nodes_boundary_hostname | 204 | tongsuan_1d_four_nodes_boundary_hostname |
| 205 | + tongsuan_1d_four_nodes_candidate_nodes | ||
| 204 | zhisuan_npu_single_node | 206 | 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::infra | 340 | +} // 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 | + | ||
| 13 | + | ||
| 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 | + | ||
| 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) | |||
| 335 | TEST_F(Tongsuan1dFullMeshSingleNodeScenario, BmcFaultSingleNode) | 347 | TEST_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: 查询节点借入汇总 |
| 852 | TEST_F(Tongsuan1dFullMeshTwoNodesScenario, P0CliNodeBorrowOk01) | 859 | TEST_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) | |||
| 1145 | TEST_F(Tongsuan1dFullMeshTwoNodesScenario, ITSeiDegradeLifecycle) | 1160 | TEST_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 | + | ||
| 13 | + | ||
| 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 | + | ||
| 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 | + | ||
| 71 | bool CsvContainsIp(const char* csv, in_addr_t ip) | 82 | bool 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 | 21 | ||
| 22 | 22 | ||
| 23 | + | ||
| 23 | 24 | ||
| 24 | 25 | ||
| 25 | namespace ubse::it::tests::election { | 26 | namespace 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 | } // namespace | 161 | } // 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 | // 收敛后主节点应为候选节点、备节点为最小节点,并验证主节点周期性心跳。 |
| 204 | void RunTwoNodeElectionCandidateFalseTest(ubse::it::infra::ItCluster& cluster) | 253 | void 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代理 |
| 259 | void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster) | 362 | void 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::election | 587 | } // namespace ubse::it::tests::election |
| @@ -40,16 +40,32 @@ void RunSingleNodeElectionTest(ubse::it::infra::ItCluster& cluster); | |||
| 40 | // 双节点选举测试:验证集群收敛为1主+1备(默认候选配置) | 40 | // 双节点选举测试:验证集群收敛为1主+1备(默认候选配置) |
| 41 | void RunTwoNodeElectionTest(ubse::it::infra::ItCluster& cluster); | 41 | void 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 | // 收敛后主节点应为候选节点、备节点为最小节点,并验证主节点周期性心跳。 |
| 45 | void RunTwoNodeElectionCandidateFalseTest(ubse::it::infra::ItCluster& cluster); | 49 | void 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代理 |
| 48 | void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster); | 56 | void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster); |
| 49 | 57 | ||
| 50 | // 四节点主节点重启测试:收敛后重启主节点,备节点应接管成为新主,集群最终重新收敛 | 58 | // 四节点主节点重启测试:收敛后重启主节点,备节点应接管成为新主,集群最终重新收敛 |
| 51 | void RunFourNodeMasterRestartTest(ubse::it::infra::ItCluster& cluster); | 59 | void 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::election | 69 | } // namespace ubse::it::tests::election |
| 54 | 70 | ||
| 55 | -#endif // IT_ELECTION_CASES_H | 71 | +#endif // IT_ELECTION_CASES_H |
| @@ -19,6 +19,7 @@ | |||
| 19 | 19 | ||
| 20 | 20 | ||
| 21 | 21 | ||
| 22 | + | ||
| 22 | 23 | ||
| 23 | 24 | ||
| 24 | 25 | ||
| @@ -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 | } // namespace | 93 | } // 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节点借入汇总查询测试 |
| 99 | void RunP0CliNodeBorrowOk01(ubse::it::infra::ItCluster& cluster) | 177 | void 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_borrow | 4747 | +} // namespace ubse::it::tests::mem_borrow |
| @@ -24,6 +24,9 @@ namespace ubse::it::tests::mem_borrow { | |||
| 24 | // CLI查询节点内存状态测试:调用check memory命令,验证返回包含两个节点的状态信息 | 24 | // CLI查询节点内存状态测试:调用check memory命令,验证返回包含两个节点的状态信息 |
| 25 | void RunP0CliCheckMemOk01(ubse::it::infra::ItCluster& cluster); | 25 | void 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节点借入汇总查询测试 |
| 28 | void RunP0CliNodeBorrowOk01(ubse::it::infra::ItCluster& cluster); | 31 | void 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_borrow | 328 | } // namespace ubse::it::tests::mem_borrow |
| 326 | 329 | ||
| 327 | -#endif // IT_MEM_BORROW_CASES_H | 330 | +#endif // IT_MEM_BORROW_CASES_H |
| @@ -22,11 +22,18 @@ | |||
| 22 | 22 | ||
| 23 | 23 | ||
| 24 | 24 | ||
| 25 | + | ||
| 25 | 26 | ||
| 27 | + | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + | ||
| 26 | 31 | ||
| 27 | 32 | ||
| 28 | 33 | ||
| 29 | 34 | ||
| 35 | + | ||
| 36 | + | ||
| 30 | 37 | ||
| 31 | namespace ubse::it::tests::topo { | 38 | namespace ubse::it::tests::topo { |
| 32 | namespace util = ubse::it::infra::util; | 39 | namespace 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 | } // namespace | 209 | } // 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::topo | 720 | + |
| 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: 链路一端节点在线, 另一侧节点离线 |
| 70 | void RunP1CliTopoCpuLinkOneNodeOnline01(ubse::it::infra::ItCluster& cluster); | 70 | void 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::topo | 77 | } // namespace ubse::it::tests::topo |
| 72 | 78 | ||
| 73 | -#endif // IT_TOPO_CASES_H | 79 | +#endif // IT_TOPO_CASES_H |