#include #include #include #include #include #include #include #include #include #include #include #include "loghandler.h" namespace { std::atomic_int g_previousHandlerCalls{0}; // 测试替代 handler 只计数、不调用 Qt 日志 API,避免断言旧 handler 恢复时自身触发递归输出。 void previousHandler(QtMsgType, const QMessageLogContext &, const QString &) { ++g_previousHandlerCalls; } } // MyLogger 的 QtTest 用例类:每个用例使用独立临时目录和独立全局 handler, // 使进程级 qInstallMessageHandler 状态不会跨用例泄漏或污染开发机实际 logs 目录。 class LogHandlerTest : public QObject { Q_OBJECT private slots: // 套件开始前保存测试进程原有 handler,结束后恢复,避免 QtTest 宿主的日志策略被本模块测试改变。 void initTestCase(); void cleanupTestCase(); // 每条用例前后重置全局 logger、测试覆盖和临时目录,确保用例可独立重复运行。 void init(); void cleanup(); // 覆盖安装成功、重复安装、重复卸载以及安装状态查询。 void installAndUninstallAreIdempotent(); // 覆盖日志目录不可用后的失败返回及恢复目录后可重新安装。 void installFailureCanBeRetried(); // 覆盖安装期间接管输出、卸载后恢复调用前 Qt handler 的公共库契约。 void previousHandlerIsRestored(); // 覆盖 UTF-8 内容、等级和源文件信息已实际写入文件。 void writesUtf8MessageToFile(); // 覆盖多线程写入不会死锁,且每个线程的固定标识都能完整落盘。 void writesFromMultipleThreads(); // 覆盖达到测试阈值后产生连续分卷文件。 void rotatesAtConfiguredSize(); // 覆盖只清理由模块命名规则匹配且超过保留期的日志,不触碰宿主文件。 void removesOnlyExpiredOwnedLogs(); private: // 返回临时目录内模块生成的日志内容,用于在 qCritical 强制 flush 后做精确断言。 QString readAllLogs() const; // 清理前一条用例留下的文件,使当前用例的文件数量和分卷序号只反映自身行为。 void clearTestLogDirectory(); // 保存测试进程原始 handler;临时目录由每条用例创建,生命周期覆盖断言和 cleanup。 QtMessageHandler m_originalHandler = nullptr; QTemporaryDir m_logDirectory; }; // 套件开始时保存 QtTest 进程原有 handler,后续用例可自由替换而不影响测试进程退出后的默认行为。 void LogHandlerTest::initTestCase() { m_originalHandler = qInstallMessageHandler(nullptr); qInstallMessageHandler(m_originalHandler); } // 套件结束时先确保 logger 已释放文件句柄,再清除测试覆盖并恢复原始 handler。 void LogHandlerTest::cleanupTestCase() { LogHandler::Get().uninstallMessageHandler(); LogHandlerTestAccess::reset(); qInstallMessageHandler(m_originalHandler); } // 每条测试开始前创建干净的目录和替代 handler,防止进程级状态从前序用例泄漏。 void LogHandlerTest::init() { QVERIFY(m_logDirectory.isValid()); LogHandler::Get().uninstallMessageHandler(); LogHandlerTestAccess::reset(); clearTestLogDirectory(); LogHandlerTestAccess::setLogDirectory(m_logDirectory.path()); qInstallMessageHandler(previousHandler); g_previousHandlerCalls = 0; } // 每条测试结束后恢复运行时默认状态,保证 QtTest 自身和下一条用例不再受 MyLogger 接管。 void LogHandlerTest::cleanup() { LogHandler::Get().uninstallMessageHandler(); LogHandlerTestAccess::reset(); qInstallMessageHandler(m_originalHandler); } // 验证安装与卸载均幂等,重复调用不会留下错误状态或引发全局 handler 竞争。 void LogHandlerTest::installAndUninstallAreIdempotent() { QVERIFY(LogHandler::Get().installMessageHandler()); QVERIFY(LogHandler::Get().isInstalled()); QVERIFY(LogHandler::Get().installMessageHandler()); LogHandler::Get().uninstallMessageHandler(); QVERIFY(!LogHandler::Get().isInstalled()); LogHandler::Get().uninstallMessageHandler(); } // 用普通文件冒充目录模拟路径不可用,随后切回临时目录验证失败不会永久锁死安装状态。 void LogHandlerTest::installFailureCanBeRetried() { const QString blockedPath = m_logDirectory.filePath(QStringLiteral("not-a-directory")); QFile blockedFile(blockedPath); QVERIFY(blockedFile.open(QIODevice::WriteOnly)); blockedFile.close(); LogHandlerTestAccess::setLogDirectory(blockedPath); QVERIFY(!LogHandler::Get().installMessageHandler()); QVERIFY(!LogHandler::Get().isInstalled()); LogHandlerTestAccess::setLogDirectory(m_logDirectory.path()); QVERIFY(LogHandler::Get().installMessageHandler()); } // 验证安装期间旧 handler 不接收消息,而卸载后立即恢复为当前 Qt 全局处理器。 void LogHandlerTest::previousHandlerIsRestored() { QVERIFY(LogHandler::Get().installMessageHandler()); qInfo() << "while-mylogger-is-installed"; QCOMPARE(g_previousHandlerCalls.load(), 0); LogHandler::Get().uninstallMessageHandler(); qInfo() << "after-mylogger-is-uninstalled"; QCOMPARE(g_previousHandlerCalls.load(), 1); } // 验证 qCritical 立即 flush 后,日志文件保留 UTF-8 中文、等级和源文件上下文。 void LogHandlerTest::writesUtf8MessageToFile() { QVERIFY(LogHandler::Get().installMessageHandler()); qCritical() << QStringLiteral("中文 UTF-8 日志标记"); const QString content = readAllLogs(); QVERIFY(content.contains(QStringLiteral("ERROR"))); QVERIFY(content.contains(QStringLiteral("中文 UTF-8 日志标记"))); QVERIFY(content.contains(QStringLiteral("tst_loghandler.cpp"))); } // 用多个标准线程并发输出固定标识,验证互斥写盘不会死锁或混淆消息内容。 void LogHandlerTest::writesFromMultipleThreads() { QVERIFY(LogHandler::Get().installMessageHandler()); constexpr int threadCount = 4; constexpr int messagesPerThread = 40; std::vector workers; workers.reserve(threadCount); for (int threadIndex = 0; threadIndex < threadCount; ++threadIndex) { workers.emplace_back([threadIndex]() { for (int messageIndex = 0; messageIndex < messagesPerThread; ++messageIndex) { qInfo().noquote() << QStringLiteral("worker-%1-message-%2").arg(threadIndex).arg(messageIndex); } }); } for (std::thread &worker : workers) { worker.join(); } qCritical() << "flush-concurrent-log"; const QString content = readAllLogs(); for (int threadIndex = 0; threadIndex < threadCount; ++threadIndex) { for (int messageIndex = 0; messageIndex < messagesPerThread; ++messageIndex) { QVERIFY(content.contains(QStringLiteral("worker-%1-message-%2").arg(threadIndex).arg(messageIndex))); } } } // 通过测试专用的小阈值触发多次轮转,并确认连续的第一、第二分卷均被创建。 void LogHandlerTest::rotatesAtConfiguredSize() { LogHandlerTestAccess::setMaxLogFileBytes(1024); QVERIFY(LogHandler::Get().installMessageHandler()); const QString payload(300, QLatin1Char('x')); for (int index = 0; index < 10; ++index) { qInfo().noquote() << QStringLiteral("rotate-%1-%2").arg(index).arg(payload); } qCritical() << "flush-rotated-log"; const QStringList files = QDir(m_logDirectory.path()).entryList(QStringList() << QStringLiteral("*.log"), QDir::Files); QVERIFY(files.size() >= 2); QVERIFY(!files.filter(QRegularExpression(QStringLiteral("_001\\.log$"))).isEmpty()); QVERIFY(!files.filter(QRegularExpression(QStringLiteral("_002\\.log$"))).isEmpty()); } // 验证清理策略只处理超过保留期且符合 MyLogger 命名约定的文件,宿主业务日志必须保留。 void LogHandlerTest::removesOnlyExpiredOwnedLogs() { const QString oldOwnedName = QStringLiteral("%1_%2_001.log") .arg(QDate::currentDate().addDays(-31).toString(QStringLiteral("yyyy-MM-dd"))) .arg(QCoreApplication::applicationPid()); const QString oldOwnedPath = m_logDirectory.filePath(oldOwnedName); const QString hostLogPath = m_logDirectory.filePath(QStringLiteral("host-business.log")); QFile oldOwnedFile(oldOwnedPath); QVERIFY(oldOwnedFile.open(QIODevice::WriteOnly)); oldOwnedFile.close(); QFile hostLogFile(hostLogPath); QVERIFY(hostLogFile.open(QIODevice::WriteOnly)); hostLogFile.close(); QVERIFY(LogHandler::Get().installMessageHandler()); QVERIFY(!QFileInfo::exists(oldOwnedPath)); QVERIFY(QFileInfo::exists(hostLogPath)); } // 汇总当前用例生成的 UTF-8 日志,避免依赖单一分卷名称或文件系统遍历顺序。 QString LogHandlerTest::readAllLogs() const { QString content; const QFileInfoList files = QDir(m_logDirectory.path()).entryInfoList(QStringList() << QStringLiteral("*.log"), QDir::Files); for (const QFileInfo &fileInfo : files) { QFile file(fileInfo.absoluteFilePath()); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { content += QString::fromUtf8(file.readAll()); } } return content; } // 临时目录仅允许本测试创建普通文件;每条用例前删除它们可消除前序分卷对后续断言的影响。 void LogHandlerTest::clearTestLogDirectory() { const QFileInfoList entries = QDir(m_logDirectory.path()).entryInfoList(QDir::Files | QDir::NoDotAndDotDot); for (const QFileInfo &entry : entries) { QVERIFY2(QFile::remove(entry.absoluteFilePath()), qPrintable(entry.absoluteFilePath())); } } QTEST_MAIN(LogHandlerTest) #include "tst_loghandler.moc"