/** * @file runtime_stats_test.cpp * @brief Kiểm @ref move_base2::RuntimeStats. * * Ba tính chất được khoá lại ở đây, đều là thứ mà một lỗi ở chúng sẽ làm bảng thống kê nói dối chứ * không làm chương trình chết: * * 1. **Tắt là tắt hẳn** — `period <= 0` thì không đăng ký được đoạn nào, không đo, không in. * Telemetry bật ngoài ý muốn trên robot thật nghĩa là log chen vào vòng điều khiển. * 2. **Cửa sổ được reset sau mỗi lần in** — số liệu là của cửa sổ vừa qua, không phải tích luỹ từ * lúc khởi động. Cộng dồn mãi thì mọi đỉnh tức thời sẽ bị pha loãng và không bao giờ thấy lại. * 3. **Thread tự tạo được gán nhãn qua cửa sổ chụp** — đây là cơ chế duy nhất gọi tên được thread * của costmap mà không phải sửa gói costmap. */ #include #include #include #include #include namespace { using move_base2::RuntimeStats; using move_base2::ScopedSection; /// Đốt CPU thật trong khoảng @p ms — sleep không làm tăng bộ đếm CPU của thread. void burnCpu(int ms) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms); volatile double sink = 0.0; while (std::chrono::steady_clock::now() < deadline) { for (int i = 0; i < 1000; ++i) { sink += static_cast(i) * 1.000001; } } (void)sink; } TEST(RuntimeStatsTest, DisabledMeansNoWork) { RuntimeStats stats(0.0); EXPECT_FALSE(stats.enabled()); EXPECT_EQ(stats.section("anything"), RuntimeStats::kInvalidSection); EXPECT_FALSE(stats.tick()) << "period = 0 yet it still printed to the terminal"; // Không được crash dù chỉ số không hợp lệ. stats.record(RuntimeStats::kInvalidSection, 1000); stats.registerCurrentThread("must not be recorded"); const std::string table = stats.render(); EXPECT_EQ(table.find("anything"), std::string::npos); } TEST(RuntimeStatsTest, NegativePeriodDisables) { RuntimeStats stats(-1.0); EXPECT_FALSE(stats.enabled()); } TEST(RuntimeStatsTest, SectionAccumulatesAndResetsPerWindow) { RuntimeStats stats(3600.0); // chu kỳ dài: chỉ render() thủ công mới đóng cửa sổ ASSERT_TRUE(stats.enabled()); const RuntimeStats::SectionId id = stats.section("test.section"); ASSERT_NE(id, RuntimeStats::kInvalidSection); stats.record(id, 1'000'000); // 1 ms stats.record(id, 3'000'000); // 3 ms const std::string first = stats.render(); EXPECT_NE(first.find("test.section"), std::string::npos); EXPECT_NE(first.find("2.00"), std::string::npos) << "average must be 2.00 ms:\n" << first; EXPECT_NE(first.find("3.00"), std::string::npos) << "peak must be 3.00 ms:\n" << first; // Cửa sổ mới: đoạn vẫn còn trong bảng nhưng số liệu về 0. const std::string second = stats.render(); EXPECT_NE(second.find("test.section"), std::string::npos); EXPECT_NE(second.find("0.00"), std::string::npos) << "a new window must reset:\n" << second; } TEST(RuntimeStatsTest, SameNameReturnsSameId) { RuntimeStats stats(3600.0); EXPECT_EQ(stats.section("loop"), stats.section("loop")); } TEST(RuntimeStatsTest, TickOnlyPrintsWhenPeriodElapsed) { RuntimeStats stats(3600.0); EXPECT_FALSE(stats.tick()) << "printed before the period elapsed"; RuntimeStats fast(0.001); // [s] std::this_thread::sleep_for(std::chrono::milliseconds(5)); EXPECT_TRUE(fast.tick()); EXPECT_FALSE(fast.tick()) << "the window must be reopened right after printing"; } TEST(RuntimeStatsTest, RegisteredThreadAppearsWithCpuTime) { RuntimeStats stats(3600.0); std::atomic registered{ false }; std::atomic stop{ false }; std::thread worker([&stats, ®istered, &stop]() { stats.registerCurrentThread("test/worker"); registered.store(true); while (!stop.load()) { burnCpu(5); } }); while (!registered.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } burnCpu(60); const std::string table = stats.render(); stop.store(true); worker.join(); EXPECT_NE(table.find("test/worker"), std::string::npos) << table; EXPECT_NE(table.find("(unregistered)"), std::string::npos) << "CPU outside the registered threads must show up, otherwise the table hides the culprit:\n" << table; } TEST(RuntimeStatsTest, ThreadCaptureLabelsThreadsCreatedInsideWindow) { RuntimeStats stats(3600.0); std::atomic stop{ false }; std::thread created; stats.beginThreadCapture(); created = std::thread([&stop]() { while (!stop.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } }); // Thread phải thực sự tồn tại trong /proc trước khi đóng cửa sổ chụp. std::this_thread::sleep_for(std::chrono::milliseconds(20)); stats.endThreadCapture("simulated/costmap"); const std::string table = stats.render(); stop.store(true); created.join(); #ifdef __linux__ EXPECT_NE(table.find("simulated/costmap"), std::string::npos) << table; #else GTEST_SKIP() << "thread capture windows rely on /proc, Linux only"; #endif } TEST(RuntimeStatsTest, ScopedSectionToleratesNullStats) { // Đây là đường đi bình thường của mọi test dùng cổng giả: runner không được gắn telemetry. { ScopedSection timer(nullptr, RuntimeStats::kInvalidSection); } RuntimeStats stats(0.0); { ScopedSection timer(&stats, stats.section("skipped")); } SUCCEED(); } } // namespace int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }