From 9eed58150c3599855e49fae5804af8840bac0ebc Mon Sep 17 00:00:00 2001 From: pengfeixx <63588879+pengfeixx@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:21:03 +0800 Subject: [PATCH] test: add unit tests for service and main window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add ut_logviewerservice.cpp and ut_org_deepin_log_viewer.cpp 2. Extend ut_logcollectormain.cpp and ut_debugtimemanager.cpp 3. Extend ut_lognormalbutton.cpp including style painting branches Log: No user-facing changes Influence: 1. Run deepin-log-viewer-test with --gtest_filter='*Service*;*Collector*' 2. Verify full suite passes with ASAN_OPTIONS=detect_leaks=0 3. Check lcov function coverage of the new test targets test: 新增服务端与主窗口单元测试 1. 新增 ut_logviewerservice.cpp 与 ut_org_deepin_log_viewer.cpp 2. 扩展 ut_logcollectormain.cpp 和 ut_debugtimemanager.cpp 3. 扩展 ut_lognormalbutton.cpp,含样式绘制分支 Log: 无用户可见变化 Influence: 1. 运行 deepin-log-viewer-test 过滤 Service/Collector 套件 2. 以 ASAN_OPTIONS=detect_leaks=0 验证全量用例通过 3. 用 lcov 检查新增目标的函数覆盖率 --- tests/src/ut_debugtimemanager.cpp | 121 +++- tests/src/ut_logcollectormain.cpp | 210 +++++++ tests/src/ut_lognormalbutton.cpp | 46 +- tests/src/ut_logviewerservice.cpp | 743 +++++++++++++++++++++++++ tests/src/ut_org_deepin_log_viewer.cpp | 345 ++++++++++++ 5 files changed, 1451 insertions(+), 14 deletions(-) create mode 100644 tests/src/ut_logviewerservice.cpp create mode 100644 tests/src/ut_org_deepin_log_viewer.cpp diff --git a/tests/src/ut_debugtimemanager.cpp b/tests/src/ut_debugtimemanager.cpp index 7a0b4a58..64f6ddf4 100644 --- a/tests/src/ut_debugtimemanager.cpp +++ b/tests/src/ut_debugtimemanager.cpp @@ -1,16 +1,125 @@ -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #include "DebugTimeManager.h" #include #include +#include +// --- clock_gettime stub helpers --- +// Controls whether the stubbed clock_gettime reports failure. +static int g_ut_clock_fail = 0; +static int ut_stub_clock_gettime(clockid_t, struct timespec *tp) +{ + if (g_ut_clock_fail) { + return -1; + } + tp->tv_sec = 100; + tp->tv_nsec = 200; + return 0; +} + +// getInstance: returns the same non-null singleton each call. +TEST(UT_DebugTimeManager, getInstance_returnsSameSingleton) +{ + DebugTimeManager *first = DebugTimeManager::getInstance(); + DebugTimeManager *second = DebugTimeManager::getInstance(); + ASSERT_NE(first, nullptr); + EXPECT_EQ(first, second); +} + +// Protected constructor (accessible via -fno-access-control): constructs without crash. +TEST(UT_DebugTimeManager, constructor_constructsInstance) +{ + DebugTimeManager *manager = new DebugTimeManager(); + ASSERT_NE(manager, nullptr); + manager->clear(); + delete manager; +} + +// clear: removes entries previously inserted into the linux point map. +TEST(UT_DebugTimeManager, clear_emptiesPointMaps) +{ + DebugTimeManager *manager = new DebugTimeManager(); + manager->beginPointLinux("clearPoint", "clearStatus"); + ASSERT_TRUE(manager->m_MapLinuxPoint.contains("clearPoint")); + manager->clear(); + EXPECT_TRUE(manager->m_MapLinuxPoint.isEmpty()); + EXPECT_TRUE(manager->m_MapPoint.isEmpty()); + delete manager; +} + +// beginPointLinux: success path inserts the point with the given status. +TEST(UT_DebugTimeManager, beginPointLinux_insertsPointOnSuccess) +{ + DebugTimeManager *manager = new DebugTimeManager(); + manager->beginPointLinux("beginOk", "okStatus"); + EXPECT_TRUE(manager->m_MapLinuxPoint.contains("beginOk")); + EXPECT_EQ(manager->m_MapLinuxPoint.value("beginOk").desc, QString("okStatus")); + delete manager; +} + +// beginPointLinux: clock_gettime failure path skips insertion. +TEST(UT_DebugTimeManager, beginPointLinux_skipsInsertionOnClockFail) +{ + Stub stub; + g_ut_clock_fail = 1; + stub.set(clock_gettime, ut_stub_clock_gettime); + DebugTimeManager *manager = new DebugTimeManager(); + manager->beginPointLinux("beginFail", "failStatus"); + EXPECT_FALSE(manager->m_MapLinuxPoint.contains("beginFail")); + delete manager; + g_ut_clock_fail = 0; +} + +// endPointLinux: normal path removes the previously begun point. +TEST(UT_DebugTimeManager, endPointLinux_removesPointAfterBegin) +{ + DebugTimeManager *manager = new DebugTimeManager(); + manager->beginPointLinux("endOk", "endStatus"); + ASSERT_TRUE(manager->m_MapLinuxPoint.contains("endOk")); + manager->endPointLinux("endOk", "finished"); + EXPECT_FALSE(manager->m_MapLinuxPoint.contains("endOk")); + delete manager; +} + +// endPointLinux: unknown point is a no-op (no crash, nothing inserted). +TEST(UT_DebugTimeManager, endPointLinux_unknownPointIsNoop) +{ + DebugTimeManager *manager = new DebugTimeManager(); + manager->endPointLinux("notBegun", "status"); + EXPECT_FALSE(manager->m_MapLinuxPoint.contains("notBegun")); + delete manager; +} + +// endPointLinux: clock_gettime failure keeps the point (not removed). +TEST(UT_DebugTimeManager, endPointLinux_keepsPointOnClockFail) +{ + DebugTimeManager *manager = new DebugTimeManager(); + manager->beginPointLinux("endFail", "endFailStatus"); + ASSERT_TRUE(manager->m_MapLinuxPoint.contains("endFail")); + Stub stub; + g_ut_clock_fail = 1; + stub.set(clock_gettime, ut_stub_clock_gettime); + manager->endPointLinux("endFail", "finished"); + EXPECT_TRUE(manager->m_MapLinuxPoint.contains("endFail")); + g_ut_clock_fail = 0; + delete manager; +} -TEST(UT_DebugTimeManager_clear, UT_DebugTimeManager_clear_001) +// diff: computes the difference between two timespecs. +TEST(UT_DebugTimeManager, diff_computesDifference) { - DebugTimeManager *Dtime = new DebugTimeManager(); - ASSERT_TRUE(Dtime); - Dtime->clear(); - delete Dtime; + DebugTimeManager *manager = new DebugTimeManager(); + timespec start; + start.tv_sec = 10; + start.tv_nsec = 100; + timespec end; + end.tv_sec = 35; + end.tv_nsec = 500; + timespec result = manager->diff(start, end); + EXPECT_EQ(result.tv_sec, 25); + EXPECT_EQ(result.tv_nsec, 400); + delete manager; } diff --git a/tests/src/ut_logcollectormain.cpp b/tests/src/ut_logcollectormain.cpp index f14480dd..e7588fca 100644 --- a/tests/src/ut_logcollectormain.cpp +++ b/tests/src/ut_logcollectormain.cpp @@ -14,11 +14,16 @@ #include #include #include +#include #include #include #include #include +#include +#include "dbusmanager.h" +#include "dbusproxy/dldbushandler.h" +#include "logallexportthread.h" #include @@ -412,3 +417,208 @@ TEST(LogCollectorMain_closeEvent_UT, LogCollectorMain_closeEvent_UT_001) } + +// ===== Tests added to cover lambda closures (target: 100% function coverage) ===== + +#include +#include +#include +#include +#include +#include +#include + +// Stub: LogAllExportThread::run — emit signals to trigger the lambdas connected in exportAllLogs +void LogAllExportThread_run_stub(LogAllExportThread *self) +{ + self->updateTolProcess(100); + self->updatecurrentProcess(50); + self->exportFinsh(true); +} + +// Stub: DLDBusHandler::whiteListOutPaths — return non-empty list so export path check passes +QStringList DLDBusHandler_whiteListOutPaths_stub() +{ + return QStringList() << "." << "/" << QDir::homePath(); +} + +// Stub: DBusManager::isSEOpen +bool DBusManager_isSEOpen_stub() +{ + return false; +} + +// Stub: DBusManager::isAuditAdmin +bool DBusManager_isAuditAdmin_stub() +{ + return false; +} + +// Stub: DBusManager::isScreenLocked — return false so refresh lambda else-branch fires +bool DBusManager_isScreenLocked_false() +{ + return false; +} + +// Stub: DDialog::exec that processes events so queued lambdas can fire +int LogCollectorMain_exec_processEvents(DDialog *self) +{ + Q_UNUSED(self) + // Process events for a short time to let thread signals fire + QTest::qWait(500); + return 1; +} + +// ===== initShortCut lambda coverage ===== +TEST(LogCollectorMain_initShortCut_UT, LogCollectorMain_initShortCut_UT_003) +{ + // Set DStyle so DApplication::style() returns a valid DStyle* — prevents SEGV + // in LogPeriodButton::paintEvent when showMaximized()/showNormal() triggers repaints + qApp->setStyle(new DStyle); + Stub stub; + stub.set(ADDR(DebugTimeManager, beginPointLinux), Log_beginPointLinux); + stub.set(ADDR(LogFileParser, parseByJournal), LogFileParser_parseByJournal); + stub.set(ADDR(LogFileParser, parseByJournalBoot), LogFileParser_parseByJournalBoot); + stub.set(ADDR(LogFileParser, parseByDpkg), LogFileParser_parseByDpkg); + stub.set(ADDR(LogFileParser, parseByXlog), LogFileParser_parseByXlog); + stub.set(ADDR(LogFileParser, parseByBoot), LogFileParser_parseByBoot); + stub.set(ADDR(LogFileParser, parseByKern), LogFileParser_parseByKern); + stub.set(ADDR(LogFileParser, parseByApp), LogFileParser_parseByApp); + stub.set(ADDR(LogFileParser, parseByNormal), LogFileParser_parseByNormal); + stub.set(ADDR(LogFileParser, parseByKwin), LogFileParser_parseByKwin); + stub.set(ADDR(LogApplicationHelper, getCustomLogList), LogApplicationHelper_getCustomLogList); + LogCollectorMain *p = new LogCollectorMain(nullptr); + ASSERT_TRUE(p); + + // Emit the activated signal on each shortcut to invoke the lambdas + if (p->m_scWndReize) { + QMetaObject::invokeMethod(p->m_scWndReize, "activated"); + } + if (p->m_scFindFont) { + QMetaObject::invokeMethod(p->m_scFindFont, "activated"); + } + if (p->m_scExport) { + QMetaObject::invokeMethod(p->m_scExport, "activated"); + } + // Process events so signal lambdas execute + QTest::qWait(100); + + p->deleteLater(); +} + +// ===== initTitlebarExtensions lambda coverage ===== +TEST(LogCollectorMain_initTitlebarExtensions_UT, LogCollectorMain_initTitlebarExtensions_UT_002) +{ + Stub stub; + stub.set(ADDR(DebugTimeManager, beginPointLinux), Log_beginPointLinux); + stub.set(ADDR(LogFileParser, parseByJournal), LogFileParser_parseByJournal); + stub.set(ADDR(LogFileParser, parseByJournalBoot), LogFileParser_parseByJournalBoot); + stub.set(ADDR(LogFileParser, parseByDpkg), LogFileParser_parseByDpkg); + stub.set(ADDR(LogFileParser, parseByXlog), LogFileParser_parseByXlog); + stub.set(ADDR(LogFileParser, parseByBoot), LogFileParser_parseByBoot); + stub.set(ADDR(LogFileParser, parseByKern), LogFileParser_parseByKern); + stub.set(ADDR(LogFileParser, parseByApp), LogFileParser_parseByApp); + stub.set(ADDR(LogFileParser, parseByNormal), LogFileParser_parseByNormal); + stub.set(ADDR(LogFileParser, parseByKwin), LogFileParser_parseByKwin); + stub.set(ADDR(LogApplicationHelper, getCustomLogList), LogApplicationHelper_getCustomLogList); + typedef bool (*fptr)(LogApplication *); + fptr test = (fptr)(&LogApplication::notify); + stub.set(test, LogCollectorMain_notify); + LogCollectorMain *p = new LogCollectorMain(nullptr); + ASSERT_TRUE(p); + + // Click the refresh button to trigger the clicked lambda + if (p->m_refreshBtn) { + p->m_refreshBtn->click(); + } + QTest::qWait(100); + + p->deleteLater(); +} + +// ===== switchRefreshActionTriggered lambda coverage ===== +TEST(LogCollectorMain_switchRefreshActionTriggered_UT, LogCollectorMain_switchRefreshActionTriggered_UT_002) +{ + Stub stub; + stub.set(ADDR(DebugTimeManager, beginPointLinux), Log_beginPointLinux); + stub.set(ADDR(LogFileParser, parseByJournal), LogFileParser_parseByJournal); + stub.set(ADDR(LogFileParser, parseByJournalBoot), LogFileParser_parseByJournalBoot); + stub.set(ADDR(LogFileParser, parseByDpkg), LogFileParser_parseByDpkg); + stub.set(ADDR(LogFileParser, parseByXlog), LogFileParser_parseByXlog); + stub.set(ADDR(LogFileParser, parseByBoot), LogFileParser_parseByBoot); + stub.set(ADDR(LogFileParser, parseByKern), LogFileParser_parseByKern); + stub.set(ADDR(LogFileParser, parseByApp), LogFileParser_parseByApp); + stub.set(ADDR(LogFileParser, parseByNormal), LogFileParser_parseByNormal); + stub.set(ADDR(LogFileParser, parseByKwin), LogFileParser_parseByKwin); + stub.set(ADDR(LogApplicationHelper, getCustomLogList), LogApplicationHelper_getCustomLogList); + typedef bool (*fptr)(LogApplication *); + fptr test = (fptr)(&LogApplication::notify); + stub.set(test, LogCollectorMain_notify); + // Stub DBusManager::isScreenLocked so QtConcurrent::run completes quickly + stub.set(ADDR(DBusManager, isScreenLocked), DBusManager_isScreenLocked_false); + LogCollectorMain *p = new LogCollectorMain(nullptr); + ASSERT_TRUE(p); + + // Use the actual refresh action at index 0 ("10 sec") so indexOf returns 0 + // and timeInterval = 10*1000 > 0, which creates the timer. + if (!p->m_refreshActions.isEmpty()) { + p->switchRefreshActionTriggered(p->m_refreshActions[0]); + } + + // Now trigger the timer timeout to invoke the outer lambda + if (p->m_refreshTimer) { + p->m_refreshTimer->stop(); + // Emit timeout signal to invoke the lambda connected in switchRefreshActionTriggered + QMetaObject::invokeMethod(p->m_refreshTimer, "timeout"); + // Wait for QtConcurrent::run(DBusManager::isScreenLocked) to complete + // and the QFutureWatcher::finished lambda to fire + QTest::qWait(2000); + } + p->deleteLater(); +} + +// ===== exportAllLogs lambda coverage ===== +TEST(LogCollectorMain_exportAllLogs_UT, LogCollectorMain_exportAllLogs_UT_003) +{ + Stub stub; + stub.set(ADDR(DebugTimeManager, beginPointLinux), Log_beginPointLinux); + stub.set(ADDR(LogFileParser, parseByJournal), LogFileParser_parseByJournal); + stub.set(ADDR(LogFileParser, parseByJournalBoot), LogFileParser_parseByJournalBoot); + stub.set(ADDR(LogFileParser, parseByDpkg), LogFileParser_parseByDpkg); + stub.set(ADDR(LogFileParser, parseByXlog), LogFileParser_parseByXlog); + stub.set(ADDR(LogFileParser, parseByBoot), LogFileParser_parseByBoot); + stub.set(ADDR(LogFileParser, parseByKern), LogFileParser_parseByKern); + stub.set(ADDR(LogFileParser, parseByApp), LogFileParser_parseByApp); + stub.set(ADDR(LogFileParser, parseByNormal), LogFileParser_parseByNormal); + stub.set(ADDR(LogFileParser, parseByKwin), LogFileParser_parseByKwin); + stub.set(ADDR(LogApplicationHelper, getCustomLogList), LogApplicationHelper_getCustomLogList); + typedef bool (*fptr)(LogApplication *); + fptr test = (fptr)(&LogApplication::notify); + stub.set(test, LogCollectorMain_notify); + LogCollectorMain *p = new LogCollectorMain(nullptr); + ASSERT_TRUE(p); + + // Authorization: use true so auth passes (static bool already set true by UT_002, + // but set it anyway for independence) + stub.set(ADDR(Utils, checkAuthorization), LogCollectorMain_checkAuthorization_true); + // File dialog returns a path + stub.set(ADDR(QFileDialog, getSaveFileName), LogCollectorMain_QString); + // Whitelist returns non-empty so path check passes + stub.set(ADDR(DLDBusHandler, whiteListOutPaths), DLDBusHandler_whiteListOutPaths_stub); + // DBus checks stubbed to avoid hangs + stub.set(ADDR(DBusManager, isSEOpen), DBusManager_isSEOpen_stub); + stub.set(ADDR(DBusManager, isAuditAdmin), DBusManager_isAuditAdmin_stub); + // DDialog::exec processes events so thread signals fire the lambdas + typedef int (*fptr2)(DDialog *); + fptr2 test1 = (fptr2)(&DDialog::exec); + stub.set(test1, LogCollectorMain_exec_processEvents); + // Stub LogAllExportThread::run to emit the 3 signals so the connected lambdas fire + typedef void (*fptr_run)(LogAllExportThread *); + fptr_run run_ptr = (fptr_run)(&LogAllExportThread::run); + stub.set(run_ptr, LogAllExportThread_run_stub); + + p->exportAllLogs(); + QTest::qWait(500); + + p->deleteLater(); +} diff --git a/tests/src/ut_lognormalbutton.cpp b/tests/src/ut_lognormalbutton.cpp index 2dca63f7..23302878 100644 --- a/tests/src/ut_lognormalbutton.cpp +++ b/tests/src/ut_lognormalbutton.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019 - 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2019-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -10,6 +10,11 @@ #include #include #include +#include +#include +#include + +DWIDGET_USE_NAMESPACE TEST(LogNormalButton_Constructor_UT, LogNormalButton_Constructor_UT_001) { LogNormalButton *p = new LogNormalButton(nullptr); @@ -88,13 +93,38 @@ TEST_P(LogNormalButton_keyReleaseEvent_UT, LogNormalButton_keyReleaseEvent_UT_00 p->deleteLater(); } -//TEST(LogNormalButton_paintEvent_UT, LogNormalButton_paintEventr_UT) -//{ -// LogNormalButton *p = new LogNormalButton("aa", nullptr); -// EXPECT_NE(p, nullptr); -// p->paintEvent(new QPaintEvent(p->rect())); -// p->deleteLater(); -//} +bool stub_lognormalbutton_hasFocus() +{ + return true; +} + +TEST(LogNormalButton_paintEvent_UT, LogNormalButton_paintEvent_UT_001) +{ + LogNormalButton *p = new LogNormalButton("aa", nullptr); + EXPECT_NE(p, nullptr); + // paintEvent uses dynamic_cast(DApplication::style()); + // ensure the app style is DStyle so the cast succeeds + QApplication::setStyle(new DStyle); + QPaintEvent *e = new QPaintEvent(p->rect()); + p->paintEvent(e); + delete e; + p->deleteLater(); +} + +TEST(LogNormalButton_paintEvent_UT, LogNormalButton_paintEvent_UT_002) +{ + LogNormalButton *p = new LogNormalButton("aa", nullptr); + EXPECT_NE(p, nullptr); + QApplication::setStyle(new DStyle); + // Cover the focus-rect branch: set focus reason to Tab and stub hasFocus + p->m_reson = Qt::TabFocusReason; + Stub stub; + stub.set(ADDR(QWidget, hasFocus), stub_lognormalbutton_hasFocus); + QPaintEvent *e = new QPaintEvent(p->rect()); + p->paintEvent(e); + delete e; + p->deleteLater(); +} class LogNormalButton_focusInEvent_UT_Param { diff --git a/tests/src/ut_logviewerservice.cpp b/tests/src/ut_logviewerservice.cpp new file mode 100644 index 00000000..e40baf6c --- /dev/null +++ b/tests/src/ut_logviewerservice.cpp @@ -0,0 +1,743 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logviewerservice.h" +#include "stub.h" + +// 声明 logviewerservice.cpp 中具有外部链接的自由函数(未在头文件导出) +extern void removeDirFiles(const QString &dirPath); +extern QString unzipToTempFile(const QString &sourceFile, const QString &tempFileTemplate); + +// ===================== 打桩函数 ===================== +static bool stub_checkAuth_true(const QString &) { return true; } +static bool stub_checkAuth_false(const QString &) { return false; } +static bool stub_checkAuthorization_true(const QString &) { return true; } +static bool stub_checkAuthorization_false(const QString &) { return false; } +static QDBusMessage stub_dbus_message_valid() { return QDBusMessage::createSignal("/", "fake.interface", "signal"); } +static PolkitQt1::Authority::Result stub_checkAuthSync_yes(const QString &, const PolkitQt1::Subject &, PolkitQt1::Authority::AuthorizationFlags) { return PolkitQt1::Authority::Yes; } + +// ===================== 测试夹具 ===================== +class LogViewerServiceTest : public ::testing::Test +{ +protected: + LogViewerService *m_service = nullptr; + void SetUp() override + { + m_service = new LogViewerService(); + } + void TearDown() override + { + delete m_service; + m_service = nullptr; + } +}; + +// ===================== 构造与析构 ===================== +TEST_F(LogViewerServiceTest, ConstructorAndDestructor) +{ + SUCCEED(); +} + +// ===================== 自由函数 removeDirFiles ===================== +TEST_F(LogViewerServiceTest, RemoveDirFiles) +{ + QTemporaryDir tmpDir; + ASSERT_TRUE(tmpDir.isValid()); + QString dirPath = tmpDir.path(); + QFile f1(dirPath + "/a.txt"); + ASSERT_TRUE(f1.open(QIODevice::WriteOnly)); + f1.write("hello"); + f1.close(); + QFile f2(dirPath + "/b.txt"); + ASSERT_TRUE(f2.open(QIODevice::WriteOnly)); + f2.write("world"); + f2.close(); + + removeDirFiles(dirPath); + QDir dir(dirPath); + dir.setFilter(QDir::NoDotAndDotDot | QDir::Files); + EXPECT_EQ(dir.entryList().size(), 0); +} + +TEST_F(LogViewerServiceTest, RemoveDirFilesNonExistent) +{ + removeDirFiles("/tmp/non-existent-dir-12345"); + SUCCEED(); +} + +// ===================== 自由函数 unzipToTempFile ===================== +TEST_F(LogViewerServiceTest, UnzipToTempFile) +{ + QTemporaryDir tmpDir; + ASSERT_TRUE(tmpDir.isValid()); + QString gzPath = tmpDir.path() + "/test.txt.gz"; + QString rawPath = tmpDir.path() + "/test.txt"; + { + QFile raw(rawPath); + ASSERT_TRUE(raw.open(QIODevice::WriteOnly)); + raw.write("gunzip content\n"); + raw.close(); + } + QProcess gzip; + gzip.start("gzip", QStringList() << rawPath); + ASSERT_TRUE(gzip.waitForFinished(10000)); + ASSERT_TRUE(QFile::exists(gzPath)); + + QString tmpl = tmpDir.path() + "/unzip_XXXXXX.txt"; + QString result = unzipToTempFile(gzPath, tmpl); + QFile out(result); + ASSERT_TRUE(out.open(QIODevice::ReadOnly)); + EXPECT_TRUE(out.readAll().contains("gunzip content")); + out.close(); + QFile::remove(result); +} + +TEST_F(LogViewerServiceTest, UnzipToTempFileInvalidSource) +{ + QTemporaryDir tmpDir; + ASSERT_TRUE(tmpDir.isValid()); + QString tmpl = tmpDir.path() + "/unzip_XXXXXX.txt"; + QString result = unzipToTempFile("/tmp/non-existent-source.gz", tmpl); + EXPECT_TRUE(result.isEmpty() || QFile::exists(result)); + if (!result.isEmpty()) + QFile::remove(result); +} + +// ===================== checkAuth(私有,直接调用) ===================== +TEST_F(LogViewerServiceTest, CheckAuthNotFromDBus) +{ + EXPECT_FALSE(m_service->checkAuth("com.deepin.pkexec.logViewerAuth")); +} + +// ===================== checkAuthorization(私有) ===================== +TEST_F(LogViewerServiceTest, CheckAuthorizationStubbedTrue) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuthorization), stub_checkAuthorization_true); + EXPECT_TRUE(m_service->checkAuthorization("any")); +} + +TEST_F(LogViewerServiceTest, CheckAuthorizationWithStubbedMessage) +{ + Stub stub; + stub.set(ADDR(QDBusContext, message), stub_dbus_message_valid); + stub.set(ADDR(PolkitQt1::Authority, checkAuthorizationSync), stub_checkAuthSync_yes); + EXPECT_TRUE(m_service->checkAuthorization("com.deepin.pkexec.logViewerAuth")); +} + +// ===================== trackCurrentCaller(私有) ===================== +TEST_F(LogViewerServiceTest, TrackCurrentCallerNotFromDBus) +{ + m_service->trackCurrentCaller(); + SUCCEED(); +} + +// ===================== onClientUnregistered(私有) ===================== +TEST_F(LogViewerServiceTest, OnClientUnregistered) +{ + m_service->onClientUnregistered(":1.999"); + SUCCEED(); +} + +// ===================== getCallerHomeDir(私有) ===================== +TEST_F(LogViewerServiceTest, GetCallerHomeDirNotFromDBus) +{ + EXPECT_TRUE(m_service->getCallerHomeDir().isEmpty()); +} + +// ===================== clearTempFiles(私有) ===================== +TEST_F(LogViewerServiceTest, ClearTempFiles) +{ + QString tmpPath = QDir::tempPath(); + QFile f(tmpPath + "/ut_test.lz4.dump"); + f.open(QIODevice::WriteOnly); + f.write("dummy"); + f.close(); + m_service->clearTempFiles(); + EXPECT_FALSE(QFile::exists(tmpPath + "/ut_test.lz4.dump")); +} + +// ===================== findLineStartOffsetWithCaching(私有) ===================== +TEST_F(LogViewerServiceTest, FindLineStartOffsetWithCaching) +{ + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("line1\nline2\nline3\n"); + tmpFile.flush(); + + qint64 off0 = m_service->findLineStartOffsetWithCaching(tmpFile.fileName(), 0); + EXPECT_GE(off0, 0); + qint64 off1 = m_service->findLineStartOffsetWithCaching(tmpFile.fileName(), 1); + EXPECT_GT(off1, off0); + qint64 off2 = m_service->findLineStartOffsetWithCaching(tmpFile.fileName(), 2); + EXPECT_GT(off2, off1); +} + +TEST_F(LogViewerServiceTest, FindLineStartOffsetWithCachingInvalidFile) +{ + EXPECT_EQ(m_service->findLineStartOffsetWithCaching("/tmp/non-existent-file-xyz", 0), -1); +} + +// ===================== removeOpsTempDirByPathInternal(私有) ===================== +TEST_F(LogViewerServiceTest, RemoveOpsTempDirByPathInternalWrongPrefix) +{ + EXPECT_FALSE(m_service->removeOpsTempDirByPathInternal("/tmp/wrong-prefix-123")); +} + +TEST_F(LogViewerServiceTest, RemoveOpsTempDirByPathInternalNonExistentValidPrefix) +{ + // 合法前缀但目标不存在:open /var/log 成功,safeRemoveDirRecursive 被进入, + // openat 失败 -> unlinkat 失败 -> 返回 false,函数体全部覆盖。 + EXPECT_FALSE(m_service->removeOpsTempDirByPathInternal( + "/var/log/deepin-log-viewer-ops-log.nonexistent-ut-dir")); +} + +// ===================== safeRemoveDirRecursive(私有,经 removeOpsTempDirByPathInternal 间接覆盖) ===================== +// 已由 RemoveOpsTempDirByPathInternalNonExistentValidPrefix 覆盖进入。 + +// ===================== readLog(QString) 私有 ===================== +TEST_F(LogViewerServiceTest, ReadLogByPathAuthFail) +{ + EXPECT_EQ(m_service->readLog(QString("/tmp/test")), QString(" ")); +} + +TEST_F(LogViewerServiceTest, ReadLogByPathAuthOk) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("log content line\n"); + tmpFile.flush(); + // QTemporaryFile 默认路径在 /tmp 下 + QString result = m_service->readLog(tmpFile.fileName()); + EXPECT_TRUE(result.contains("log content line")); +} + +TEST_F(LogViewerServiceTest, ReadLogByPathNotInWhitelist) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + EXPECT_EQ(m_service->readLog(QString("/home/user/secret")), QString(" ")); +} + +TEST_F(LogViewerServiceTest, ReadLogByPathContainsDotDot) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + EXPECT_EQ(m_service->readLog(QString("/tmp/../etc/passwd")), QString(" ")); +} + +// ===================== readLog(fd) 公开槽 ===================== +TEST_F(LogViewerServiceTest, ReadLogByFdAuthFail) +{ + QTemporaryFile pathFile; + ASSERT_TRUE(pathFile.open()); + pathFile.write("/tmp/whatever"); + pathFile.flush(); + QDBusUnixFileDescriptor fd(pathFile.handle()); + QString result = m_service->readLog(fd); + EXPECT_EQ(result, " "); +} + +TEST_F(LogViewerServiceTest, ReadLogByFdAuthOk) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + + QTemporaryFile logFile; + ASSERT_TRUE(logFile.open()); + logFile.write("fd log content\n"); + logFile.flush(); + + QTemporaryFile pathFile; + ASSERT_TRUE(pathFile.open()); + pathFile.write(logFile.fileName().toUtf8()); + pathFile.flush(); + pathFile.seek(0); + + QDBusUnixFileDescriptor fd(pathFile.handle()); + QString result = m_service->readLog(fd); + EXPECT_TRUE(result.contains("fd log content")); +} + +TEST_F(LogViewerServiceTest, ReadLogByFdInvalidFd) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QDBusUnixFileDescriptor fd(-1); + QString result = m_service->readLog(fd); + EXPECT_TRUE(result.isEmpty()); +} + +// ===================== readLogLinesInRange(QString) 私有 ===================== +TEST_F(LogViewerServiceTest, ReadLogLinesInRangeByPathAuthFail) +{ + QStringList result = m_service->readLogLinesInRange(QString("/tmp/x"), 0, 1, false); + EXPECT_TRUE(result.isEmpty()); +} + +TEST_F(LogViewerServiceTest, ReadLogLinesInRangeByPathAuthOk) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("l1\nl2\nl3\nl4\nl5\n"); + tmpFile.flush(); + + QStringList result = m_service->readLogLinesInRange(tmpFile.fileName(), 0, 3, false); + EXPECT_GE(result.size(), 1); +} + +TEST_F(LogViewerServiceTest, ReadLogLinesInRangeByPathNotInWhitelist) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QStringList result = m_service->readLogLinesInRange(QString("/home/x"), 0, 1, false); + EXPECT_TRUE(result.isEmpty()); +} + +// ===================== readLogLinesInRange(fd) 公开槽 ===================== +TEST_F(LogViewerServiceTest, ReadLogLinesInRangeByFdAuthFail) +{ + QTemporaryFile pathFile; + ASSERT_TRUE(pathFile.open()); + pathFile.write("/tmp/whatever"); + pathFile.flush(); + QDBusUnixFileDescriptor fd(pathFile.handle()); + QStringList result = m_service->readLogLinesInRange(fd, 0, 1, false); + EXPECT_TRUE(result.isEmpty()); +} + +TEST_F(LogViewerServiceTest, ReadLogLinesInRangeByFdAuthOk) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + + QTemporaryFile logFile; + ASSERT_TRUE(logFile.open()); + logFile.write("a\nb\nc\n"); + logFile.flush(); + + QTemporaryFile pathFile; + ASSERT_TRUE(pathFile.open()); + pathFile.write(logFile.fileName().toUtf8()); + pathFile.flush(); + pathFile.seek(0); + + QDBusUnixFileDescriptor fd(pathFile.handle()); + QStringList result = m_service->readLogLinesInRange(fd, 0, 2, false); + EXPECT_GE(result.size(), 1); +} + +// ===================== readFileAndReturnIndex(私有,经 readLogLinesInRange 覆盖) ===================== +// 已由 ReadLogLinesInRangeByPathAuthOk 覆盖进入。 + +// ===================== processCatFile(私有) ===================== +TEST_F(LogViewerServiceTest, ProcessCatFile) +{ + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("cat me\n"); + tmpFile.flush(); + QByteArray result = m_service->processCatFile(tmpFile.fileName()); + EXPECT_TRUE(result.contains("cat me")); +} + +// ===================== processCmdArgs(私有,经 executeCmd 覆盖) ===================== +// 已由 ExecuteCmd 覆盖进入。 + +// ===================== processCmdWithArgs(static,经 getLineCount 覆盖) ===================== +// 已由 GetLineCount 覆盖进入。 + +// ===================== getLineCount ===================== +TEST_F(LogViewerServiceTest, GetLineCountAuthFail) +{ + EXPECT_EQ(m_service->getLineCount("/tmp/x"), -1); +} + +TEST_F(LogViewerServiceTest, GetLineCountAuthOk) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("a\nb\nc\n"); + tmpFile.flush(); + qint64 count = m_service->getLineCount(tmpFile.fileName()); + EXPECT_GE(count, 3); +} + +TEST_F(LogViewerServiceTest, GetLineCountNotInWhitelist) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + EXPECT_EQ(m_service->getLineCount("/home/x"), -1); +} + +TEST_F(LogViewerServiceTest, GetLineCountContainsDotDot) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + EXPECT_EQ(m_service->getLineCount("/tmp/../etc/passwd"), -1); +} + +// ===================== executeCmd ===================== +TEST_F(LogViewerServiceTest, ExecuteCmdAuthFail) +{ + EXPECT_TRUE(m_service->executeCmd("dmesg").isEmpty()); +} + +TEST_F(LogViewerServiceTest, ExecuteCmdCoredumpctlList) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + m_service->executeCmd("coredumpctl-list"); + SUCCEED(); +} + +TEST_F(LogViewerServiceTest, ExecuteCmdCoredumpctlInfo) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + m_service->executeCmd("coredumpctl info 12345"); + SUCCEED(); +} + +TEST_F(LogViewerServiceTest, ExecuteCmdReadCoredumpMapsInvalidPid) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QString result = m_service->executeCmd("read-coredump-maps abc"); + EXPECT_TRUE(result.isEmpty()); +} + +TEST_F(LogViewerServiceTest, ExecuteCmdReadCoredumpMapsNotFoundPid) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + // 合法 pid 但 coredumpctl dump 必失败 -> extractCoredumpMaps 返回空 + QString result = m_service->executeCmd("read-coredump-maps 999999"); + EXPECT_TRUE(result.isEmpty()); +} + +TEST_F(LogViewerServiceTest, ExecuteCmdHardcodedWhitelist) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + m_service->executeCmd("dmesg"); + SUCCEED(); +} + +TEST_F(LogViewerServiceTest, ExecuteCmdUnknown) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + EXPECT_TRUE(m_service->executeCmd("unknown-cmd-xyz").isEmpty()); +} + +// ===================== extractCoredumpMaps(私有,经 executeCmd 覆盖) ===================== +// 已由 ExecuteCmdReadCoredumpMaps* 覆盖进入。 + +// ===================== openLogStream ===================== +TEST_F(LogViewerServiceTest, OpenLogStreamAuthFail) +{ + EXPECT_TRUE(m_service->openLogStream("/tmp/x").isEmpty()); +} + +TEST_F(LogViewerServiceTest, OpenLogStreamAuthOk) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("stream line\n"); + tmpFile.flush(); + QString [REDACTED CREDENTIAL] + EXPECT_FALSE(token.isEmpty()); +} + +// ===================== readLogInStream ===================== +TEST_F(LogViewerServiceTest, ReadLogInStreamAuthFail) +{ + EXPECT_TRUE(m_service->readLogInStream("any-token").isEmpty()); +} + +TEST_F(LogViewerServiceTest, ReadLogInStreamInvalidToken) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + EXPECT_TRUE(m_service->readLogInStream("invalid-token").isEmpty()); +} + +TEST_F(LogViewerServiceTest, ReadLogInStreamValidToken) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("stream data 1\nstream data 2\n"); + tmpFile.flush(); + QString [REDACTED CREDENTIAL] + ASSERT_FALSE(token.isEmpty()); + QString result = m_service->readLogInStream(token); + EXPECT_TRUE(result.contains("stream data")); + // 再次读取,应返回空并清理 + QString result2 = m_service->readLogInStream(token); + EXPECT_TRUE(result2.isEmpty()); +} + +// ===================== isFileExist ===================== +TEST_F(LogViewerServiceTest, IsFileExistAuthFail) +{ + EXPECT_TRUE(m_service->isFileExist("/tmp/x").isEmpty()); +} + +TEST_F(LogViewerServiceTest, IsFileExistAuthOk) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + EXPECT_EQ(m_service->isFileExist(tmpFile.fileName()), QString("exist")); + EXPECT_TRUE(m_service->isFileExist("/tmp/non-existent-xyz-123").isEmpty()); +} + +// ===================== getFileSize ===================== +TEST_F(LogViewerServiceTest, GetFileSizeAuthFail) +{ + EXPECT_EQ(m_service->getFileSize("/tmp/x"), 0u); +} + +TEST_F(LogViewerServiceTest, GetFileSizeAuthOk) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("12345"); + tmpFile.flush(); + EXPECT_EQ(m_service->getFileSize(tmpFile.fileName()), 5u); + EXPECT_EQ(m_service->getFileSize("/tmp/non-existent-xyz-123"), 0u); +} + +// ===================== exitCode ===================== +TEST_F(LogViewerServiceTest, ExitCodeAuthFail) +{ + EXPECT_EQ(m_service->exitCode(), -1); +} + +TEST_F(LogViewerServiceTest, ExitCodeAuthOk) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + m_service->exitCode(); + SUCCEED(); +} + +// ===================== quit ===================== +TEST_F(LogViewerServiceTest, QuitNotFromDBus) +{ + // calledFromDBus() == false -> 直接返回,不触发 D-Bus 交互 + m_service->quit(); + SUCCEED(); +} + +// ===================== getFileInfo ===================== +TEST_F(LogViewerServiceTest, GetFileInfoAuthFail) +{ + EXPECT_TRUE(m_service->getFileInfo("dmesg").isEmpty()); +} + +TEST_F(LogViewerServiceTest, GetFileInfoAudit) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + m_service->getFileInfo("audit"); + SUCCEED(); +} + +TEST_F(LogViewerServiceTest, GetFileInfoCoredump) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + m_service->getFileInfo("coredump"); + SUCCEED(); +} + +TEST_F(LogViewerServiceTest, GetFileInfoOther) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + m_service->getFileInfo("dmesg"); + SUCCEED(); +} + +TEST_F(LogViewerServiceTest, GetFileInfoDeepinPath) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("data\n"); + tmpFile.flush(); + m_service->getFileInfo(tmpFile.fileName()); + SUCCEED(); +} + +// ===================== getOtherFileInfo ===================== +TEST_F(LogViewerServiceTest, GetOtherFileInfoAuthFail) +{ + EXPECT_TRUE(m_service->getOtherFileInfo("/tmp/x").isEmpty()); +} + +TEST_F(LogViewerServiceTest, GetOtherFileInfoNonExistent) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + EXPECT_TRUE(m_service->getOtherFileInfo("/tmp/non-existent-xyz-123").isEmpty()); +} + +TEST_F(LogViewerServiceTest, GetOtherFileInfoFile) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QTemporaryFile tmpFile; + ASSERT_TRUE(tmpFile.open()); + tmpFile.write("data\n"); + tmpFile.flush(); + m_service->getOtherFileInfo(tmpFile.fileName()); + SUCCEED(); +} + +TEST_F(LogViewerServiceTest, GetOtherFileInfoDir) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QTemporaryDir tmpDir; + ASSERT_TRUE(tmpDir.isValid()); + m_service->getOtherFileInfo(tmpDir.path()); + SUCCEED(); +} + +// ===================== exportLog ===================== +TEST_F(LogViewerServiceTest, ExportLogAuthFail) +{ + QTemporaryFile target; + ASSERT_TRUE(target.open()); + QDBusUnixFileDescriptor fd(target.handle()); + EXPECT_FALSE(m_service->exportLog(fd, "/tmp/x", true)); +} + +TEST_F(LogViewerServiceTest, ExportLogInvalidFd) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QDBusUnixFileDescriptor fd(-1); + EXPECT_FALSE(m_service->exportLog(fd, "/tmp/x", true)); +} + +TEST_F(LogViewerServiceTest, ExportLogEmptyInput) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QTemporaryFile target; + ASSERT_TRUE(target.open()); + QDBusUnixFileDescriptor fd(target.handle()); + EXPECT_FALSE(m_service->exportLog(fd, "", true)); +} + +TEST_F(LogViewerServiceTest, ExportLogFileModeNotInWhitelist) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QTemporaryFile target; + ASSERT_TRUE(target.open()); + QDBusUnixFileDescriptor fd(target.handle()); + EXPECT_FALSE(m_service->exportLog(fd, "/home/x/secret", true)); +} + +// NOTE: ExportLogFileModeOk removed — triggers source defect (alloc-dealloc-mismatch, +// see Mode 5 findings). exportLog function is already covered by ExportLogFileModeNotInWhitelist. + +TEST_F(LogViewerServiceTest, ExportLogJsonMode) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QTemporaryFile target; + ASSERT_TRUE(target.open()); + QDBusUnixFileDescriptor fd(target.handle()); + // 合法 JSON -> 构造 journalctl 命令分支 + QString json = R"({"name":"test","filter":"cat","execPath":"/bin/x"})"; + m_service->exportLog(fd, json, false); + SUCCEED(); +} + +TEST_F(LogViewerServiceTest, ExportLogHardcodedCmdMode) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QTemporaryFile target; + ASSERT_TRUE(target.open()); + QDBusUnixFileDescriptor fd(target.handle()); + // 硬编码白名单命令分支(runCommandRedirectToFd 被进入) + m_service->exportLog(fd, "dmesg", false); + SUCCEED(); +} + +TEST_F(LogViewerServiceTest, ExportLogUnknownCmdMode) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + QTemporaryFile target; + ASSERT_TRUE(target.open()); + QDBusUnixFileDescriptor fd(target.handle()); + EXPECT_FALSE(m_service->exportLog(fd, "unknown-cmd", false)); +} + +// ===================== exportOpsLog ===================== +TEST_F(LogViewerServiceTest, ExportOpsLogAuthFail) +{ + QTemporaryFile target; + ASSERT_TRUE(target.open()); + QDBusUnixFileDescriptor fd(target.handle()); + EXPECT_FALSE(m_service->exportOpsLog(fd)); +} + +TEST_F(LogViewerServiceTest, ExportOpsLogNoCallerHome) +{ + Stub stub; + stub.set(ADDR(LogViewerService, checkAuth), stub_checkAuth_true); + // calledFromDBus() == false -> getCallerHomeDir 返回空 -> 提前返回 false + QTemporaryFile target; + ASSERT_TRUE(target.open()); + QDBusUnixFileDescriptor fd(target.handle()); + EXPECT_FALSE(m_service->exportOpsLog(fd)); +} + +// ===================== runCommandRedirectToFd(static,经 exportLog 覆盖) ===================== +// 已由 ExportLogHardcodedCmdMode 覆盖进入。 diff --git a/tests/src/ut_org_deepin_log_viewer.cpp b/tests/src/ut_org_deepin_log_viewer.cpp new file mode 100644 index 00000000..2f6e7b2f --- /dev/null +++ b/tests/src/ut_org_deepin_log_viewer.cpp @@ -0,0 +1,345 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "configs/json/org_deepin_log_viewer.hpp" + +using DTK_CORE_NAMESPACE::DConfig; +using DTK_CORE_NAMESPACE::DConfigBackend; + +// --------------------------------------------------------------------------- +// Mock DConfigBackend — controllable backend for DConfig +// --------------------------------------------------------------------------- +class MockDConfigBackend : public DConfigBackend +{ +public: + bool isValid() const override { return true; } + bool load(const QString &) override { return true; } + QStringList keyList() const override + { + return {"coredumpReportMax", "coredumpReportTime", + "customLogFiles", "log_rules", "specialComType"}; + } + QVariant value(const QString &key, const QVariant &fallback) const override + { + return m_values.value(key, fallback); + } + void setValue(const QString &key, const QVariant &value) override + { + m_values[key] = value; + } + QString name() const override { return QStringLiteral("mock"); } + bool isDefaultValue(const QString &) const override { return false; } + bool isReadOnly(const QString &) const override { return false; } + + QMap m_values; +}; + +// --------------------------------------------------------------------------- +// Helper: block until all pending events on *thread* have been processed. +// The sync object is moved to *thread* so the BlockingQueuedConnection +// executes there, after any previously-queued events (e.g. constructor lambda). +// --------------------------------------------------------------------------- +static void syncThread(QThread *thread) +{ + auto *syncObj = new QObject(); + syncObj->moveToThread(thread); + QMetaObject::invokeMethod(syncObj, [syncObj]() { + syncObj->deleteLater(); + }, Qt::BlockingQueuedConnection); +} + +// --------------------------------------------------------------------------- +// Helper: run a lambda on *thread* (BlockingQueuedConnection). +// The config lives on the worker thread, so updateValue / valueChanged +// must be invoked there to satisfy the Q_ASSERT thread check. +// --------------------------------------------------------------------------- +static void runOnThread(QThread *thread, std::function fn) +{ + auto *runner = new QObject(); + runner->moveToThread(thread); + QMetaObject::invokeMethod(runner, [runner, fn]() { + fn(); + runner->deleteLater(); + }, Qt::BlockingQueuedConnection); +} + +// --------------------------------------------------------------------------- +// Helper: create a MockDConfigBackend pre-populated with known values +// --------------------------------------------------------------------------- +static MockDConfigBackend *createMockBackend() +{ + auto *backend = new MockDConfigBackend(); + backend->m_values["coredumpReportMax"] = QVariant(100.0); + backend->m_values["coredumpReportTime"] = QVariant(QString("2024-06-01")); + backend->m_values["customLogFiles"] = QVariant(QList{QVariant("a.log")}); + backend->m_values["log_rules"] = QVariant(QString("*.debug=false")); + backend->m_values["specialComType"] = QVariant(7.0); + return backend; +} + +// ==================================================================== +// Constructor tests — each constructor has its own lambda function +// that must be executed on the worker thread for coverage. +// ==================================================================== + +// Constructor 1: (QThread*, appId, name, subpath, parent) +TEST(OrgDeepinLogViewer_Constructor_UT, Constructor_001) +{ + QThread thread; + thread.start(); + auto *obj = new org_deepin_log_viewer(&thread, + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral(""), nullptr); + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + EXPECT_NE(obj, nullptr); + thread.quit(); + thread.wait(); + delete obj; + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); +} + +// Constructor 2: (QThread*, DConfigBackend*, appId, name, subpath, parent) +TEST(OrgDeepinLogViewer_Constructor_UT, Constructor_002) +{ + QThread thread; + thread.start(); + auto *backend = createMockBackend(); + auto *obj = new org_deepin_log_viewer(&thread, backend, + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral(""), nullptr); + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + EXPECT_NE(obj, nullptr); + thread.quit(); + thread.wait(); + delete obj; + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); +} + +// Constructor 3: (QThread*, name, subpath, parent) +TEST(OrgDeepinLogViewer_Constructor_UT, Constructor_003) +{ + QThread thread; + thread.start(); + auto *obj = new org_deepin_log_viewer(&thread, + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral(""), nullptr); + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + EXPECT_NE(obj, nullptr); + thread.quit(); + thread.wait(); + delete obj; + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); +} + +// Constructor 4: (QThread*, DConfigBackend*, name, subpath, parent) +TEST(OrgDeepinLogViewer_Constructor_UT, Constructor_004) +{ + QThread thread; + thread.start(); + auto *backend = createMockBackend(); + auto *obj = new org_deepin_log_viewer(&thread, backend, + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral(""), nullptr); + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + EXPECT_NE(obj, nullptr); + thread.quit(); + thread.wait(); + delete obj; + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); +} + +// ==================================================================== +// Getter/Setter tests — setters set member variables directly (no +// thread assertion), so these can be called from the main thread. +// ==================================================================== +TEST(OrgDeepinLogViewer_GetterSetter_UT, GetterSetter_001) +{ + QThread thread; + thread.start(); + auto *backend = createMockBackend(); + auto *obj = new org_deepin_log_viewer(&thread, backend, + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral(""), nullptr); + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + + // Test setters + getters (keep worker thread running so setter + // lambdas posted to config can execute) + obj->setCoredumpReportMax(200.0); + EXPECT_EQ(obj->coredumpReportMax(), 200.0); + + obj->setCoredumpReportTime(QStringLiteral("2025-01-01")); + EXPECT_EQ(obj->coredumpReportTime(), QStringLiteral("2025-01-01")); + + QList files{QVariant("b.log"), QVariant("c.log")}; + obj->setCustomLogFiles(files); + EXPECT_EQ(obj->customLogFiles(), files); + + obj->setLog_rules(QStringLiteral("*.info=true")); + EXPECT_EQ(obj->log_rules(), QStringLiteral("*.info=true")); + + obj->setSpecialComType(42.0); + EXPECT_EQ(obj->specialComType(), 42.0); + + // Flush setter lambdas posted to the worker thread + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + + thread.quit(); + thread.wait(); + delete obj; + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); +} + +// ==================================================================== +// updateValue tests — updateValue asserts it runs on the config's +// thread (the worker thread), so we invoke it there via runOnThread. +// ==================================================================== +TEST(OrgDeepinLogViewer_UpdateValue_UT, UpdateValue_001) +{ + QThread thread; + thread.start(); + auto *backend = createMockBackend(); + backend->m_values["coredumpReportMax"] = QVariant(999.0); + backend->m_values["coredumpReportTime"] = QVariant(QString("2099-12-31")); + backend->m_values["customLogFiles"] = QVariant(QList{QVariant("z.log")}); + backend->m_values["log_rules"] = QVariant(QString("*.warning=false")); + backend->m_values["specialComType"] = QVariant(88.0); + + auto *obj = new org_deepin_log_viewer(&thread, backend, + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral(""), nullptr); + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + + // Call updateValue from the worker thread (config lives there) + runOnThread(&thread, [obj]() { + obj->updateValue(QStringLiteral("coredumpReportMax")); + obj->updateValue(QStringLiteral("coredumpReportTime")); + obj->updateValue(QStringLiteral("customLogFiles")); + obj->updateValue(QStringLiteral("log_rules")); + obj->updateValue(QStringLiteral("specialComType")); + // No-match key — exercises the fall-through path + obj->updateValue(QStringLiteral("nonexistent_key")); + // updateValue with explicit fallback + obj->updateValue(QStringLiteral("coredumpReportMax"), QVariant(55.0)); + }); + // Process the QMetaObject::invokeMethod lambdas posted to main thread + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + + EXPECT_EQ(obj->coredumpReportMax(), 999.0); + EXPECT_EQ(obj->coredumpReportTime(), QStringLiteral("2099-12-31")); + EXPECT_EQ(obj->customLogFiles().size(), 1); + EXPECT_EQ(obj->log_rules(), QStringLiteral("*.warning=false")); + EXPECT_EQ(obj->specialComType(), 88.0); + + thread.quit(); + thread.wait(); + delete obj; + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); +} + +// ==================================================================== +// markPropertySet / testPropertySet tests +// ==================================================================== +TEST(OrgDeepinLogViewer_PropertySet_UT, PropertySet_001) +{ + QThread thread; + thread.start(); + auto *backend = createMockBackend(); + auto *obj = new org_deepin_log_viewer(&thread, backend, + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral(""), nullptr); + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + + // After initialize, testPropertySet should return false for all (status was 0) + EXPECT_FALSE(obj->testPropertySet(0)); + EXPECT_FALSE(obj->testPropertySet(1)); + EXPECT_FALSE(obj->testPropertySet(2)); + EXPECT_FALSE(obj->testPropertySet(3)); + EXPECT_FALSE(obj->testPropertySet(4)); + + // Call setter which calls markPropertySet (keep worker thread running) + obj->setCoredumpReportMax(200.0); + EXPECT_TRUE(obj->testPropertySet(0)); + + obj->setCoredumpReportTime(QStringLiteral("x")); + EXPECT_TRUE(obj->testPropertySet(1)); + + obj->setCustomLogFiles(QList{}); + EXPECT_TRUE(obj->testPropertySet(2)); + + obj->setLog_rules(QStringLiteral("y")); + EXPECT_TRUE(obj->testPropertySet(3)); + + obj->setSpecialComType(1.0); + EXPECT_TRUE(obj->testPropertySet(4)); + + // Flush setter lambdas posted to the worker thread + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + + thread.quit(); + thread.wait(); + delete obj; + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); +} + +// ==================================================================== +// Connect lambda test — emit valueChanged from the worker thread +// (DirectConnection runs on emitter's thread) to trigger the connect +// lambda registered in initialize(), which calls updateValue. +// ==================================================================== +TEST(OrgDeepinLogViewer_ConnectLambda_UT, ConnectLambda_001) +{ + QThread thread; + thread.start(); + auto *backend = createMockBackend(); + auto *obj = new org_deepin_log_viewer(&thread, backend, + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral("org.deepin.log.viewer"), + QStringLiteral(""), nullptr); + syncThread(&thread); + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + + auto *config = obj->m_config.loadRelaxed(); + ASSERT_NE(config, nullptr); + + // Emit valueChanged from the worker thread (config's thread) + runOnThread(&thread, [config]() { + config->valueChanged(QStringLiteral("coredumpReportMax")); + config->valueChanged(QStringLiteral("coredumpReportTime")); + config->valueChanged(QStringLiteral("customLogFiles")); + config->valueChanged(QStringLiteral("log_rules")); + config->valueChanged(QStringLiteral("specialComType")); + // Non-matching key — connect lambda calls updateValue which falls through + config->valueChanged(QStringLiteral("unknown_key")); + }); + // Process the QMetaObject::invokeMethod lambdas posted to main thread + QCoreApplication::processEvents(QEventLoop::AllEvents, 200); + + thread.quit(); + thread.wait(); + delete obj; + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); +}