263 lines
9.9 KiB
C++
263 lines
9.9 KiB
C++
#include <atomic>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include <QDate>
|
|
#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()
|
|
{
|
|
// 过期文件必须严格符合新命名规则:日期、启动时分秒和分卷号,
|
|
// 才能验证清理逻辑不会把同目录中其他业务日志误认为 MyLogger 文件。
|
|
const QString oldOwnedName = QStringLiteral("%1_000000_001.log")
|
|
.arg(QDate::currentDate().addDays(-31).toString(QStringLiteral("yyyy-MM-dd")));
|
|
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"
|