feat: harden logger lifecycle and add tests

This commit is contained in:
2026-07-13 13:22:10 +08:00
parent 95a8067342
commit 62ed176bdd
8 changed files with 799 additions and 190 deletions
+13
View File
@@ -0,0 +1,13 @@
QT += core testlib
TEMPLATE = app
TARGET = MyLoggerTests
# 测试直接编译被测源码,并显式开启测试访问宏;不会链接或改写正式 MyLogger 动态库
CONFIG += testcase console c++17
DEFINES += MYLOGGER_STATIC MYLOGGER_TESTING QT_MESSAGELOGCONTEXT
INCLUDEPATH += $$PWD/..
HEADERS += $$PWD/../loghandler.h
SOURCES += \
$$PWD/tst_loghandler.cpp \
$$PWD/../loghandler.cpp
+262
View File
@@ -0,0 +1,262 @@
#include <atomic>
#include <thread>
#include <vector>
#include <QDate>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <QTemporaryDir>
#include <QtTest>
#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<std::thread> 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"