已合并
【UBSE】补充IT用例 #1511
【UBSE】补充IT用例 #1511
已合并
weaver创建于 12 天前
12 个文件变更+863-5
@@ -40,7 +40,7 @@ struct PairHash {
40using UbseClientIpcHandlerMap = std::unordered_map<std::pair<uint16_t, uint16_t>, UbseClientIpcHandler, PairHash>;40using UbseClientIpcHandlerMap = std::unordered_map<std::pair<uint16_t, uint16_t>, UbseClientIpcHandler, PairHash>;
41std::mutex clientIpcHandlerMutex; // mutex锁41std::mutex clientIpcHandlerMutex; // mutex锁
42static UbseClientIpcHandlerMap clientIpcHandlerMap{};42static 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;
44constexpr uint32_t MAX_RESPONSE_SIZE = 10 * 1024 * 1024;44constexpr uint32_t MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
45 45 
46static uint32_t CopyResponseBody(const UbseResponseMessage& src, ubse_api_buffer_t* dest)46static 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 @@
1add_it_scene(single_node1add_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_cases2+ 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#include "tests/client/client_cases.h"14#include "tests/client/client_cases.h"
15#include "tests/election/election_cases.h"15#include "tests/election/election_cases.h"
16#include "tests/fault/fault_cases.h"16#include "tests/fault/fault_cases.h"
17+#include "tests/log/log_cases.h"
17#include "tests/mem_borrow/mem_borrow_cases.h"18#include "tests/mem_borrow/mem_borrow_cases.h"
18#include "tests/smoke/smoke_cases.h"19#include "tests/smoke/smoke_cases.h"
19#include "tests/topo/topo_cases.h"20#include "tests/topo/topo_cases.h"
@@ -347,4 +348,33 @@ TEST_F(Tongsuan1dFullMeshSingleNodeScenario, ElectionConvergence)
347TEST_F(Tongsuan1dFullMeshSingleNodeScenario, BmcFaultSingleNode)348TEST_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 @@
1add_it_scene(two_nodes1add_it_scene(two_nodes
2 LINK_LIBS it_election_cases it_mem_borrow_cases it_sei_degrade_cases it_topo_cases2 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)
10add_subdirectory(sei_degrade)10add_subdirectory(sei_degrade)
11add_subdirectory(urma)11add_subdirectory(urma)
12add_subdirectory(topo)12add_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] ProcTimer123// 读取主节点配置的心跳周期,等待 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代理
362void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster)464void 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::election689+// 四节点新集群启动平滑测试:验证新集群启动后各节点平滑上线。
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。
53void RunTwoNodeBothElectionDisabledTest(ubse::it::infra::ItCluster& cluster);53void 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代理
56void RunFourNodeElectionTest(ubse::it::infra::ItCluster& cluster);66void 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可先后升主并选出唯一备。
67void RunFourNodeCandidateNodesOnlyMasterTest(ubse::it::infra::ItCluster& cluster);77void 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::election83} // namespace ubse::it::tests::election
70 84 
71#endif // IT_ELECTION_CASES_H85#endif // IT_ELECTION_CASES_H
@@ -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+#include "log_cases.h"
14+ 
15+#include <gtest/gtest.h>
16+ 
17+#include <algorithm>
18+#include <array>
19+#include <chrono>
20+#include <cstdio>
21+#include <cstring>
22+#include <ctime>
23+#include <filesystem>
24+#include <fstream>
25+#include <map>
26+#include <mutex>
27+#include <regex>
28+#include <string>
29+#include <thread>
30+#include <vector>
31+ 
32+#include "ubse_ipc_log.h"
33+#include "it_assertion.h"
34+#include "it_console_log.h"
35+#include "it_wait_helper.h"
36+#include "ubs_engine_log.h"
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+#ifndef IT_LOG_CASES_H
14+#define IT_LOG_CASES_H
15+ 
16+#include "it_cluster.h"
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+#endif // IT_LOG_CASES_H