已合并
【UBSE】补充IT用例 #1511
weaver创建于 12 天前
【UBSE】补充IT用例 #1511
已合并
共 12 个文件变更+863-5
| @@ -40,7 +40,7 @@ struct PairHash { | |||
| 40 | using UbseClientIpcHandlerMap = std::unordered_map<std::pair<uint16_t, uint16_t>, UbseClientIpcHandler, PairHash>; | 40 | using UbseClientIpcHandlerMap = std::unordered_map<std::pair<uint16_t, uint16_t>, UbseClientIpcHandler, PairHash>; |
| 41 | std::mutex clientIpcHandlerMutex; // mutex锁 | 41 | std::mutex clientIpcHandlerMutex; // mutex锁 |
| 42 | static UbseClientIpcHandlerMap clientIpcHandlerMap{}; | 42 | static UbseClientIpcHandlerMap clientIpcHandlerMap{}; |
| 43 | -static std::string ubseSocketPath = ubse::common::def::UBSE_UDS_SOCKET_PATH; | 43 | +thread_local std::string ubseSocketPath = ubse::common::def::UBSE_UDS_SOCKET_PATH; |
| 44 | constexpr uint32_t MAX_RESPONSE_SIZE = 10 * 1024 * 1024; | 44 | constexpr uint32_t MAX_RESPONSE_SIZE = 10 * 1024 * 1024; |
| 45 | 45 | ||
| 46 | static uint32_t CopyResponseBody(const UbseResponseMessage& src, ubse_api_buffer_t* dest) | 46 | static uint32_t CopyResponseBody(const UbseResponseMessage& src, ubse_api_buffer_t* dest) |
| @@ -248,6 +248,12 @@ TEST_F(Tongsuan1dFullMeshFourNodesScenario, ExistingMasterUniqueStandby) | |||
| 248 | ubse::it::tests::election::RunFourNodeExistingMasterUniqueStandbyTest(Cluster()); | 248 | ubse::it::tests::election::RunFourNodeExistingMasterUniqueStandbyTest(Cluster()); |
| 249 | } | 249 | } |
| 250 | 250 | ||
| 251 | +// 新集群启动平滑:四节点启动收敛后,校验各节点 local:ready 与 cluster:working 状态日志。 | ||
| 252 | +TEST_F(Tongsuan1dFullMeshFourNodesScenario, StartupSmooth) | ||
| 253 | +{ | ||
| 254 | + ubse::it::tests::election::RunFourNodeStartupSmoothTest(Cluster()); | ||
| 255 | +} | ||
| 256 | + | ||
| 251 | // ==================================================================== | 257 | // ==================================================================== |
| 252 | // P1 测试 — Fault Log 校验 | 258 | // P1 测试 — Fault Log 校验 |
| 253 | // ==================================================================== | 259 | // ==================================================================== |
| @@ -1,3 +1,3 @@ | |||
| 1 | add_it_scene(single_node | 1 | add_it_scene(single_node |
| 2 | - LINK_LIBS it_client_cases it_smoke_cases it_topo_cases it_election_cases it_urma_qos_cases it_fault_cases it_mem_borrow_cases | 2 | + LINK_LIBS it_client_cases it_smoke_cases it_topo_cases it_election_cases it_urma_qos_cases it_fault_cases it_mem_borrow_cases it_log_cases |
| 3 | -) | 3 | +) |
| @@ -14,6 +14,7 @@ | |||
| 14 | 14 | ||
| 15 | 15 | ||
| 16 | 16 | ||
| 17 | + | ||
| 17 | 18 | ||
| 18 | 19 | ||
| 19 | 20 | ||
| @@ -347,4 +348,33 @@ TEST_F(Tongsuan1dFullMeshSingleNodeScenario, ElectionConvergence) | |||
| 347 | TEST_F(Tongsuan1dFullMeshSingleNodeScenario, BmcFaultSingleNode) | 348 | TEST_F(Tongsuan1dFullMeshSingleNodeScenario, BmcFaultSingleNode) |
| 348 | { | 349 | { |
| 349 | ubse::it::tests::fault::RunBmcFaultSingleNodeTest(Cluster(), "1"); | 350 | ubse::it::tests::fault::RunBmcFaultSingleNodeTest(Cluster(), "1"); |
| 351 | +} | ||
| 352 | + | ||
| 353 | +// 日志绕接测试:修改 log.max.fileSize=2M 并重启生效,追加数据触发绕接, | ||
| 354 | +// 验证有新的绕接压缩文件生成且绕接后日志正常打印,最后恢复配置 | ||
| 355 | +TEST_F(Tongsuan1dFullMeshSingleNodeScenario, LogRotationTest) | ||
| 356 | +{ | ||
| 357 | + ubse::it::tests::log::RunLogRotationTest(Cluster()); | ||
| 358 | +} | ||
| 359 | + | ||
| 360 | +// 日志绕接文件数量上限测试:设置 log.max.fileSize=2M、log.fileNums=3 并重启生效, | ||
| 361 | +// 连续触发 4 次绕接,验证绕接文件仅保留 3 个且最早生成的日志包被删除替换,最后恢复配置 | ||
| 362 | +TEST_F(Tongsuan1dFullMeshSingleNodeScenario, LogRotationFileNumTest) | ||
| 363 | +{ | ||
| 364 | + ubse::it::tests::log::RunLogRotationFileNumTest(Cluster()); | ||
| 365 | +} | ||
| 366 | + | ||
| 367 | +// 日志格式校验测试:验证默认目录下存在日志,日志文件权限为 640, | ||
| 368 | +// 获取最近 50h 日志并校验格式:标准格式(含进程/线程/traceid/函数信息)、 | ||
| 369 | +// 第三方组件格式(无函数名),日志等级必须在 WARN/DEBUG/INFO/ERROR/CRIT 内 | ||
| 370 | +TEST_F(Tongsuan1dFullMeshSingleNodeScenario, LogFormatTest) | ||
| 371 | +{ | ||
| 372 | + ubse::it::tests::log::RunLogFormatTest(Cluster()); | ||
| 373 | +} | ||
| 374 | + | ||
| 375 | +// 日志回调注册测试:注册自定义日志处理函数,验证注册成功日志及自定义处理函数 | ||
| 376 | +// 输出的日志均符合自定义格式 [simple_log_handler] {LEVEL}:{message}(等级 INFO|WARN|ERROR|CRIT) | ||
| 377 | +TEST_F(Tongsuan1dFullMeshSingleNodeScenario, LogCallbackTest) | ||
| 378 | +{ | ||
| 379 | + ubse::it::tests::log::RunLogCallbackTest(Cluster()); | ||
| 350 | } | 380 | } |
| @@ -1,3 +1,3 @@ | |||
| 1 | add_it_scene(two_nodes | 1 | add_it_scene(two_nodes |
| 2 | LINK_LIBS it_election_cases it_mem_borrow_cases it_sei_degrade_cases it_topo_cases | 2 | LINK_LIBS it_election_cases it_mem_borrow_cases it_sei_degrade_cases it_topo_cases |
| 3 | -) | 3 | +) |
| @@ -1088,6 +1088,20 @@ TEST_F(Tongsuan1dFullMeshTwoNodesScenario, MasterPeriodicHeartbeat) | |||
| 1088 | ubse::it::tests::election::RunTwoNodeMasterPeriodicHeartbeatTest(Cluster()); | 1088 | ubse::it::tests::election::RunTwoNodeMasterPeriodicHeartbeatTest(Cluster()); |
| 1089 | } | 1089 | } |
| 1090 | 1090 | ||
| 1091 | +// 周期对账定时触发:双节点长时间运行(周期对账 5 分钟一次), | ||
| 1092 | +// 验证主节点日志中出现周期性的 cluster smoothing 与 cluster working 记录。 | ||
| 1093 | +TEST_F(Tongsuan1dFullMeshTwoNodesScenario, PeriodicLedger) | ||
| 1094 | +{ | ||
| 1095 | + // ubse::it::tests::election::RunTwoNodePeriodicLedgerTest(Cluster()); | ||
| 1096 | +} | ||
| 1097 | + | ||
| 1098 | +// 节点信息上报汇总:验证节点内存等信息采集持续进行。 | ||
| 1099 | +// 获取 "ubse node last 1min report summary" 首个时间戳,等待 70s 后取最新时间戳,两者应不相等。 | ||
| 1100 | +TEST_F(Tongsuan1dFullMeshTwoNodesScenario, NodeReportSummary) | ||
| 1101 | +{ | ||
| 1102 | + ubse::it::tests::election::RunTwoNodeNodeReportSummaryTest(Cluster()); | ||
| 1103 | +} | ||
| 1104 | + | ||
| 1091 | // ==================================================================== | 1105 | // ==================================================================== |
| 1092 | // P1 测试 — 拓扑校验 | 1106 | // P1 测试 — 拓扑校验 |
| 1093 | // ==================================================================== | 1107 | // ==================================================================== |
| @@ -10,3 +10,4 @@ add_subdirectory(npu) | |||
| 10 | add_subdirectory(sei_degrade) | 10 | add_subdirectory(sei_degrade) |
| 11 | add_subdirectory(urma) | 11 | add_subdirectory(urma) |
| 12 | add_subdirectory(topo) | 12 | add_subdirectory(topo) |
| 13 | +add_subdirectory(log) | ||
| @@ -91,6 +91,34 @@ size_t CountLogLines(const std::string& path, const std::string& substring) | |||
| 91 | return count; | 91 | return count; |
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | +// 返回日志文件中首个(first=true)或最后一个(first=false)包含指定子串的行的时间戳文本。 | ||
| 95 | +// 日志行首格式为 "[YYYY-MM-DD HH:MM:SS.mmm+HH:MM]",取首个 '[' 到首个 ']'(含)之间的内容; | ||
| 96 | +// 无匹配行返回空串。 | ||
| 97 | +std::string MatchTimeText(const std::string& path, const std::string& substring, bool first) | ||
| 98 | +{ | ||
| 99 | + std::ifstream ifs(path); | ||
| 100 | + if (!ifs.is_open()) { | ||
| 101 | + return ""; | ||
| 102 | + } | ||
| 103 | + std::string result; | ||
| 104 | + std::string line; | ||
| 105 | + while (std::getline(ifs, line)) { | ||
| 106 | + if (line.find(substring) == std::string::npos) { | ||
| 107 | + continue; | ||
| 108 | + } | ||
| 109 | + auto open = line.find('['); | ||
| 110 | + auto close = line.find(']'); | ||
| 111 | + std::string ts = (open == std::string::npos || close == std::string::npos || close < open) ? | ||
| 112 | + line : | ||
| 113 | + line.substr(open, close - open + 1); | ||
| 114 | + if (first) { | ||
| 115 | + return ts; | ||
| 116 | + } | ||
| 117 | + result = ts; | ||
| 118 | + } | ||
| 119 | + return result; | ||
| 120 | +} | ||
| 121 | + | ||
| 94 | // 通过源码日志校验主节点是否周期性向对端节点发送心跳。 | 122 | // 通过源码日志校验主节点是否周期性向对端节点发送心跳。 |
| 95 | // 读取主节点配置的心跳周期,等待 3 个完整周期 + 余量后统计 [ELECTION] ProcTimer | 123 | // 读取主节点配置的心跳周期,等待 3 个完整周期 + 余量后统计 [ELECTION] ProcTimer |
| 96 | // MASTER send pkt 日志,验证周期性(>=3 次)且确实发往对端节点。 | 124 | // MASTER send pkt 日志,验证周期性(>=3 次)且确实发往对端节点。 |
| @@ -358,6 +386,80 @@ void RunTwoNodeBothElectionDisabledTest(ubse::it::infra::ItCluster& cluster) | |||
| 358 | } | 386 | } |
| 359 | } | 387 | } |
| 360 | 388 | ||
| 389 | +// 双节点周期对账(定时触发)测试:验证主节点周期对账定时器(UbseNodeLedger,5 分钟一次) | ||
| 390 | +// 会周期性触发节点的 cluster 状态平滑(WORKING->SMOOTHING)与恢复(SMOOTHING->WORKING)。 | ||
| 391 | +// 状态日志仅由主节点输出,故统一在主节点 ubse.log 上按 nodeId 校验两个节点: | ||
| 392 | +// - 周期 smoothing:初始上线为 INIT(0)->SMOOTHING(1)(current state=0, update state=1), | ||
| 393 | +// 只有周期对账才会 WORKING(2)->SMOOTHING(1)(current state=2, update state=1),该记录唯一标识周期触发。 | ||
| 394 | +// - working:初始 1 次 + 每个周期 1 次,累计 >=2 即证明定时再度触发。 | ||
| 395 | +void RunTwoNodePeriodicLedgerTest(ubse::it::infra::ItCluster& cluster) | ||
| 396 | +{ | ||
| 397 | + const auto& nodeIds = cluster.GetNodeIds(); | ||
| 398 | + ASSERT_EQ(nodeIds.size(), 2U) << "本用例要求双节点集群,实际=" << nodeIds.size(); | ||
| 399 | + | ||
| 400 | + // 前置:集群收敛为1主+1备 | ||
| 401 | + auto roles = CollectElectionRoles(cluster); | ||
| 402 | + ASSERT_EQ(roles.masterCount, 1U) << "集群应存在唯一主节点"; | ||
| 403 | + ASSERT_EQ(roles.standbyCount, 1U) << "集群应存在唯一备节点"; | ||
| 404 | + const std::string masterId = roles.masterNodeId; | ||
| 405 | + const std::string masterLog = cluster.GetNode(masterId).GetLogFilePath(); | ||
| 406 | + | ||
| 407 | + // 周期对账间隔 5 分钟(UBSE_NODE_LEDGER_INTERVAL=300s);等待 1 个周期 + 余量。 | ||
| 408 | + constexpr uint32_t kLedgerIntervalMs = 300 * 1000; // 5min | ||
| 409 | + constexpr uint32_t kWaitTimeoutMs = kLedgerIntervalMs + 120 * 1000; // 7min 余量 | ||
| 410 | + | ||
| 411 | + for (const auto& nodeId : nodeIds) { | ||
| 412 | + const std::string periodicSmoothing = | ||
| 413 | + "nodeId=" + nodeId + " update cluster state, current state=2, update state=1"; | ||
| 414 | + const std::string workingLog = "nodeId=" + nodeId + " update cluster state, current state=1, update state=2"; | ||
| 415 | + | ||
| 416 | + // 周期 smoothing:WORKING(2)->SMOOTHING(1),仅周期对账产生 | ||
| 417 | + auto retSmoothing = ubse::it::infra::ItWaitHelper::WaitForCondition( | ||
| 418 | + [&]() -> bool { return CountLogLines(masterLog, periodicSmoothing) >= 1; }, kWaitTimeoutMs); | ||
| 419 | + EXPECT_IT_OK(retSmoothing) << "master 应记录 node " << nodeId << " 的周期 smoothing (2->1)"; | ||
| 420 | + | ||
| 421 | + // working:初始 1 次 + 周期至少 1 次 | ||
| 422 | + auto retWorking = ubse::it::infra::ItWaitHelper::WaitForCondition( | ||
| 423 | + [&]() -> bool { return CountLogLines(masterLog, workingLog) >= 2; }, kWaitTimeoutMs); | ||
| 424 | + EXPECT_IT_OK(retWorking) << "master 应记录 node " << nodeId << " 的周期 working (累计>=2)"; | ||
| 425 | + } | ||
| 426 | +} | ||
| 427 | + | ||
| 428 | +// 双节点节点信息上报汇总测试:验证节点内存等信息采集持续进行。 | ||
| 429 | +// 主节点每 60s(UBSE_REPORT_LOG_INTERVAL)打印一次 "ubse node last 1min report summary" 汇总日志。 | ||
| 430 | +// 取首个汇总日志时间戳,等待 70s(大于 1 个上报周期)后取最新汇总日志时间戳, | ||
| 431 | +// 两者应不相等,否则说明采集上报已停止。 | ||
| 432 | +void RunTwoNodeNodeReportSummaryTest(ubse::it::infra::ItCluster& cluster) | ||
| 433 | +{ | ||
| 434 | + const auto& nodeIds = cluster.GetNodeIds(); | ||
| 435 | + ASSERT_EQ(nodeIds.size(), 2U) << "本用例要求双节点集群,实际=" << nodeIds.size(); | ||
| 436 | + | ||
| 437 | + auto roles = CollectElectionRoles(cluster); | ||
| 438 | + ASSERT_EQ(roles.masterCount, 1U) << "集群应存在唯一主节点"; | ||
| 439 | + ASSERT_EQ(roles.standbyCount, 1U) << "集群应存在唯一备节点"; | ||
| 440 | + const std::string masterId = roles.masterNodeId; | ||
| 441 | + const std::string masterLog = cluster.GetNode(masterId).GetLogFilePath(); | ||
| 442 | + | ||
| 443 | + const std::string summaryMark = "ubse node last 1min report summary"; | ||
| 444 | + | ||
| 445 | + // 等待首个上报汇总日志出现(上报汇总周期 60s,主节点上线后首次打印不超过 60s) | ||
| 446 | + auto retFirst = ubse::it::infra::ItWaitHelper::WaitForCondition( | ||
| 447 | + [&]() -> bool { return CountLogLines(masterLog, summaryMark) >= 1; }, 90000); | ||
| 448 | + ASSERT_IT_OK(retFirst) << "master 未在预期时间内打印上报汇总日志"; | ||
| 449 | + | ||
| 450 | + // 初始打印时间戳 | ||
| 451 | + const std::string initialTs = MatchTimeText(masterLog, summaryMark, true); | ||
| 452 | + ASSERT_FALSE(initialTs.empty()) << "应能解析首个上报汇总日志的时间戳"; | ||
| 453 | + | ||
| 454 | + // 等待 70s(大于 1 个上报周期),确保采集上报再次发生 | ||
| 455 | + std::this_thread::sleep_for(std::chrono::seconds(70)); | ||
| 456 | + | ||
| 457 | + // 最新时间戳 | ||
| 458 | + const std::string latestTs = MatchTimeText(masterLog, summaryMark, false); | ||
| 459 | + ASSERT_FALSE(latestTs.empty()) << "应能解析最新上报汇总日志的时间戳"; | ||
| 460 | + EXPECT_NE(latestTs, initialTs) << "等待 70s 后应有新的采集上报,汇总日志时间戳应发生变化"; | ||
| 461 | +} | ||
| 462 | + | ||
| 361 | // 四节点选举测试:验证集群收敛为1主+1备+2代理 | 463 | // 四节点选举测试:验证集群收敛为1主+1备+2代理 |
| 362 | void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster) | 464 | void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster) |
| 363 | { | 465 | { |
| @@ -584,4 +686,44 @@ void RunFourNodeCandidateNodesOnlyMasterTest(ubse::it::infra::ItCluster& cluster | |||
| 584 | EXPECT_IT_OK(ret4) << "candidate node 4 should become master and appoint a unique standby"; | 686 | EXPECT_IT_OK(ret4) << "candidate node 4 should become master and appoint a unique standby"; |
| 585 | } | 687 | } |
| 586 | 688 | ||
| 587 | -} // namespace ubse::it::tests::election | 689 | +// 四节点新集群启动平滑测试:验证新集群启动后各节点平滑上线。 |
| 690 | +// 步骤:① CLI 确认集群收敛为1主+1备+2agent; | ||
| 691 | +// ② 逐节点校验其自身日志记录 local 状态 READY(1)("local node update local state to 1"); | ||
| 692 | +// ③ 逐节点校验主节点日志记录 cluster 状态 WORKING(2) | ||
| 693 | +// ("nodeId=<nodeId> update cluster state, current state=1, update state=2")。 | ||
| 694 | +void RunFourNodeStartupSmoothTest(ubse::it::infra::ItCluster& cluster) | ||
| 695 | +{ | ||
| 696 | + const auto& nodeIds = cluster.GetNodeIds(); | ||
| 697 | + ASSERT_EQ(nodeIds.size(), 4U) << "本用例要求四节点集群,实际=" << nodeIds.size(); | ||
| 698 | + | ||
| 699 | + // ① CLI 确认集群收敛 | ||
| 700 | + std::string masterId; | ||
| 701 | + ASSERT_IT_OK(cluster.GetMasterNodeId(masterId)); | ||
| 702 | + auto roles = CollectElectionRoles(cluster); | ||
| 703 | + ASSERT_EQ(roles.masterCount, 1U) << "集群应存在唯一主节点"; | ||
| 704 | + ASSERT_EQ(roles.standbyCount, 1U) << "集群应存在唯一备节点"; | ||
| 705 | + ASSERT_EQ(roles.agentCount, 2U) << "集群应存在2个agent节点"; | ||
| 706 | + | ||
| 707 | + std::vector<ubse::it::infra::ItNodeInfo> nodeInfos; | ||
| 708 | + EXPECT_EQ(cluster.GetCliInvoker(masterId).QueryClusterInfo(nodeInfos), UBS_SUCCESS); | ||
| 709 | + EXPECT_EQ(nodeInfos.size(), 4U) << "display cluster 应显示全部4个节点"; | ||
| 710 | + | ||
| 711 | + // ②③ 逐节点校验 local: READY(1) 与 cluster: WORKING(2) | ||
| 712 | + for (const auto& nodeId : nodeIds) { | ||
| 713 | + // local: ready — 该节点自身日志 "local node update local state to 1" | ||
| 714 | + const std::string localReady = "local node update local state to 1"; | ||
| 715 | + auto retLocal = ubse::it::infra::ItWaitHelper::WaitForCondition( | ||
| 716 | + [&]() -> bool { return CountLogLines(cluster.GetNode(nodeId).GetLogFilePath(), localReady) > 0; }, 30000); | ||
| 717 | + EXPECT_IT_OK(retLocal) << "node " << nodeId << " local state should reach READY(1)"; | ||
| 718 | + | ||
| 719 | + // cluster: working — 主节点日志 "nodeId=<nodeId> update cluster state, current state=1, update state=2" | ||
| 720 | + const std::string clusterWorking = | ||
| 721 | + "nodeId=" + nodeId + " update cluster state, current state=1, update state=2"; | ||
| 722 | + auto retCluster = ubse::it::infra::ItWaitHelper::WaitForCondition( | ||
| 723 | + [&]() -> bool { return CountLogLines(cluster.GetNode(masterId).GetLogFilePath(), clusterWorking) > 0; }, | ||
| 724 | + 30000); | ||
| 725 | + EXPECT_IT_OK(retCluster) << "master should log cluster state WORKING(2) for node " << nodeId; | ||
| 726 | + } | ||
| 727 | +} | ||
| 728 | + | ||
| 729 | +} // namespace ubse::it::tests::election | ||
| @@ -52,6 +52,16 @@ void RunTwoNodeElectionCandidateFalseTest(ubse::it::infra::ItCluster& cluster); | |||
| 52 | // 预期两节点均停留在 init 角色,不升主、不升备、不升 agent。 | 52 | // 预期两节点均停留在 init 角色,不升主、不升备、不升 agent。 |
| 53 | void RunTwoNodeBothElectionDisabledTest(ubse::it::infra::ItCluster& cluster); | 53 | void RunTwoNodeBothElectionDisabledTest(ubse::it::infra::ItCluster& cluster); |
| 54 | 54 | ||
| 55 | +// 双节点周期对账(定时触发)测试:集群收敛后长时间运行(周期对账 5 分钟一次), | ||
| 56 | +// 验证主节点日志中出现周期性的 cluster smoothing(WORKING->SMOOTHING) 与 | ||
| 57 | +// cluster working(SMOOTHING->WORKING) 记录。 | ||
| 58 | +void RunTwoNodePeriodicLedgerTest(ubse::it::infra::ItCluster& cluster); | ||
| 59 | + | ||
| 60 | +// 双节点节点信息上报汇总测试:验证节点内存等信息采集持续进行。 | ||
| 61 | +// 取首个 "ubse node last 1min report summary" 日志时间戳,等待 70s 后取最新时间戳, | ||
| 62 | +// 两者应不相等。 | ||
| 63 | +void RunTwoNodeNodeReportSummaryTest(ubse::it::infra::ItCluster& cluster); | ||
| 64 | + | ||
| 55 | // 四节点选举测试:验证集群收敛为1主+1备+2代理 | 65 | // 四节点选举测试:验证集群收敛为1主+1备+2代理 |
| 56 | void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster); | 66 | void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster); |
| 57 | 67 | ||
| @@ -66,6 +76,10 @@ void RunFourNodeExistingMasterUniqueStandbyTest(ubse::it::infra::ItCluster& clus | |||
| 66 | // 配置 candidateNodes=3,4:非候选节点(1、2)始终不能升主;候选节点3、4可先后升主并选出唯一备。 | 76 | // 配置 candidateNodes=3,4:非候选节点(1、2)始终不能升主;候选节点3、4可先后升主并选出唯一备。 |
| 67 | void RunFourNodeCandidateNodesOnlyMasterTest(ubse::it::infra::ItCluster& cluster); | 77 | void RunFourNodeCandidateNodesOnlyMasterTest(ubse::it::infra::ItCluster& cluster); |
| 68 | 78 | ||
| 79 | +// 四节点新集群启动平滑测试:集群启动收敛后,通过 CLI 确认收敛(1主+1备+2agent), | ||
| 80 | +// 并校验各节点 ubse.log 中的节点状态:local 状态 READY(1)、cluster 状态 WORKING(2)。 | ||
| 81 | +void RunFourNodeStartupSmoothTest(ubse::it::infra::ItCluster& cluster); | ||
| 82 | + | ||
| 69 | } // namespace ubse::it::tests::election | 83 | } // namespace ubse::it::tests::election |
| 70 | 84 | ||
| 71 | 85 | ||
| @@ -0,0 +1,11 @@ | |||
| 1 | +add_library(it_log_cases STATIC | ||
| 2 | + log_cases.cpp | ||
| 3 | +) | ||
| 4 | +target_include_directories(it_log_cases PUBLIC | ||
| 5 | + ${CMAKE_SOURCE_DIR}/test/IT | ||
| 6 | + ${CMAKE_SOURCE_DIR}/src/framework/ipc/include | ||
| 7 | +) | ||
| 8 | +target_link_libraries(it_log_cases PUBLIC | ||
| 9 | + ubse_it_infra | ||
| 10 | +) | ||
| 11 | +target_compile_options(it_log_cases PRIVATE ${DEBUG_FLAGS}) | ||
| @@ -0,0 +1,586 @@ | |||
| 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, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 11 | + */ | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + | ||
| 31 | + | ||
| 32 | + | ||
| 33 | + | ||
| 34 | + | ||
| 35 | + | ||
| 36 | + | ||
| 37 | + | ||
| 38 | +namespace ubse::it::tests::log { | ||
| 39 | + | ||
| 40 | +namespace { | ||
| 41 | + | ||
| 42 | +// 修改配置文件中的 key=value(仅替换未注释的行,保留其他内容不变) | ||
| 43 | +bool SetConfigValue(const std::string& cfgPath, const std::string& key, const std::string& value) | ||
| 44 | +{ | ||
| 45 | + std::ifstream ifs(cfgPath); | ||
| 46 | + if (!ifs.is_open()) { | ||
| 47 | + return false; | ||
| 48 | + } | ||
| 49 | + std::vector<std::string> lines; | ||
| 50 | + std::string line; | ||
| 51 | + const std::string prefix = key + "="; | ||
| 52 | + while (std::getline(ifs, line)) { | ||
| 53 | + if (line.rfind(prefix, 0) == 0) { | ||
| 54 | + line = prefix + value; | ||
| 55 | + } | ||
| 56 | + lines.push_back(line); | ||
| 57 | + } | ||
| 58 | + ifs.close(); | ||
| 59 | + | ||
| 60 | + std::ofstream ofs(cfgPath, std::ios::trunc); | ||
| 61 | + if (!ofs.is_open()) { | ||
| 62 | + return false; | ||
| 63 | + } | ||
| 64 | + for (const auto& l : lines) { | ||
| 65 | + ofs << l << "\n"; | ||
| 66 | + } | ||
| 67 | + ofs.close(); | ||
| 68 | + return true; | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +// 向文件追加指定字节数的数据(模拟日志量增长,触发超过文件大小上限) | ||
| 72 | +void AppendBytes(const std::string& path, size_t bytes) | ||
| 73 | +{ | ||
| 74 | + std::ofstream ofs(path, std::ios::app | std::ios::binary); | ||
| 75 | + const std::string chunk(1024, 'R'); | ||
| 76 | + size_t written = 0; | ||
| 77 | + while (written < bytes) { | ||
| 78 | + size_t toWrite = std::min(chunk.size(), bytes - written); | ||
| 79 | + ofs.write(chunk.data(), static_cast<std::streamsize>(toWrite)); | ||
| 80 | + written += toWrite; | ||
| 81 | + } | ||
| 82 | + ofs.flush(); | ||
| 83 | + ofs.close(); | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +// 从日志文件路径提取模块名(如 /path/log/ubse.log -> ubse) | ||
| 87 | +std::string ModuleNameOf(const std::string& logFilePath) | ||
| 88 | +{ | ||
| 89 | + return std::filesystem::path(logFilePath).stem().string(); | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +// 判断是否为指定模块的日志绕接压缩文件 <module>_YYYYMMDD_HHMMSS_XXX.tar.gz。 | ||
| 93 | +// 注意:daemon 中多个模块(ubse_mem_scheduler/ubse_fault/ubse_cli 等)共用同一日志绕接配置, | ||
| 94 | +// 其绕接文件同样以 ubse_ 开头、.tar.gz 结尾,必须严格匹配"模块名+数字时间戳"避免误统计。 | ||
| 95 | +bool IsRotationFileOf(const std::string& name, const std::string& module) | ||
| 96 | +{ | ||
| 97 | + const std::string prefix = module + "_"; | ||
| 98 | + if (name.rfind(prefix, 0) != 0) { | ||
| 99 | + return false; | ||
| 100 | + } | ||
| 101 | + static const std::regex suffixPattern(R"(\d{8}_\d{6}_\d{3}\.tar\.gz)"); | ||
| 102 | + return std::regex_match(name.substr(prefix.size()), suffixPattern); | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +// 统计日志目录下指定模块绕接压缩文件的数量 | ||
| 106 | +size_t CountRotationFiles(const std::string& dir, const std::string& module) | ||
| 107 | +{ | ||
| 108 | + size_t count = 0; | ||
| 109 | + std::error_code ec; | ||
| 110 | + for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) { | ||
| 111 | + if (ec) { | ||
| 112 | + break; | ||
| 113 | + } | ||
| 114 | + if (!entry.is_regular_file()) { | ||
| 115 | + continue; | ||
| 116 | + } | ||
| 117 | + if (IsRotationFileOf(entry.path().filename().string(), module)) { | ||
| 118 | + ++count; | ||
| 119 | + } | ||
| 120 | + } | ||
| 121 | + return count; | ||
| 122 | +} | ||
| 123 | + | ||
| 124 | +// 返回日志目录下指定模块所有绕接压缩文件名(按名称排序,最早的在前) | ||
| 125 | +std::vector<std::string> ListRotationFiles(const std::string& dir, const std::string& module) | ||
| 126 | +{ | ||
| 127 | + std::vector<std::string> files; | ||
| 128 | + std::error_code ec; | ||
| 129 | + for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) { | ||
| 130 | + if (ec) { | ||
| 131 | + break; | ||
| 132 | + } | ||
| 133 | + if (!entry.is_regular_file()) { | ||
| 134 | + continue; | ||
| 135 | + } | ||
| 136 | + const std::string name = entry.path().filename().string(); | ||
| 137 | + if (IsRotationFileOf(name, module)) { | ||
| 138 | + files.push_back(name); | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | + std::sort(files.begin(), files.end()); | ||
| 142 | + return files; | ||
| 143 | +} | ||
| 144 | + | ||
| 145 | +// 删除日志目录下指定模块所有绕接压缩文件(保证绕接数量统计准确) | ||
| 146 | +void ClearRotationFiles(const std::string& dir, const std::string& module) | ||
| 147 | +{ | ||
| 148 | + for (const auto& name : ListRotationFiles(dir, module)) { | ||
| 149 | + std::error_code ec; | ||
| 150 | + std::filesystem::remove(dir + "/" + name, ec); | ||
| 151 | + } | ||
| 152 | +} | ||
| 153 | + | ||
| 154 | +// 解析日志行开头的 "YYYY-MM-DD HH:MM:SS.mmm" 时间戳,解析失败返回 false | ||
| 155 | +bool TryParseLogTime(const std::string& line, std::time_t& out) | ||
| 156 | +{ | ||
| 157 | + int year = 0; | ||
| 158 | + int mon = 0; | ||
| 159 | + int day = 0; | ||
| 160 | + int hour = 0; | ||
| 161 | + int min = 0; | ||
| 162 | + int sec = 0; | ||
| 163 | + // 日志格式为 "[YYYY-MM-DD HH:MM:SS.mmm+HH:MM][LEVEL]...",时间戳以 '[' 开头 | ||
| 164 | + if (sscanf(line.c_str(), " [%d-%d-%d %d:%d:%d", &year, &mon, &day, &hour, &min, &sec) != 6) { | ||
| 165 | + return false; | ||
| 166 | + } | ||
| 167 | + struct tm t { | ||
| 168 | + }; | ||
| 169 | + t.tm_year = year - 1900; | ||
| 170 | + t.tm_mon = mon - 1; | ||
| 171 | + t.tm_mday = day; | ||
| 172 | + t.tm_hour = hour; | ||
| 173 | + t.tm_min = min; | ||
| 174 | + t.tm_sec = sec; | ||
| 175 | + t.tm_isdst = -1; | ||
| 176 | + out = mktime(&t); | ||
| 177 | + return out != static_cast<std::time_t>(-1); | ||
| 178 | +} | ||
| 179 | + | ||
| 180 | +// 日志文件中最后一条有效日志的时间戳,无有效日志返回 0 | ||
| 181 | +std::time_t LastLogTime(const std::string& path) | ||
| 182 | +{ | ||
| 183 | + std::ifstream ifs(path); | ||
| 184 | + if (!ifs.is_open()) { | ||
| 185 | + return 0; | ||
| 186 | + } | ||
| 187 | + std::time_t last = 0; | ||
| 188 | + std::string line; | ||
| 189 | + while (std::getline(ifs, line)) { | ||
| 190 | + std::time_t t = 0; | ||
| 191 | + if (TryParseLogTime(line, t) && t > last) { | ||
| 192 | + last = t; | ||
| 193 | + } | ||
| 194 | + } | ||
| 195 | + return last; | ||
| 196 | +} | ||
| 197 | + | ||
| 198 | +// 日志文件中是否存在时间戳晚于指定时间的日志(证明绕接后日志正常打印) | ||
| 199 | +bool HasLogEntryAfter(const std::string& path, std::time_t after) | ||
| 200 | +{ | ||
| 201 | + std::ifstream ifs(path); | ||
| 202 | + if (!ifs.is_open()) { | ||
| 203 | + return false; | ||
| 204 | + } | ||
| 205 | + std::string line; | ||
| 206 | + while (std::getline(ifs, line)) { | ||
| 207 | + std::time_t t = 0; | ||
| 208 | + if (TryParseLogTime(line, t) && t > after) { | ||
| 209 | + return true; | ||
| 210 | + } | ||
| 211 | + } | ||
| 212 | + return false; | ||
| 213 | +} | ||
| 214 | + | ||
| 215 | +// 检查文件权限是否为 640(owner rw / group r / others 无任何权限) | ||
| 216 | +bool IsPerm640(const std::string& path) | ||
| 217 | +{ | ||
| 218 | + std::error_code ec; | ||
| 219 | + const std::filesystem::file_status st = std::filesystem::status(path, ec); | ||
| 220 | + if (ec) { | ||
| 221 | + return false; | ||
| 222 | + } | ||
| 223 | + using P = std::filesystem::perms; | ||
| 224 | + const P p = st.permissions(); | ||
| 225 | + return ((p & P::owner_read) != P::none) && ((p & P::owner_write) != P::none) && ((p & P::owner_exec) == P::none) && | ||
| 226 | + ((p & P::group_read) != P::none) && ((p & P::group_write) == P::none) && ((p & P::group_exec) == P::none) && | ||
| 227 | + ((p & P::others_read) == P::none) && ((p & P::others_write) == P::none) && ((p & P::others_exec) == P::none); | ||
| 228 | +} | ||
| 229 | + | ||
| 230 | +// ==================== 日志回调测试辅助 ==================== | ||
| 231 | + | ||
| 232 | +// 自定义日志处理函数的输出文件路径(回调为普通函数指针,不可带捕获,经文件级变量传递) | ||
| 233 | +std::string g_simpleLogPath; | ||
| 234 | + | ||
| 235 | +// 自定义日志处理函数:将 SDK 日志按自定义格式写入文件 | ||
| 236 | +// 自定义格式:[simple_log_handler] {log_level.name}:{message} | ||
| 237 | +void SimpleLogHandler(uint32_t level, const char* message) | ||
| 238 | +{ | ||
| 239 | + static constexpr std::array<const char*, 5> levelNames = {"DEBUG", "INFO", "WARN", "ERROR", "CRIT"}; | ||
| 240 | + if (level >= levelNames.size()) { | ||
| 241 | + return; | ||
| 242 | + } | ||
| 243 | + static std::mutex fileMutex; | ||
| 244 | + std::lock_guard<std::mutex> lock(fileMutex); | ||
| 245 | + std::ofstream ofs(g_simpleLogPath, std::ios::app); | ||
| 246 | + if (!ofs.is_open()) { | ||
| 247 | + return; | ||
| 248 | + } | ||
| 249 | + ofs << "[simple_log_handler] " << levelNames[level] << ":" << (message != nullptr ? message : "") << "\n"; | ||
| 250 | +} | ||
| 251 | + | ||
| 252 | +} // namespace | ||
| 253 | + | ||
| 254 | +void RunLogRotationTest(ubse::it::infra::ItCluster& cluster) | ||
| 255 | +{ | ||
| 256 | + auto& node = cluster.GetNode("1"); | ||
| 257 | + auto& cliInvoker = cluster.GetCliInvoker("1"); | ||
| 258 | + const std::string cfgPath = node.GetConfigFilePath(); | ||
| 259 | + const std::string logPath = node.GetLogFilePath(); | ||
| 260 | + const std::string logDir = node.GetSpec().LogDir(); | ||
| 261 | + const std::string module = ModuleNameOf(logPath); | ||
| 262 | + | ||
| 263 | + // 1. 修改配置 log.max.fileSize=2 (2MB),重启节点使配置生效 | ||
| 264 | + EXPECT_TRUE(SetConfigValue(cfgPath, "log.max.fileSize", "2")) << "修改 log.max.fileSize 配置应成功"; | ||
| 265 | + ASSERT_IT_OK(cluster.RestartNode("1", true, 30000)) << "重启节点使日志绕接配置生效应成功"; | ||
| 266 | + | ||
| 267 | + // 2. 记录绕接前日志打印时间 | ||
| 268 | + const std::time_t preRotateTime = LastLogTime(logPath); | ||
| 269 | + IT_LOG_INFO << "Log rotation: pre-rotation last log time=" << preRotateTime << ", log=" << logPath; | ||
| 270 | + | ||
| 271 | + // 3. 向日志追加 >2MB 数据,使 ubse.log 超过 log.max.fileSize | ||
| 272 | + constexpr size_t mB = 1024 * 1024; | ||
| 273 | + AppendBytes(logPath, 3 * mB); | ||
| 274 | + IT_LOG_INFO << "Log rotation: appended 3MB to " << logPath; | ||
| 275 | + | ||
| 276 | + // 通过 CLI 触发 daemon 写入日志(每次 UDS 连接都会打印 INFO 日志),从而触发绕接 | ||
| 277 | + cliInvoker.ExecCli("display node"); | ||
| 278 | + cliInvoker.ExecCli("display node"); | ||
| 279 | + | ||
| 280 | + // 4. 等待绕接:有新的绕接压缩文件生成则绕接成功(仅统计本模块,排除其他模块的绕接文件) | ||
| 281 | + auto ret = ubse::it::infra::ItWaitHelper::WaitForCondition( | ||
| 282 | + [&logDir, &module]() -> bool { return CountRotationFiles(logDir, module) >= 1; }, | ||
| 283 | + ubse::it::infra::ItWaitHelper::DEFAULT_ELECTION_TIMEOUT_MS); | ||
| 284 | + ASSERT_IT_OK(ret) << "超过 log.max.fileSize 后应生成新的绕接压缩文件"; | ||
| 285 | + | ||
| 286 | + const size_t rotatedCount = CountRotationFiles(logDir, module); | ||
| 287 | + IT_LOG_INFO << "Log rotation: generated " << rotatedCount << " rotation file(s) in " << logDir; | ||
| 288 | + | ||
| 289 | + // 5. 验证绕接后日志正常打印:再次触发 daemon 写日志, | ||
| 290 | + // 新 ubse.log 中应出现时间戳晚于绕接前最后一条日志的新日志 | ||
| 291 | + cliInvoker.ExecCli("display node"); | ||
| 292 | + ret = ubse::it::infra::ItWaitHelper::WaitForCondition( | ||
| 293 | + [&logPath, &preRotateTime]() -> bool { return HasLogEntryAfter(logPath, preRotateTime); }, | ||
| 294 | + ubse::it::infra::ItWaitHelper::DEFAULT_ELECTION_TIMEOUT_MS); | ||
| 295 | + ASSERT_IT_OK(ret) << "绕接后日志应继续正常打印"; | ||
| 296 | + | ||
| 297 | + const std::time_t postRotateTime = LastLogTime(logPath); | ||
| 298 | + IT_LOG_INFO << "Log rotation: post-rotation last log time=" << postRotateTime << ", pre-rotation=" << preRotateTime; | ||
| 299 | + EXPECT_GE(postRotateTime, preRotateTime) << "绕接后日志打印时间应不早于绕接前"; | ||
| 300 | + | ||
| 301 | + // 6. 恢复配置 log.max.fileSize=20 并重启进程 | ||
| 302 | + EXPECT_TRUE(SetConfigValue(cfgPath, "log.max.fileSize", "20")) << "恢复 log.max.fileSize 配置应成功"; | ||
| 303 | + ASSERT_IT_OK(cluster.RestartNode("1", true, 30000)) << "恢复配置并重启应成功"; | ||
| 304 | + | ||
| 305 | + IT_LOG_INFO << "Log rotation test passed"; | ||
| 306 | +} | ||
| 307 | + | ||
| 308 | +// 日志绕接文件数量上限测试(单节点场景): | ||
| 309 | +// 1) 修改配置 log.max.fileSize=2 (2MB)、log.fileNums=3,重启节点使配置生效; | ||
| 310 | +// 2) 连续触发 4 次日志绕接; | ||
| 311 | +// 3) 验证绕接文件数量始终不超过 log.fileNums(3),且绕接 4 次后最早生成的日志包已被删除替换; | ||
| 312 | +// 4) 恢复配置 log.max.fileSize=20、log.fileNums=20,重启节点。 | ||
| 313 | +void RunLogRotationFileNumTest(ubse::it::infra::ItCluster& cluster) | ||
| 314 | +{ | ||
| 315 | + auto& node = cluster.GetNode("1"); | ||
| 316 | + auto& cliInvoker = cluster.GetCliInvoker("1"); | ||
| 317 | + const std::string cfgPath = node.GetConfigFilePath(); | ||
| 318 | + const std::string logPath = node.GetLogFilePath(); | ||
| 319 | + const std::string logDir = node.GetSpec().LogDir(); | ||
| 320 | + const std::string module = ModuleNameOf(logPath); | ||
| 321 | + | ||
| 322 | + // 1. 修改配置 log.max.fileSize=2 (2MB)、log.fileNums=3,重启节点使配置生效 | ||
| 323 | + EXPECT_TRUE(SetConfigValue(cfgPath, "log.max.fileSize", "2")) << "修改 log.max.fileSize 配置应成功"; | ||
| 324 | + EXPECT_TRUE(SetConfigValue(cfgPath, "log.fileNums", "3")) << "修改 log.fileNums 配置应成功"; | ||
| 325 | + ASSERT_IT_OK(cluster.RestartNode("1", true, 30000)) << "重启节点使日志绕接配置生效应成功"; | ||
| 326 | + | ||
| 327 | + // 清理历史绕接文件,保证绕接文件数量统计准确 | ||
| 328 | + ClearRotationFiles(logDir, module); | ||
| 329 | + | ||
| 330 | + constexpr size_t MB = 1024 * 1024; | ||
| 331 | + | ||
| 332 | + // 2. 连续触发 3 次绕接,日志目录应累计 3 个绕接文件。 | ||
| 333 | + // 绕接压缩文件名形如 <module>_YYYYMMDD_HHMMSS_XXX.tar.gz(秒级时间戳)。绕接删除最早 | ||
| 334 | + // 文件后,剩余文件会重命名补位(序号前移),若多次绕接落在同一秒内,文件名会被完全 | ||
| 335 | + // 复用,导致无法通过文件名判断最早文件是否被删除。因此每轮注入前等待 >1s,确保各次 | ||
| 336 | + // 绕接的时间戳互不相同,文件名即可唯一标识物理文件。 | ||
| 337 | + for (int i = 1; i <= 3; ++i) { | ||
| 338 | + if (i > 1) { | ||
| 339 | + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); | ||
| 340 | + } | ||
| 341 | + AppendBytes(logPath, 3 * MB); | ||
| 342 | + IT_LOG_INFO << "Log rotation fileNums: round " << i << " appended 3MB to " << logPath; | ||
| 343 | + cliInvoker.ExecCli("display node"); | ||
| 344 | + auto ret = ubse::it::infra::ItWaitHelper::WaitForCondition( | ||
| 345 | + [&logDir, &module, i]() -> bool { return CountRotationFiles(logDir, module) >= static_cast<size_t>(i); }, | ||
| 346 | + ubse::it::infra::ItWaitHelper::DEFAULT_ELECTION_TIMEOUT_MS); | ||
| 347 | + ASSERT_IT_OK(ret) << "第 " << i << " 次绕接后应累计 " << i << " 个绕接文件"; | ||
| 348 | + } | ||
| 349 | + | ||
| 350 | + // 3. 记录第 3 次绕接后最早的绕接文件(即第 1 次绕接生成的日志包)。 | ||
| 351 | + // 由于各轮绕接时间戳不同,文件名可唯一标识物理文件。 | ||
| 352 | + auto files = ListRotationFiles(logDir, module); | ||
| 353 | + ASSERT_FALSE(files.empty()) << "第 3 次绕接后应存在绕接文件"; | ||
| 354 | + const std::string oldestFile = logDir + "/" + files.front(); | ||
| 355 | + IT_LOG_INFO << "Log rotation fileNums: oldest rotation file after round 3 = " << oldestFile; | ||
| 356 | + | ||
| 357 | + // 4. 触发第 4 次绕接:超过 log.fileNums(3) 后最早生成的日志包应被删除,仅保留 3 个。 | ||
| 358 | + // 同样等待 >1s 保证第 4 次绕接的时间戳与之前不同,文件名不被复用。 | ||
| 359 | + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); | ||
| 360 | + AppendBytes(logPath, 3 * MB); | ||
| 361 | + IT_LOG_INFO << "Log rotation fileNums: round 4 appended 3MB to " << logPath; | ||
| 362 | + cliInvoker.ExecCli("display node"); | ||
| 363 | + auto ret = ubse::it::infra::ItWaitHelper::WaitForCondition( | ||
| 364 | + [&logDir, &module, &oldestFile]() -> bool { | ||
| 365 | + std::error_code ec; | ||
| 366 | + return CountRotationFiles(logDir, module) == 3 && !std::filesystem::exists(oldestFile, ec); | ||
| 367 | + }, | ||
| 368 | + ubse::it::infra::ItWaitHelper::DEFAULT_ELECTION_TIMEOUT_MS); | ||
| 369 | + // 输出第 4 次绕接后的实际状态,便于定位问题 | ||
| 370 | + IT_LOG_INFO << "Log rotation fileNums: after round-4 wait, count=" << CountRotationFiles(logDir, module) | ||
| 371 | + << ", oldestExists=" << std::filesystem::exists(oldestFile) << ", oldest=" << oldestFile; | ||
| 372 | + for (const auto& f : ListRotationFiles(logDir, module)) { | ||
| 373 | + IT_LOG_INFO << "Log rotation fileNums: rotation file: " << f; | ||
| 374 | + } | ||
| 375 | + ASSERT_IT_OK(ret) << "绕接超过 log.fileNums 后最早生成的日志包应被删除,绕接文件数量应保持为 3"; | ||
| 376 | + | ||
| 377 | + // 5. 验证最终绕接文件数量不超过 log.fileNums(3) | ||
| 378 | + const size_t rotatedCount = CountRotationFiles(logDir, module); | ||
| 379 | + IT_LOG_INFO << "Log rotation fileNums: final rotation file count = " << rotatedCount; | ||
| 380 | + EXPECT_EQ(rotatedCount, 3u) << "绕接四次后应仅保留 3 个绕接文件"; | ||
| 381 | + EXPECT_LE(rotatedCount, 3u) << "绕接文件数量不应超过 log.fileNums(3)"; | ||
| 382 | + | ||
| 383 | + // 6. 恢复配置 log.max.fileSize=20、log.fileNums=20 并重启进程 | ||
| 384 | + EXPECT_TRUE(SetConfigValue(cfgPath, "log.max.fileSize", "20")) << "恢复 log.max.fileSize 配置应成功"; | ||
| 385 | + EXPECT_TRUE(SetConfigValue(cfgPath, "log.fileNums", "20")) << "恢复 log.fileNums 配置应成功"; | ||
| 386 | + ASSERT_IT_OK(cluster.RestartNode("1", true, 30000)) << "恢复配置并重启应成功"; | ||
| 387 | + | ||
| 388 | + IT_LOG_INFO << "Log rotation fileNums test passed"; | ||
| 389 | +} | ||
| 390 | + | ||
| 391 | +// 日志文件格式校验测试(单节点场景): | ||
| 392 | +// 1) 验证默认日志目录存在日志文件(ubse.log 非空); | ||
| 393 | +// 2) 验证日志目录下所有 .log 文件读写权限为 640(owner rw / group r / others 无); | ||
| 394 | +// 3) 获取最近 50h 的日志,逐条目校验日志格式: | ||
| 395 | +// - 标准格式(com_pattern):[时间戳][等级][进程][线程][traceid][文件:函数:行号] 消息; | ||
| 396 | +// - 第三方组件格式(third_pattern):[时间戳][等级][进程][线程][traceid][文件:行号] 消息 | ||
| 397 | +// (如 HCOM 组件,无函数名字段); | ||
| 398 | +// - 日志等级(level_pattern)必须在合法列表 WARN/DEBUG/INFO/ERROR/CRIT 内。 | ||
| 399 | +void RunLogFormatTest(ubse::it::infra::ItCluster& cluster) | ||
| 400 | +{ | ||
| 401 | + auto& node = cluster.GetNode("1"); | ||
| 402 | + const std::string logDir = node.GetSpec().LogDir(); | ||
| 403 | + const std::string logPath = node.GetLogFilePath(); | ||
| 404 | + | ||
| 405 | + // 1. 验证默认日志目录存在且主日志文件存在非空 | ||
| 406 | + ASSERT_TRUE(std::filesystem::exists(logDir)) << "默认日志目录应存在: " << logDir; | ||
| 407 | + ASSERT_TRUE(std::filesystem::exists(logPath)) << "主日志文件应存在: " << logPath; | ||
| 408 | + const auto logSize = std::filesystem::file_size(logPath); | ||
| 409 | + IT_LOG_INFO << "Log format: log file " << logPath << ", size=" << logSize; | ||
| 410 | + ASSERT_GT(logSize, 0u) << "主日志文件应非空"; | ||
| 411 | + | ||
| 412 | + // 2. 验证日志目录下所有 .log 文件权限为 640 | ||
| 413 | + std::vector<std::string> logFiles; | ||
| 414 | + std::error_code ec; | ||
| 415 | + for (const auto& entry : std::filesystem::directory_iterator(logDir, ec)) { | ||
| 416 | + if (ec) { | ||
| 417 | + break; | ||
| 418 | + } | ||
| 419 | + if (entry.is_regular_file() && entry.path().extension() == ".log") { | ||
| 420 | + logFiles.push_back(entry.path().string()); | ||
| 421 | + } | ||
| 422 | + } | ||
| 423 | + ASSERT_FALSE(logFiles.empty()) << "日志目录下应存在 .log 日志文件"; | ||
| 424 | + for (const auto& f : logFiles) { | ||
| 425 | + IT_LOG_INFO << "Log format: checking permission of " << f; | ||
| 426 | + EXPECT_TRUE(IsPerm640(f)) << "日志文件权限应为 640: " << f; | ||
| 427 | + } | ||
| 428 | + | ||
| 429 | + // 3. 定义正则模式 | ||
| 430 | + // 日期时间戳模式:[YYYY-MM-DD HH:MM:SS.mmm+HH:MM] | ||
| 431 | + const std::regex datePattern(R"(^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}\])"); | ||
| 432 | + // 合法日志等级列表 | ||
| 433 | + const std::regex levelPattern(R"(^(DEBUG|INFO|WARN|ERROR|CRIT)$)"); | ||
| 434 | + // 标准日志格式:[时间戳][等级][进程][线程][traceid][文件:函数:行号] 消息。 | ||
| 435 | + // traceid 为 UUID 或备用 hex 格式,也可能为空(UUID 库不可用时)。 | ||
| 436 | + const std::regex comPattern(R"(^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}\])" | ||
| 437 | + R"(\[(\w+)\]\[(\d+)\]\[(\d+)\]\[([0-9a-fA-F-]*)\])" | ||
| 438 | + R"(\[([^\[\]:]+):([^\[\]:]+):(\d+)\] )"); | ||
| 439 | + // 第三方组件日志格式:[时间戳][等级][进程][线程][traceid][文件:行号] 消息(无函数名,如 HCOM 组件) | ||
| 440 | + const std::regex thirdPattern(R"(^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}\])" | ||
| 441 | + R"(\[(\w+)\]\[(\d+)\]\[(\d+)\]\[([0-9a-fA-F-]*)\])" | ||
| 442 | + R"(\[([^\[\]:]+):(\d+)\] )"); | ||
| 443 | + | ||
| 444 | + // 4. 获取最近 50h 的日志并校验格式。 | ||
| 445 | + // 日志条目以时间戳行开始,多行消息的续行跟随所属条目,仅对条目首行做格式校验。 | ||
| 446 | + constexpr int64_t logWindowSeconds = 50 * 3600; | ||
| 447 | + const std::time_t cutoff = std::time(nullptr) - logWindowSeconds; | ||
| 448 | + | ||
| 449 | + std::ifstream ifs(logPath); | ||
| 450 | + ASSERT_TRUE(ifs.is_open()) << "应能打开日志文件: " << logPath; | ||
| 451 | + | ||
| 452 | + size_t totalEntries = 0; // 50h 内的日志条目总数 | ||
| 453 | + size_t comEntries = 0; // 标准格式条目数 | ||
| 454 | + size_t thirdEntries = 0; // 第三方组件格式条目数 | ||
| 455 | + size_t levelBad = 0; // 等级非法的条目数 | ||
| 456 | + size_t formatBad = 0; // 格式错误的条目数 | ||
| 457 | + std::map<std::string, size_t> levelCount; // 各等级条目数统计 | ||
| 458 | + std::string line; | ||
| 459 | + while (std::getline(ifs, line)) { | ||
| 460 | + if (std::regex_search(line, datePattern)) { | ||
| 461 | + // 新日志条目开始:解析时间戳判断是否在 50h 窗口内 | ||
| 462 | + std::time_t t = 0; | ||
| 463 | + if (!(TryParseLogTime(line, t) && t >= cutoff)) { | ||
| 464 | + continue; | ||
| 465 | + } | ||
| 466 | + ++totalEntries; | ||
| 467 | + // 注意用 regex_search 而非 regex_match:模式仅锚定行首头部格式,消息部分任意 | ||
| 468 | + std::smatch m; | ||
| 469 | + if (std::regex_search(line, m, comPattern)) { | ||
| 470 | + ++comEntries; | ||
| 471 | + } else if (std::regex_search(line, m, thirdPattern)) { | ||
| 472 | + ++thirdEntries; | ||
| 473 | + } else { | ||
| 474 | + ++formatBad; | ||
| 475 | + IT_LOG_INFO << "Log format: bad format line: " << line.substr(0, 200); | ||
| 476 | + continue; | ||
| 477 | + } | ||
| 478 | + // 提取日志等级并校验合法性(com/third 模式第 1 组均为等级) | ||
| 479 | + const std::string level = m[1].str(); | ||
| 480 | + if (std::regex_match(level, levelPattern)) { | ||
| 481 | + ++levelCount[level]; | ||
| 482 | + } else { | ||
| 483 | + ++levelBad; | ||
| 484 | + IT_LOG_INFO << "Log format: invalid level '" << level << "': " << line.substr(0, 200); | ||
| 485 | + } | ||
| 486 | + } | ||
| 487 | + // 续行(多行消息)跟随所属条目,无需格式校验 | ||
| 488 | + } | ||
| 489 | + ifs.close(); | ||
| 490 | + | ||
| 491 | + // 5. 输出校验统计并断言 | ||
| 492 | + IT_LOG_INFO << "Log format: total=" << totalEntries << ", com=" << comEntries << ", third=" << thirdEntries | ||
| 493 | + << ", badFormat=" << formatBad << ", badLevel=" << levelBad; | ||
| 494 | + for (const auto& [lv, cnt] : levelCount) { | ||
| 495 | + IT_LOG_INFO << "Log format: level " << lv << " count=" << cnt; | ||
| 496 | + } | ||
| 497 | + | ||
| 498 | + ASSERT_GT(totalEntries, 0u) << "最近 50h 内应存在日志条目"; | ||
| 499 | + EXPECT_EQ(formatBad, 0u) << "所有日志条目格式应正确(标准或第三方组件格式)"; | ||
| 500 | + EXPECT_EQ(levelBad, 0u) << "所有日志条目等级应合法(WARN/DEBUG/INFO/ERROR/CRIT)"; | ||
| 501 | + EXPECT_GT(comEntries, 0u) << "应存在标准格式(含进程/线程/traceid/函数信息)的日志条目"; | ||
| 502 | + | ||
| 503 | + IT_LOG_INFO << "Log format test passed"; | ||
| 504 | +} | ||
| 505 | + | ||
| 506 | +// 日志回调注册测试(单节点场景): | ||
| 507 | +// 1) 通过 ubs_engine_log_callback_register 注册自定义日志处理函数 SimpleLogHandler; | ||
| 508 | +// 2) 注册成功后输出日志 "Custom log handler registered successfully"(经回调输出); | ||
| 509 | +// 3) 触发多等级日志(INFO/WARN/ERROR)及真实 SDK 调用日志(非法参数调用产生 ERROR 级日志), | ||
| 510 | +// 验证均经自定义处理函数按自定义格式输出; | ||
| 511 | +// 4) 校验自定义格式:[simple_log_handler] {log_level.name}:{message}, | ||
| 512 | +// 其中 log_level.name 必须在合法列表 INFO|WARN|ERROR|CRIT 内; | ||
| 513 | +// 5) 恢复默认输出(注册 nullptr),清理临时文件。 | ||
| 514 | +void RunLogCallbackTest(ubse::it::infra::ItCluster& cluster) | ||
| 515 | +{ | ||
| 516 | + auto& node = cluster.GetNode("1"); | ||
| 517 | + auto& sdk = cluster.GetSdkClient("1"); | ||
| 518 | + | ||
| 519 | + // 1. 准备自定义日志文件(放在节点工作目录下,用例结束清理) | ||
| 520 | + g_simpleLogPath = node.GetWorkDir() + "/simple_log_handler.log"; | ||
| 521 | + std::error_code ec; | ||
| 522 | + std::filesystem::remove(g_simpleLogPath, ec); | ||
| 523 | + | ||
| 524 | + // 2. 注册自定义日志处理函数,此后本进程内 SDK 日志均经该回调输出 | ||
| 525 | + ubs_engine_log_callback_register(SimpleLogHandler); | ||
| 526 | + // 注册成功日志:经自定义处理函数按自定义格式输出 | ||
| 527 | + IPC_LOG_INFO << "Custom log handler registered successfully"; | ||
| 528 | + // 触发多等级日志,验证均按自定义格式输出 | ||
| 529 | + IPC_LOG_WARN << "Log callback test: warn level message"; | ||
| 530 | + IPC_LOG_ERROR << "Log callback test: error level message"; | ||
| 531 | + // 真实 SDK 调用:非法参数调用在 SDK 内部产生 ERROR 级日志,同样经回调输出 | ||
| 532 | + uint32_t cnt = 0; | ||
| 533 | + EXPECT_EQ(sdk.TopoNodeList(nullptr, &cnt), UBS_ERR_NULL_POINTER) << "非法参数调用应返回 NULL_POINTER"; | ||
| 534 | + | ||
| 535 | + // 3. 恢复默认输出(stdout),避免影响其他用例;后续仅校验已写文件 | ||
| 536 | + ubs_engine_log_callback_register(nullptr); | ||
| 537 | + | ||
| 538 | + // 4. 读取自定义日志文件校验格式 | ||
| 539 | + const std::string customLogPath = g_simpleLogPath; | ||
| 540 | + g_simpleLogPath.clear(); | ||
| 541 | + ASSERT_TRUE(std::filesystem::exists(customLogPath)) << "自定义日志文件应存在: " << customLogPath; | ||
| 542 | + | ||
| 543 | + // 自定义格式:[simple_log_handler] {log_level.name}:{message}(":"为全角冒号) | ||
| 544 | + // 合法日志等级列表:INFO|WARN|ERROR|CRIT | ||
| 545 | + const std::regex customPattern(R"(^\[simple_log_handler\] (INFO|WARN|ERROR|CRIT):)"); | ||
| 546 | + | ||
| 547 | + std::ifstream ifs(customLogPath); | ||
| 548 | + ASSERT_TRUE(ifs.is_open()) << "应能打开自定义日志文件: " << customLogPath; | ||
| 549 | + | ||
| 550 | + size_t totalLines = 0; // 总行数 | ||
| 551 | + size_t matchedLines = 0; // 符合自定义格式的行数 | ||
| 552 | + bool hasRegisterMsg = false; // 是否包含注册成功日志 | ||
| 553 | + bool hasSdkLog = false; // 是否包含 SDK 内部日志(非法参数调用触发) | ||
| 554 | + std::string line; | ||
| 555 | + while (std::getline(ifs, line)) { | ||
| 556 | + if (line.empty()) { | ||
| 557 | + continue; | ||
| 558 | + } | ||
| 559 | + ++totalLines; | ||
| 560 | + if (std::regex_search(line, customPattern)) { | ||
| 561 | + ++matchedLines; | ||
| 562 | + } else { | ||
| 563 | + IT_LOG_INFO << "Log callback: bad format line: " << line.substr(0, 200); | ||
| 564 | + } | ||
| 565 | + if (line.find("Custom log handler registered successfully") != std::string::npos) { | ||
| 566 | + hasRegisterMsg = true; | ||
| 567 | + } | ||
| 568 | + if (line.find("Invalid parameters") != std::string::npos) { | ||
| 569 | + hasSdkLog = true; | ||
| 570 | + } | ||
| 571 | + } | ||
| 572 | + ifs.close(); | ||
| 573 | + IT_LOG_INFO << "Log callback: total=" << totalLines << ", matched=" << matchedLines; | ||
| 574 | + | ||
| 575 | + EXPECT_GT(totalLines, 0u) << "自定义日志文件应有日志输出"; | ||
| 576 | + EXPECT_TRUE(hasRegisterMsg) << "自定义日志应包含注册成功日志 Custom log handler registered successfully"; | ||
| 577 | + EXPECT_TRUE(hasSdkLog) << "自定义日志应包含 SDK 内部日志(验证 SDK 日志经回调输出)"; | ||
| 578 | + EXPECT_EQ(matchedLines, totalLines) << "所有日志行都应符合自定义格式 [simple_log_handler] LEVEL:message"; | ||
| 579 | + | ||
| 580 | + // 5. 清理临时文件 | ||
| 581 | + std::filesystem::remove(customLogPath, ec); | ||
| 582 | + | ||
| 583 | + IT_LOG_INFO << "Log callback test passed"; | ||
| 584 | +} | ||
| 585 | + | ||
| 586 | +} // namespace ubse::it::tests::log | ||
| @@ -0,0 +1,54 @@ | |||
| 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, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 11 | + */ | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | +namespace ubse::it::tests::log { | ||
| 19 | + | ||
| 20 | +// 日志绕接测试(单节点场景): | ||
| 21 | +// 1) 修改配置 log.max.fileSize=2 (2MB),重启节点使配置生效; | ||
| 22 | +// 2) 向 ubse.log 追加数据使文件超过 2MB,触发日志绕接; | ||
| 23 | +// 3) 验证有新的绕接压缩文件 ubse_*.tar.gz 生成(绕接成功); | ||
| 24 | +// 4) 对比绕接前后日志打印时间,验证绕接后日志继续正常打印; | ||
| 25 | +// 5) 恢复配置 log.max.fileSize=20,重启节点。 | ||
| 26 | +void RunLogRotationTest(ubse::it::infra::ItCluster& cluster); | ||
| 27 | + | ||
| 28 | +// 日志绕接文件数量上限测试(单节点场景): | ||
| 29 | +// 1) 修改配置 log.max.fileSize=2 (2MB)、log.fileNums=3,重启节点使配置生效; | ||
| 30 | +// 2) 连续触发 4 次日志绕接; | ||
| 31 | +// 3) 验证绕接文件数量始终不超过 log.fileNums(3),且绕接 4 次后最早生成的日志包已被删除替换; | ||
| 32 | +// 4) 恢复配置 log.max.fileSize=20、log.fileNums=20,重启节点。 | ||
| 33 | +void RunLogRotationFileNumTest(ubse::it::infra::ItCluster& cluster); | ||
| 34 | + | ||
| 35 | +// 日志文件格式校验测试(单节点场景): | ||
| 36 | +// 1) 验证默认日志目录存在日志文件(ubse.log 非空); | ||
| 37 | +// 2) 验证日志目录下所有 .log 文件读写权限为 640; | ||
| 38 | +// 3) 获取最近 50h 的日志,逐条目校验格式: | ||
| 39 | +// - 标准格式(com_pattern):[时间戳][等级][进程][线程][traceid][文件:函数:行号] 消息; | ||
| 40 | +// - 第三方组件格式(third_pattern):[时间戳][等级][进程][线程][traceid][文件:行号] 消息; | ||
| 41 | +// - 日志等级(level_pattern)必须在 WARN/DEBUG/INFO/ERROR/CRIT 内。 | ||
| 42 | +void RunLogFormatTest(ubse::it::infra::ItCluster& cluster); | ||
| 43 | + | ||
| 44 | +// 日志回调注册测试(单节点场景): | ||
| 45 | +// 1) 通过 ubs_engine_log_callback_register 注册自定义日志处理函数; | ||
| 46 | +// 2) 验证注册成功日志 "Custom log handler registered successfully" 经回调按自定义格式输出; | ||
| 47 | +// 3) 触发多等级日志及真实 SDK 调用日志,验证均按自定义格式输出: | ||
| 48 | +// [simple_log_handler] {log_level.name}:{message},等级在 INFO|WARN|ERROR|CRIT 内; | ||
| 49 | +// 4) 恢复默认输出并清理临时文件。 | ||
| 50 | +void RunLogCallbackTest(ubse::it::infra::ItCluster& cluster); | ||
| 51 | + | ||
| 52 | +} // namespace ubse::it::tests::log | ||
| 53 | + | ||
| 54 | + | ||