feat: harden logger lifecycle and add tests
This commit is contained in:
+395
-118
@@ -1,157 +1,434 @@
|
||||
#include "loghandler.h"
|
||||
#include <iostream>
|
||||
#include <QDebug>
|
||||
#include "loghandler.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDate>
|
||||
#include <QDateTime>
|
||||
#include <QMutexLocker>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QRegularExpression>
|
||||
#include <QScopedValueRollback>
|
||||
#include <QTextStream>
|
||||
#include <QCoreApplication>
|
||||
|
||||
QMutex g_mutex;
|
||||
QFile g_file;
|
||||
QTextStream g_stream;
|
||||
qint64 g_lastFlushMs = 0;
|
||||
// Qt6 将 QTextStream 编码接口改为 QStringConverter 枚举;Qt5 分支继续使用 setCodec,
|
||||
// 条件包含避免 Qt5 公用库因为不存在该头文件而无法构建。
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
#include <QStringConverter>
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr qint64 kLogFlushIntervalMs = 1000;
|
||||
constexpr qint64 kMaxLogFileBytes = 10LL * 1024 * 1024;
|
||||
constexpr int kLogRetentionDays = 30;
|
||||
|
||||
// 消息处理函数
|
||||
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
// 日志运行状态集中在一个私有对象中,所有文件访问和安装状态均受同一把锁保护,
|
||||
// 使重复安装、退出期卸载和后台线程同时输出日志时不会访问到半关闭的 QTextStream。
|
||||
struct LogState
|
||||
{
|
||||
QMutexLocker locker(&g_mutex);
|
||||
QString level, colorLevel;
|
||||
QMutex mutex;
|
||||
QFile file;
|
||||
QTextStream stream;
|
||||
QtMessageHandler previousHandler = nullptr;
|
||||
qint64 lastFlushMs = 0;
|
||||
qint64 currentFileBytes = 0;
|
||||
int currentPart = 0;
|
||||
QDate activeDate;
|
||||
bool installed = false;
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
// 测试编译时将文件系统副作用收敛到临时目录,并用小阈值快速覆盖分卷逻辑。
|
||||
QString testLogDirectory;
|
||||
qint64 testMaxLogFileBytes = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case QtDebugMsg:
|
||||
level = "DEBUG";
|
||||
colorLevel = "\033[36m" + level + "\033[0m"; // 青色
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
level = "INFO ";
|
||||
colorLevel = "\033[32m" + level + "\033[0m"; // 绿色
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
level = "WARN ";
|
||||
colorLevel = "\033[33m" + level + "\033[0m"; // 黄色
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
level = "ERROR";
|
||||
colorLevel = "\033[31m" + level + "\033[0m"; // 红色
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
level = "FATAL";
|
||||
colorLevel = "\033[31m" + level + "\033[0m"; // 红色
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
LogState g_logState;
|
||||
|
||||
// 输出到日志文件, 格式: 时间 - [Level] (文件名:行数, 函数): 消息
|
||||
QString fileName = context.file ? QString::fromUtf8(context.file) : QStringLiteral("?");
|
||||
int index = fileName.lastIndexOf(QDir::separator());
|
||||
if (index >= 0)
|
||||
{
|
||||
fileName = fileName.mid(index + 1);
|
||||
}
|
||||
|
||||
const QString funcName = context.function ? QString::fromUtf8(context.function) : QStringLiteral("?");
|
||||
const QString lineText = QString::number(context.line);
|
||||
const QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
|
||||
const QString consoleMsg = QString("%1-[%2](%3:%4,%5): %6")
|
||||
.arg(timestamp, colorLevel, fileName, lineText, funcName, msg);
|
||||
const QString fileMsg = QString("%1-[%2](%3:%4,%5): %6")
|
||||
.arg(timestamp, level, fileName, lineText, funcName, msg);
|
||||
|
||||
std::cout << consoleMsg.toLocal8Bit().constData() << std::endl;
|
||||
|
||||
if (g_file.isOpen())
|
||||
{
|
||||
g_stream << fileMsg << "\n";
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
if (type >= QtCriticalMsg || now - g_lastFlushMs >= kLogFlushIntervalMs)
|
||||
{
|
||||
g_stream.flush();
|
||||
g_file.flush();
|
||||
g_lastFlushMs = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static QString LinuxRuntimeRoot()
|
||||
// Linux 部署包可能由启动脚本指定可写运行根目录,或由 AppImage 挂载在只读位置;
|
||||
// 这里优先复用 LC 已验证的环境变量策略,未提供时保持各平台原有的应用目录回退行为。
|
||||
QString runtimeRootPath()
|
||||
{
|
||||
#ifdef Q_OS_LINUX
|
||||
QByteArray root = qgetenv("LC_APP_ROOT");
|
||||
if(!root.isEmpty())
|
||||
#if defined(Q_OS_LINUX)
|
||||
const QByteArray configuredRoot = qgetenv("LC_APP_ROOT");
|
||||
if (!configuredRoot.isEmpty())
|
||||
{
|
||||
return QDir::cleanPath(QString::fromLocal8Bit(root));
|
||||
return QDir::cleanPath(QString::fromLocal8Bit(configuredRoot));
|
||||
}
|
||||
|
||||
QByteArray appImage = qgetenv("APPIMAGE");
|
||||
if(!appImage.isEmpty())
|
||||
const QByteArray appImagePath = qgetenv("APPIMAGE");
|
||||
if (!appImagePath.isEmpty())
|
||||
{
|
||||
return QFileInfo(QString::fromLocal8Bit(appImage)).absolutePath();
|
||||
return QFileInfo(QString::fromLocal8Bit(appImagePath)).absolutePath();
|
||||
}
|
||||
#endif
|
||||
|
||||
return QCoreApplication::applicationDirPath();
|
||||
}
|
||||
|
||||
// 给Qt安装消息处理函数
|
||||
void LogHandler::installMessageHandler()
|
||||
// 测试覆盖目录优先于部署策略,正式环境则在统一运行根目录下创建 logs,
|
||||
// 从而同时兼容 Windows exe 同级日志、Linux 启动脚本和 AppImage 部署。
|
||||
QString logDirectoryPath()
|
||||
{
|
||||
static bool installFlag = false;
|
||||
if(installFlag)
|
||||
return;
|
||||
installFlag = true;
|
||||
//获取日期
|
||||
auto _currentDate = QDateTime::currentDateTime().toString("yyyy-MM-dd");
|
||||
|
||||
//打开/创建文件
|
||||
#ifdef Q_OS_LINUX
|
||||
auto _appDirPath = LinuxRuntimeRoot();
|
||||
#else
|
||||
auto _appDirPath = QCoreApplication::applicationDirPath();
|
||||
#endif
|
||||
|
||||
auto _logDirPath = _appDirPath + "/logs/";
|
||||
QDir _logDir(_logDirPath);
|
||||
if(!_logDir.exists())
|
||||
_logDir.mkpath(_logDirPath);
|
||||
|
||||
QString _logPath;
|
||||
for(int i = 1; i < 1000; ++i)
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
if (!g_logState.testLogDirectory.isEmpty())
|
||||
{
|
||||
_logPath = _logDirPath + _currentDate + QString("_%1.log").arg(i);
|
||||
QFileInfo _fileInfo(_logPath);
|
||||
if(!_fileInfo.exists())
|
||||
return g_logState.testLogDirectory;
|
||||
}
|
||||
#endif
|
||||
return runtimeRootPath() + QStringLiteral("/logs");
|
||||
}
|
||||
|
||||
// 正式分卷阈值保持 10 MiB,测试仅通过编译期访问类降低阈值以缩短执行时间。
|
||||
qint64 maxLogFileBytes()
|
||||
{
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
if (g_logState.testMaxLogFileBytes > 0)
|
||||
{
|
||||
return g_logState.testMaxLogFileBytes;
|
||||
}
|
||||
#endif
|
||||
return kMaxLogFileBytes;
|
||||
}
|
||||
|
||||
// Qt 内部或文件错误路径可能再次产生日志;同线程递归时不再进入互斥写盘路径,
|
||||
// 防止非递归互斥锁死锁,同时保留最小 stderr 输出用于诊断原始故障。
|
||||
thread_local bool g_isHandlingMessage = false;
|
||||
|
||||
// 仅在标准输出确实连接终端时插入 ANSI 颜色,避免控制字符污染重定向文件和 Windows GUI 进程输出。
|
||||
bool supportsAnsiColor()
|
||||
{
|
||||
#if defined(Q_OS_WIN)
|
||||
return _isatty(_fileno(stdout)) != 0;
|
||||
#else
|
||||
return isatty(fileno(stdout)) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// 将 Qt 日志等级转换为固定宽度文本,未知枚举值也保留可读标识以兼容后续 Qt 扩展。
|
||||
QString levelName(QtMsgType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case QtDebugMsg:
|
||||
return QStringLiteral("DEBUG");
|
||||
case QtInfoMsg:
|
||||
return QStringLiteral("INFO ");
|
||||
case QtWarningMsg:
|
||||
return QStringLiteral("WARN ");
|
||||
case QtCriticalMsg:
|
||||
return QStringLiteral("ERROR");
|
||||
case QtFatalMsg:
|
||||
return QStringLiteral("FATAL");
|
||||
default:
|
||||
return QStringLiteral("UNKWN");
|
||||
}
|
||||
}
|
||||
|
||||
// 控制台颜色仅是交互显示效果,文件日志始终使用无控制字符的等级文本。
|
||||
QString consoleLevel(QtMsgType type, const QString &level)
|
||||
{
|
||||
if (!supportsAnsiColor())
|
||||
{
|
||||
return level;
|
||||
}
|
||||
|
||||
const char *color = "\033[0m";
|
||||
switch (type)
|
||||
{
|
||||
case QtDebugMsg:
|
||||
color = "\033[36m";
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
color = "\033[32m";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
color = "\033[33m";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
case QtFatalMsg:
|
||||
color = "\033[31m";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return QString::fromLatin1(color) + level + QStringLiteral("\033[0m");
|
||||
}
|
||||
|
||||
// 兼容 Windows 反斜杠与编译器可能输出的正斜杠路径,只在日志中保留文件名以控制行宽。
|
||||
QString shortFileName(const QMessageLogContext &context)
|
||||
{
|
||||
const QString fileName = context.file ? QString::fromUtf8(context.file) : QStringLiteral("?");
|
||||
const int separatorIndex = qMax(fileName.lastIndexOf(QLatin1Char('/')),
|
||||
fileName.lastIndexOf(QLatin1Char('\\')));
|
||||
return separatorIndex >= 0 ? fileName.mid(separatorIndex + 1) : fileName;
|
||||
}
|
||||
|
||||
// 仅清理本模块按日期、进程号和分卷号命名的日志,避免公共 logs 目录中的宿主业务文件被误删。
|
||||
void removeExpiredLogs(const QDir &logDir, const QDate &today)
|
||||
{
|
||||
const QRegularExpression logNamePattern(
|
||||
QStringLiteral("^\\d{4}-\\d{2}-\\d{2}_\\d+_\\d{3}\\.log$"));
|
||||
const QDate oldestDate = today.addDays(-kLogRetentionDays);
|
||||
const QFileInfoList files = logDir.entryInfoList(QStringList() << QStringLiteral("*.log"),
|
||||
QDir::Files | QDir::Readable);
|
||||
for (const QFileInfo &fileInfo : files)
|
||||
{
|
||||
if (!logNamePattern.match(fileInfo.fileName()).hasMatch())
|
||||
{
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
const QDate fileDate = QDate::fromString(fileInfo.fileName().left(10), Qt::ISODate);
|
||||
if (fileDate.isValid() && fileDate < oldestDate)
|
||||
{
|
||||
QFile::remove(fileInfo.absoluteFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在锁保护下选择当前进程当天可继续写入的分卷;已满分卷永不复用,防止重启后突破大小上限。
|
||||
bool openCurrentLogFileLocked(LogState &state, const QDir &logDir, const QDate &today)
|
||||
{
|
||||
const QString prefix = QStringLiteral("%1_%2_")
|
||||
.arg(today.toString(QStringLiteral("yyyy-MM-dd")))
|
||||
.arg(QCoreApplication::applicationPid());
|
||||
int part = 1;
|
||||
while (true)
|
||||
{
|
||||
const QString filePath = logDir.filePath(prefix + QStringLiteral("%1.log").arg(part, 3, 10, QLatin1Char('0')));
|
||||
const QFileInfo fileInfo(filePath);
|
||||
if (!fileInfo.exists() || fileInfo.size() < maxLogFileBytes())
|
||||
{
|
||||
state.file.setFileName(filePath);
|
||||
if (!state.file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.stream.setDevice(&state.file);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
state.stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
state.stream.setCodec("UTF-8");
|
||||
#endif
|
||||
state.currentPart = part;
|
||||
// QTextStream 可能缓存尚未落盘的内容,分卷判断必须从已打开文件大小开始累计,
|
||||
// 不能在每次消息时直接依赖 QFile::size(),否则高频日志会在 flush 前突破上限。
|
||||
state.currentFileBytes = state.file.size();
|
||||
state.activeDate = today;
|
||||
return true;
|
||||
}
|
||||
++part;
|
||||
}
|
||||
}
|
||||
|
||||
// 在当前文件即将超过上限时切到下一个空闲分卷;连续序号便于现场按时间顺序定位日志。
|
||||
bool rotateLogFileLocked(LogState &state)
|
||||
{
|
||||
const QFileInfo currentInfo(state.file);
|
||||
const QDir logDir = currentInfo.dir();
|
||||
const QString prefix = QStringLiteral("%1_%2_")
|
||||
.arg(QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd")))
|
||||
.arg(QCoreApplication::applicationPid());
|
||||
int part = state.currentPart + 1;
|
||||
QString filePath;
|
||||
do
|
||||
{
|
||||
filePath = logDir.filePath(prefix + QStringLiteral("%1.log").arg(part, 3, 10, QLatin1Char('0')));
|
||||
++part;
|
||||
} while (QFileInfo::exists(filePath));
|
||||
|
||||
state.stream.flush();
|
||||
state.file.flush();
|
||||
state.file.close();
|
||||
state.stream.setDevice(nullptr);
|
||||
state.file.setFileName(filePath);
|
||||
if (!state.file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.stream.setDevice(&state.file);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
state.stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
state.stream.setCodec("UTF-8");
|
||||
#endif
|
||||
state.currentPart = part - 1;
|
||||
state.currentFileBytes = 0;
|
||||
state.activeDate = QDate::currentDate();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 全局 Qt 回调负责格式化并串行写盘;致命等级只保证最后一次 flush,终止动作仍由 Qt 在回调返回后执行。
|
||||
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
if (g_isHandlingMessage)
|
||||
{
|
||||
const QByteArray recursiveMessage = msg.toLocal8Bit();
|
||||
std::fputs(recursiveMessage.constData(), stderr);
|
||||
std::fputc('\n', stderr);
|
||||
return;
|
||||
}
|
||||
QScopedValueRollback<bool> recursionGuard(g_isHandlingMessage, true);
|
||||
|
||||
const QString level = levelName(type);
|
||||
const QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz"));
|
||||
const QString fileName = shortFileName(context);
|
||||
const QString functionName = context.function ? QString::fromUtf8(context.function) : QStringLiteral("?");
|
||||
const QString fileMessage = QStringLiteral("%1-[%2](%3:%4,%5): %6")
|
||||
.arg(timestamp, level, fileName)
|
||||
.arg(context.line)
|
||||
.arg(functionName, msg);
|
||||
const QString consoleMessage = QStringLiteral("%1-[%2](%3:%4,%5): %6")
|
||||
.arg(timestamp, consoleLevel(type, level), fileName)
|
||||
.arg(context.line)
|
||||
.arg(functionName, msg);
|
||||
|
||||
const QByteArray consoleBytes = consoleMessage.toLocal8Bit();
|
||||
std::fwrite(consoleBytes.constData(), 1, static_cast<size_t>(consoleBytes.size()), stdout);
|
||||
std::fputc('\n', stdout);
|
||||
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
if (!g_logState.installed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 跨午夜时先关闭旧日期文件再按新日期选择分卷;轮转失败后的后续消息也会重试打开,
|
||||
// 这样临时磁盘或权限故障恢复后无需重启应用即可恢复文件日志。
|
||||
const QDate today = QDate::currentDate();
|
||||
if (g_logState.activeDate != today && g_logState.file.isOpen())
|
||||
{
|
||||
g_logState.stream.flush();
|
||||
g_logState.file.flush();
|
||||
g_logState.file.close();
|
||||
g_logState.stream.setDevice(nullptr);
|
||||
g_logState.currentFileBytes = 0;
|
||||
g_logState.currentPart = 0;
|
||||
}
|
||||
if (!g_logState.file.isOpen())
|
||||
{
|
||||
const QDir logDir(logDirectoryPath());
|
||||
if (!openCurrentLogFileLocked(g_logState, logDir, today))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
g_file.setFileName(_logPath);
|
||||
if(!g_file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
|
||||
const qint64 messageBytes = fileMessage.toUtf8().size() + 1;
|
||||
if (g_logState.currentFileBytes + messageBytes > maxLogFileBytes() && !rotateLogFileLocked(g_logState))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
g_stream.setDevice(&g_file);
|
||||
g_stream.setCodec("UTF-8");
|
||||
g_lastFlushMs = QDateTime::currentMSecsSinceEpoch();
|
||||
qInstallMessageHandler(messageHandler); // 给 Qt 安装自定义消息处理函数
|
||||
g_logState.stream << fileMessage << '\n';
|
||||
g_logState.currentFileBytes += messageBytes;
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
if (type == QtCriticalMsg || type == QtFatalMsg || now - g_logState.lastFlushMs >= kLogFlushIntervalMs)
|
||||
{
|
||||
g_logState.stream.flush();
|
||||
g_logState.file.flush();
|
||||
g_logState.lastFlushMs = now;
|
||||
if (type == QtCriticalMsg || type == QtFatalMsg)
|
||||
{
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消安装消息处理函数并释放资源
|
||||
// 安装函数只在文件已成功打开后才接管 Qt 全局 handler,失败时保留宿主原有 handler 并允许后续重试。
|
||||
bool LogHandler::installMessageHandler()
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
if (g_logState.installed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const QDir logDir(logDirectoryPath());
|
||||
if (!logDir.exists() && !QDir().mkpath(logDir.absolutePath()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const QDate today = QDate::currentDate();
|
||||
removeExpiredLogs(logDir, today);
|
||||
if (!openCurrentLogFileLocked(g_logState, logDir, today))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
g_logState.lastFlushMs = QDateTime::currentMSecsSinceEpoch();
|
||||
g_logState.previousHandler = qInstallMessageHandler(messageHandler);
|
||||
g_logState.installed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 卸载函数在锁内先同步关闭流再恢复旧 handler,确保回调并发进入时不会写到已释放的 QTextStream。
|
||||
void LogHandler::uninstallMessageHandler()
|
||||
{
|
||||
QMutexLocker locker(&g_mutex);
|
||||
if(g_file.isOpen())
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
if (!g_logState.installed)
|
||||
{
|
||||
g_stream.flush();
|
||||
g_file.flush();
|
||||
g_file.close();
|
||||
return;
|
||||
}
|
||||
g_stream.setDevice(nullptr);
|
||||
qInstallMessageHandler(nullptr);
|
||||
|
||||
qInstallMessageHandler(g_logState.previousHandler);
|
||||
g_logState.previousHandler = nullptr;
|
||||
if (g_logState.file.isOpen())
|
||||
{
|
||||
g_logState.stream.flush();
|
||||
g_logState.file.flush();
|
||||
g_logState.file.close();
|
||||
}
|
||||
g_logState.stream.setDevice(nullptr);
|
||||
g_logState.currentFileBytes = 0;
|
||||
g_logState.currentPart = 0;
|
||||
g_logState.activeDate = QDate();
|
||||
g_logState.installed = false;
|
||||
}
|
||||
|
||||
// 查询函数与安装/卸载共用互斥锁,避免其他线程在关闭过程看到过期安装状态。
|
||||
bool LogHandler::isInstalled() const
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
return g_logState.installed;
|
||||
}
|
||||
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
// 测试目录设置在同一把锁下更新,并要求 handler 已卸载,保证没有线程持有旧目录的文件句柄。
|
||||
void LogHandlerTestAccess::setLogDirectory(const QString &directory)
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
Q_ASSERT(!g_logState.installed);
|
||||
g_logState.testLogDirectory = directory;
|
||||
}
|
||||
|
||||
// 分卷阈值只影响随后打开的测试文件;非正值表示删除覆盖并回退到正式默认值。
|
||||
void LogHandlerTestAccess::setMaxLogFileBytes(qint64 bytes)
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
Q_ASSERT(!g_logState.installed);
|
||||
g_logState.testMaxLogFileBytes = bytes > 0 ? bytes : 0;
|
||||
}
|
||||
|
||||
// 测试结束后统一清除覆盖状态,避免同一 QtTest 进程中的下一条用例意外复用临时环境。
|
||||
void LogHandlerTestAccess::reset()
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
Q_ASSERT(!g_logState.installed);
|
||||
g_logState.testLogDirectory.clear();
|
||||
g_logState.testMaxLogFileBytes = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user