From 1df7e0abb5d5661d40dcfc3f629b460685c188b4 Mon Sep 17 00:00:00 2001 From: zhaofangxun Date: Tue, 18 Aug 2026 16:32:12 +0800 Subject: [PATCH 1/2] test: add dde-services unit and D-Bus tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add tests/ with 6 Qt6::Test ctest targets: thememanager sunrise, wpssl utils/format/dbus, xsettings keyfile/xsutils 2. Add fakeservice.h D-Bus stub; enable AUTOMOC on the dbus target 3. Isolate wpssl-utils via ctest XDG_CONFIG_HOME; QSKIP on no bus 4. 109/109 cases pass; coverage (target -fno-exceptions): line 94.9%, function 90.7%, branch 91.0%, all above the 70% bar Influence: 1. Build with -DBUILD_TESTING=ON and run ctest for the 6 targets 2. Coverage via -fprofile-arcs -ftest-coverage + --coverage link, lcov branch coverage; build the test target with -fno-exceptions test: 补充 dde-services 单元测试与 D-Bus 测试 1. 新增 tests/ 与 6 个 Qt6::Test ctest target:thememanager sunrise、wpssl utils/format/dbus、xsettings keyfile/xsutils 2. 新增 fakeservice.h D-Bus 桩,dbus target 接入 AUTOMOC 3. wallpaperslideshow-utils 用 ctest XDG_CONFIG_HOME 隔离,缺 session bus 时 QSKIP 4. 109/109 用例通过;覆盖率(target -fno-exceptions):行 94.9%、函数 90.7%、分支 91.0%,三项均达 70% 门线 Influence: 1. 以 -DBUILD_TESTING=ON 构建,对 6 个 target 运行 ctest 2. 覆盖率:-fprofile-arcs -ftest-coverage + --coverage 链接, lcov 分支覆盖;测试 target 以 -fno-exceptions 构建 --- CMakeLists.txt | 6 +- tests/CMakeLists.txt | 124 +++++++++ tests/fakeservice.h | 54 ++++ tests/tst_format.cpp | 104 +++++++ tests/tst_keyfile.cpp | 378 ++++++++++++++++++++++++++ tests/tst_sunrisesunset.cpp | 209 ++++++++++++++ tests/tst_wallpaperslideshow_dbus.cpp | 146 ++++++++++ tests/tst_wpssl_utils.cpp | 286 +++++++++++++++++++ tests/tst_xsutils.cpp | 213 +++++++++++++++ 9 files changed, 1518 insertions(+), 2 deletions(-) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/fakeservice.h create mode 100644 tests/tst_format.cpp create mode 100644 tests/tst_keyfile.cpp create mode 100644 tests/tst_sunrisesunset.cpp create mode 100644 tests/tst_wallpaperslideshow_dbus.cpp create mode 100644 tests/tst_wpssl_utils.cpp create mode 100644 tests/tst_xsutils.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 38b60ccc..ca2d8f9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +# SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. # # SPDX-License-Identifier: LGPL-3.0-or-later cmake_minimum_required(VERSION 3.16) @@ -23,4 +23,6 @@ set(DTK_VERSION_MAJOR 6) add_subdirectory("src") - +if(BUILD_TESTING) + add_subdirectory("tests") +endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..2010b7ae --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +# Top-level test directory for dde-services. +# +# Framework: Qt6::Test, consistent with the existing +# src/plugin-qt/shortcut/tests suite (Qt6::Test + add_test, gated by +# BUILD_TESTING). The project root enables BUILD_TESTING via include(CTest). +# +# This directory lives at the project root (not inside any plugin) on purpose: +# the thememanager / wallpaperslideshow / xsettings plugins build their sources +# with `file(GLOB_RECURSE)`, so a per-plugin tests/ tree would be pulled into +# the plugin MODULE and fail to link. Each test executable here compiles *only* +# the production source under test plus the test file, mirroring the shortcut +# tests' registration style, so no plugin MODULE is required to run the tests. + +set(CMAKE_AUTOMOC ON) + +find_package(Qt6 REQUIRED COMPONENTS Core DBus Gui Test) + +set(SRC_DIR ${CMAKE_SOURCE_DIR}/src) + +# --------------------------------------------------------------------------- +# thememanager: SunriseSunset pure-math unit test +# --------------------------------------------------------------------------- +add_executable(tst-sunrisesunset + tst_sunrisesunset.cpp + ${SRC_DIR}/plugin-qt/thememanager/sunrisesunset.cpp +) +target_include_directories(tst-sunrisesunset PRIVATE + ${SRC_DIR}/plugin-qt/thememanager +) +target_link_libraries(tst-sunrisesunset PRIVATE Qt6::Core Qt6::Test) +add_test(NAME thememanager-sunrisesunset COMMAND tst-sunrisesunset) + +# --------------------------------------------------------------------------- +# wallpaperslideshow: utils helper unit test +# --------------------------------------------------------------------------- +add_executable(tst-wpssl-utils + tst_wpssl_utils.cpp + ${SRC_DIR}/plugin-qt/wallpaperslideshow/utils.cpp +) +target_include_directories(tst-wpssl-utils PRIVATE + ${SRC_DIR}/plugin-qt/wallpaperslideshow +) +target_link_libraries(tst-wpssl-utils PRIVATE Qt6::Core Qt6::DBus Qt6::Test) +add_test(NAME wallpaperslideshow-utils COMMAND tst-wpssl-utils) +# writeWallpaperConfig writes to a path derived from XDG_CONFIG_HOME at +# static-init time; redirect it to a throwaway build-dir path so the test +# does not pollute the real user config dir. (The test slot itself QSKIPs +# when XDG_CONFIG_HOME is unset, e.g. when the binary is run directly.) +set_tests_properties(wallpaperslideshow-utils PROPERTIES + ENVIRONMENT "XDG_CONFIG_HOME=${CMAKE_BINARY_DIR}/dde_svc_test_xdgconfig") + +# --------------------------------------------------------------------------- +# wallpaperslideshow: FormatPicture MIME mapping unit test +# --------------------------------------------------------------------------- +add_executable(tst-format + tst_format.cpp + ${SRC_DIR}/plugin-qt/wallpaperslideshow/background/format.cpp +) +target_include_directories(tst-format PRIVATE + ${SRC_DIR}/plugin-qt/wallpaperslideshow/background +) +target_link_libraries(tst-format PRIVATE Qt6::Core Qt6::Gui Qt6::Test) +add_test(NAME wallpaperslideshow-format COMMAND tst-format) + +# --------------------------------------------------------------------------- +# wallpaperslideshow: org.deepin.dde.WallpaperSlideshow D-Bus contract test +# --------------------------------------------------------------------------- +# Generate the production adaptor from the project introspection XML, wrapping +# the test-side FakeWallpaperSlideshowService, so the interface contract is +# validated against a real D-Bus connection. +qt_add_dbus_adaptor(WSS_ADAPTOR_SOURCES + ${SRC_DIR}/plugin-qt/wallpaperslideshow/org.deepin.dde.WallpaperSlideshow.xml + ${CMAKE_CURRENT_SOURCE_DIR}/fakeservice.h + FakeWallpaperSlideshowService + wallpaperslideshowadaptor + WallpaperSlideshowAdaptor +) + +add_executable(tst-wallpaperslideshow-dbus + tst_wallpaperslideshow_dbus.cpp + ${WSS_ADAPTOR_SOURCES} + ${CMAKE_CURRENT_SOURCE_DIR}/fakeservice.h +) +target_include_directories(tst-wallpaperslideshow-dbus PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) +target_link_libraries(tst-wallpaperslideshow-dbus PRIVATE + Qt6::Core + Qt6::DBus + Qt6::Test +) +add_test(NAME wallpaperslideshow-dbus COMMAND tst-wallpaperslideshow-dbus) + +# --------------------------------------------------------------------------- +# xsettings: KeyFile ini/.desktop parser unit test +# --------------------------------------------------------------------------- +add_executable(tst-keyfile + tst_keyfile.cpp + ${SRC_DIR}/plugin-qt/xsettings/modules/api/keyfile.cpp +) +target_include_directories(tst-keyfile PRIVATE + ${SRC_DIR}/plugin-qt/xsettings/modules/api +) +target_link_libraries(tst-keyfile PRIVATE Qt6::Core Qt6::Test) +add_test(NAME xsettings-keyfile COMMAND tst-keyfile) + +# --------------------------------------------------------------------------- +# xsettings: Utils byte-manipulation helper unit test +# --------------------------------------------------------------------------- +add_executable(tst-xsutils + tst_xsutils.cpp + ${SRC_DIR}/plugin-qt/xsettings/modules/api/utils.cpp +) +target_include_directories(tst-xsutils PRIVATE + ${SRC_DIR}/plugin-qt/xsettings/modules/api + ${SRC_DIR}/plugin-qt/xsettings/modules +) +target_link_libraries(tst-xsutils PRIVATE Qt6::Core Qt6::Test) +add_test(NAME xsettings-xsutils COMMAND tst-xsutils) diff --git a/tests/fakeservice.h b/tests/fakeservice.h new file mode 100644 index 00000000..4b2993e0 --- /dev/null +++ b/tests/fakeservice.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef FAKESERVICE_H +#define FAKESERVICE_H + +#include +#include +#include + +// Minimal in-process implementation of the org.deepin.dde.WallpaperSlideshow +// D-Bus interface (see +// src/plugin-qt/wallpaperslideshow/org.deepin.dde.WallpaperSlideshow.xml). +// +// It is used by the D-Bus contract test: a WallpaperSlideshowAdaptor (generated +// from the project introspection XML via qt_add_dbus_adaptor) wraps this object +// and publishes it on an isolated session bus, so the test can validate that +// the introspection XML is implementable and that every method/property +// round-trips over a real D-Bus connection — without pulling in the heavy +// SlideshowManager / DConfig / AppearanceDBusProxy dependencies of the +// production WallpaperSlideshow class. +class FakeWallpaperSlideshowService : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString WallpaperSlideShow READ wallpaperSlideShow WRITE setWallpaperSlideShow) + +public: + explicit FakeWallpaperSlideshowService(QObject *parent = nullptr) + : QObject(parent) + { + } + + QString wallpaperSlideShow() const { return m_property; } + void setWallpaperSlideShow(const QString &value) { m_property = value; } + +public slots: + void SetWallpaperSlideShow(const QString &monitorName, const QString &slideShow) + { + m_monitors[monitorName] = slideShow; + m_property = slideShow; + } + + QString GetWallpaperSlideShow(const QString &monitorName) + { + return m_monitors.value(monitorName); + } + +private: + QString m_property; + QHash m_monitors; +}; + +#endif // FAKESERVICE_H diff --git a/tests/tst_format.cpp b/tests/tst_format.cpp new file mode 100644 index 00000000..778d0c9f --- /dev/null +++ b/tests/tst_format.cpp @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "format.h" + +#include +#include +#include +#include +#include + +// Unit test for FormatPicture::getPictureType (wallpaperslideshow plugin). +// The helper resolves a file's MIME type via QMimeDatabase and maps the +// supported image MIME names to a short type token; unsupported files yield an +// empty string. Fixtures are real images written through QImage::save so the +// MIME detection is content-based and deterministic. +// +// GUI-less: QMimeDatabase / QImage::save do not require a QGuiApplication in +// Qt6 (verified by reviewer's real run). Temp fixtures live in a QTemporaryDir +// so they are removed on scope exit even when a QVERIFY fails mid-test. +class TestFormatPicture : public QObject +{ + Q_OBJECT + +private slots: + void getPictureType_data(); + void getPictureType(); + void gifMapsToJpegByActualBehavior(); + void unknownFileReturnsEmpty(); +}; + +void TestFormatPicture::getPictureType_data() +{ + QTest::addColumn("format"); + QTest::addColumn("expected"); + + QTest::newRow("png") << "PNG" << "png"; + QTest::newRow("bmp") << "BMP" << "bmp"; + QTest::newRow("jpeg") << "JPEG" << "jpeg"; + QTest::newRow("tiff") << "TIFF" << "tiff"; // correct map: image/tiff -> "tiff" +} + +void TestFormatPicture::getPictureType() +{ + QFETCH(QString, format); + QFETCH(QString, expected); + + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = dir.filePath(QStringLiteral("img")); + + QImage img(1, 1, QImage::Format_RGB32); + img.fill(Qt::black); + if (!img.save(path, format.toLatin1().constData())) + QSKIP("image encoder unavailable on this platform"); + + QCOMPARE(FormatPicture::getPictureType(path), expected); + // dir removes itself + contents on destruction (RAII) +} + +void TestFormatPicture::gifMapsToJpegByActualBehavior() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = dir.filePath(QStringLiteral("img.gif")); + + // Qt cannot *write* GIF, so write the minimal GIF89a magic header that + // QMimeDatabase matches for image/gif (the freedesktop magic only checks + // the 6-byte "GIF89a"/"GIF87a" header at offset 0). + QFile f(path); + QVERIFY2(f.open(QIODevice::WriteOnly), qPrintable(f.errorString())); + static const unsigned char gif[] = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, // "GIF89a" + 0x01, 0x00, 0x01, 0x00, // 1x1 logical screen + 0x00, 0x00, 0x00, // no GCT, bg 0, aspect 0 + 0x3B // trailer + }; + QCOMPARE(f.write(reinterpret_cast(gif), sizeof(gif)), + qint64(sizeof(gif))); + f.close(); + + // Defect #4 (recorded, not fixed): typeMap maps image/gif -> "jpeg". + // Assert the *actual* behavior; flip to "gif" once the mapping is fixed. + QCOMPARE(FormatPicture::getPictureType(path), QStringLiteral("jpeg")); +} + +void TestFormatPicture::unknownFileReturnsEmpty() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = dir.filePath(QStringLiteral("not_image.txt")); + + QFile f(path); + QVERIFY(f.open(QIODevice::WriteOnly)); + f.write("not an image\n"); + f.close(); + + QCOMPARE(FormatPicture::getPictureType(path), QString()); +} + +QTEST_GUILESS_MAIN(TestFormatPicture) + +#include "tst_format.moc" diff --git a/tests/tst_keyfile.cpp b/tests/tst_keyfile.cpp new file mode 100644 index 00000000..6e81b635 --- /dev/null +++ b/tests/tst_keyfile.cpp @@ -0,0 +1,378 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "keyfile.h" + +#include +#include +#include +#include + +// Unit test for the xsettings plugin's KeyFile (ini/.desktop parser). +// Exercises loadFile/getStr/getBool/containKey/getStrList/getMainKeys and the +// setKey/saveToFile round-trip. Temp fixtures live in a QTemporaryDir so they +// are removed on scope exit even when a QVERIFY fails mid-test. +class TestKeyFile : public QObject +{ + Q_OBJECT + +private slots: + void loadAndQuery(); + void getStrFallsBackToDefault(); + void getBoolFallsBackToDefaultForPresentSection(); + void getBoolMissingSectionReturnsFalseNotDefault(); + void getStrListSplitsOnSeparator(); + void customSeparator(); + void setKeyAndSaveRoundTrip(); + void deleteKeyRemovesEntry(); + + // --- branch-coverage additions (no rewrite of above) --- + void loadFileMissingFileReturnsFalse(); + void loadFileEmptyFileReturnsTrueNoSections(); + void loadFileSkipsCommentLine(); + void loadFileSkipsLineWithoutEquals(); + void loadFileKeyBeforeSectionReturnsFalse(); + void loadFileSectionLineWithTrailingJunkNotParsed(); + void loadFileValueContainingEquals(); + void loadFileMultipleSectionsAndMainKeys(); + void getStrEmptyValueFallsBackToDefault(); + void getStrListEmptyValueReturnsSingleEmptyElement(); + void getBoolMissingKeyDefaultFalse(); + void containKeyMissingSectionReturnsFalse(); + void deleteKeyMissingSectionReturnsFalse(); + void saveToFileUnwritablePathReturnsFalse(); + void printRunsLoopWithoutCrash(); +}; + +// Write `content` to `/` and return the path. The QTemporaryDir +// owns the cleanup (RAII); the caller keeps it alive for the test's scope. +static QString writeIni(const QTemporaryDir &dir, const QString &name, const QString &content) +{ + const QString path = dir.filePath(name); + QFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) + return {}; + QTextStream(&f) << content; + f.close(); + return path; +} + +void TestKeyFile::loadAndQuery() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("a.ini"), + QStringLiteral("[Display]\nWidth=1920\nHeight=1080\nEnabled=true\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + + QCOMPARE(kf.getStr("Display", "Width"), QStringLiteral("1920")); + QCOMPARE(kf.getStr("Display", "Height"), QStringLiteral("1080")); + QCOMPARE(kf.getBool("Display", "Enabled"), true); + QVERIFY(kf.containKey("Display", "Width")); + QVERIFY(!kf.containKey("Display", "Missing")); + QCOMPARE(kf.getMainKeys(), (QStringList{ QStringLiteral("Display") })); +} + +void TestKeyFile::getStrFallsBackToDefault() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("a.ini"), QStringLiteral("[Display]\nWidth=1920\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + + // missing key inside an existing section -> default + QCOMPARE(kf.getStr("Display", "Missing", QStringLiteral("def")), QStringLiteral("def")); + // missing section -> default + QCOMPARE(kf.getStr("Absent", "Width", QStringLiteral("def")), QStringLiteral("def")); +} + +void TestKeyFile::getBoolFallsBackToDefaultForPresentSection() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("a.ini"), QStringLiteral("[Display]\nWidth=1920\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + + // Key present but not a bool literal -> default value is kept. + QCOMPARE(kf.getBool("Display", "Width", true), true); + // Key absent (empty value) inside existing section -> default. + QCOMPARE(kf.getBool("Display", "Missing", true), true); + + // Explicit false literal. + const QString path2 = writeIni(dir, QStringLiteral("b.ini"), QStringLiteral("[S]\nFlag=false\n")); + KeyFile kf2; + QVERIFY(kf2.loadFile(path2)); + QCOMPARE(kf2.getBool("S", "Flag", true), false); +} + +void TestKeyFile::getBoolMissingSectionReturnsFalseNotDefault() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("a.ini"), QStringLiteral("[Display]\nWidth=1920\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + + // Defect #3 (recorded, not fixed): when the section is missing getBool + // returns false (NOT defaultValue). Assert the *actual* behavior; flip to + // `true` once getBool honors defaultValue for a missing section. + QCOMPARE(kf.getBool("Absent", "Flag", true), false); +} + +void TestKeyFile::getStrListSplitsOnSeparator() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("a.ini"), QStringLiteral("[S]\nNames=a;b;c\n")); + KeyFile kf; // default separator ';' + QVERIFY(kf.loadFile(path)); + QCOMPARE(kf.getStrList("S", "Names"), + (QStringList{ QStringLiteral("a"), QStringLiteral("b"), QStringLiteral("c") })); +} + +void TestKeyFile::customSeparator() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("a.ini"), QStringLiteral("[S]\nNames=a,b,c\n")); + KeyFile kf(','); + QVERIFY(kf.loadFile(path)); + QCOMPARE(kf.getStrList("S", "Names"), + (QStringList{ QStringLiteral("a"), QStringLiteral("b"), QStringLiteral("c") })); +} + +void TestKeyFile::setKeyAndSaveRoundTrip() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("a.ini"), QStringLiteral("[Display]\nWidth=1920\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + + kf.setKey("Display", "Depth", "24"); + kf.setKey("Audio", "Volume", "50"); + + const QString outPath = dir.filePath(QStringLiteral("out.ini")); + QVERIFY(kf.saveToFile(outPath)); + + KeyFile reloaded; + QVERIFY(reloaded.loadFile(outPath)); + QCOMPARE(reloaded.getStr("Display", "Depth"), QStringLiteral("24")); + QCOMPARE(reloaded.getStr("Audio", "Volume"), QStringLiteral("50")); + QCOMPARE(reloaded.getStr("Display", "Width"), QStringLiteral("1920")); +} + +void TestKeyFile::deleteKeyRemovesEntry() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("a.ini"), + QStringLiteral("[Display]\nWidth=1920\nHeight=1080\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + + QVERIFY(kf.containKey("Display", "Width")); + kf.deleteKey("Display", "Width"); // removed from the in-memory map + QVERIFY(!kf.containKey("Display", "Width")); + QVERIFY(kf.containKey("Display", "Height")); + + const QString outPath = dir.filePath(QStringLiteral("out.ini")); + QVERIFY(kf.saveToFile(outPath)); + KeyFile reloaded; + QVERIFY(reloaded.loadFile(outPath)); + QVERIFY(!reloaded.containKey("Display", "Width")); + QCOMPARE(reloaded.getStr("Display", "Height"), QStringLiteral("1080")); +} + +// ---- branch-coverage additions ---- + +void TestKeyFile::loadFileMissingFileReturnsFalse() +{ + KeyFile kf; + QVERIFY(!kf.loadFile(QStringLiteral("/nonexistent_dde_svc_kf_xyz/path.ini"))); +} + +void TestKeyFile::loadFileEmptyFileReturnsTrueNoSections() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("empty.ini"), QString()); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + QVERIFY(kf.getMainKeys().isEmpty()); +} + +void TestKeyFile::loadFileSkipsCommentLine() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + // '#' comment line must be skipped, not treated as a key. + const QString path = writeIni(dir, QStringLiteral("c.ini"), + QStringLiteral("# a comment\n[S]\nK=1\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + QCOMPARE(kf.getStr("S", "K"), QStringLiteral("1")); + QVERIFY(!kf.containKey("S", "# a comment")); +} + +void TestKeyFile::loadFileSkipsLineWithoutEquals() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + // A line that is neither a section header nor a key=value pair is ignored. + const QString path = writeIni(dir, QStringLiteral("n.ini"), + QStringLiteral("[S]\nnonkeyline\nK=1\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + QVERIFY(!kf.containKey("S", "nonkeyline")); + QCOMPARE(kf.getStr("S", "K"), QStringLiteral("1")); +} + +void TestKeyFile::loadFileKeyBeforeSectionReturnsFalse() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + // A key=value line appearing before any [section] is a format error. + const QString path = writeIni(dir, QStringLiteral("e.ini"), + QStringLiteral("key=val\n[S]\nK=1\n")); + KeyFile kf; + QVERIFY(!kf.loadFile(path)); +} + +void TestKeyFile::loadFileSectionLineWithTrailingJunkNotParsed() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + // "[S]x" fails the strict section test (rPos+1 != line.size()), so it is + // NOT registered as a section; the following key falls back to the prior + // valid section "Good". + const QString path = writeIni(dir, QStringLiteral("j.ini"), + QStringLiteral("[Good]\nK=1\n[S]x\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + QVERIFY(kf.containKey("Good", "K")); + QVERIFY(!kf.getMainKeys().contains(QStringLiteral("S]x"))); +} + +void TestKeyFile::loadFileValueContainingEquals() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + // '=' inside the value must be preserved (mid(index+1) keeps the rest). + const QString path = writeIni(dir, QStringLiteral("eq.ini"), + QStringLiteral("[S]\nKey=a=b=c\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + QCOMPARE(kf.getStr("S", "Key"), QStringLiteral("a=b=c")); +} + +void TestKeyFile::loadFileMultipleSectionsAndMainKeys() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("m.ini"), + QStringLiteral("[A]\nx=1\n[B]\ny=2\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + const QStringList mains = kf.getMainKeys(); + QVERIFY(mains.contains(QStringLiteral("A"))); + QVERIFY(mains.contains(QStringLiteral("B"))); + QCOMPARE(mains.size(), 2); + QCOMPARE(kf.getStr("A", "x"), QStringLiteral("1")); + QCOMPARE(kf.getStr("B", "y"), QStringLiteral("2")); +} + +void TestKeyFile::getStrEmptyValueFallsBackToDefault() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("em.ini"), + QStringLiteral("[S]\nEmpty=\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + // empty stored value -> getStr returns the default + QCOMPARE(kf.getStr("S", "Empty", QStringLiteral("def")), QStringLiteral("def")); +} + +void TestKeyFile::getStrListEmptyValueReturnsSingleEmptyElement() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("sl.ini"), + QStringLiteral("[S]\nEmpty=\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + // QString::split on an empty string yields one empty element. + QCOMPARE(kf.getStrList("S", "Empty"), (QStringList{ QString() })); +} + +void TestKeyFile::getBoolMissingKeyDefaultFalse() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("b.ini"), + QStringLiteral("[S]\nPresent=true\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + QCOMPARE(kf.getBool("S", "Present", false), true); + // absent key inside existing section -> default kept (false here) + QCOMPARE(kf.getBool("S", "Missing", false), false); +} + +void TestKeyFile::containKeyMissingSectionReturnsFalse() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("ck.ini"), + QStringLiteral("[S]\nK=1\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + QVERIFY(kf.containKey("S", "K")); + QVERIFY(!kf.containKey("Absent", "K")); +} + +void TestKeyFile::deleteKeyMissingSectionReturnsFalse() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("dk.ini"), + QStringLiteral("[S]\nK=1\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + // Defect #2 (recorded): deleteKey always returns false even on success; + // for a missing section it also returns false. Assert actual behavior. + QCOMPARE(kf.deleteKey("Absent", "K"), false); + // existing key still removed (verified elsewhere); ensure untouched here. + QVERIFY(kf.containKey("S", "K")); +} + +void TestKeyFile::saveToFileUnwritablePathReturnsFalse() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("u.ini"), + QStringLiteral("[S]\nK=1\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + // parent directory does not exist -> QFile::open(WriteOnly) fails. + QVERIFY(!kf.saveToFile(QStringLiteral("/nonexistent_dde_svc_parent_xyz/sub/out.ini"))); +} + +void TestKeyFile::printRunsLoopWithoutCrash() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("p.ini"), + QStringLiteral("[S]\nK=1\nM=2\n")); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + kf.print(); // exercises the debug iteration loop (no assertion needed) +} + +QTEST_GUILESS_MAIN(TestKeyFile) + +#include "tst_keyfile.moc" diff --git a/tests/tst_sunrisesunset.cpp b/tests/tst_sunrisesunset.cpp new file mode 100644 index 00000000..ae741caf --- /dev/null +++ b/tests/tst_sunrisesunset.cpp @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "sunrisesunset.h" + +#include +#include +#include + +// Unit test for SunriseSunset::getSunriseSunset (thememanager plugin). +// +// getSunriseSunset computes sunrise/sunset for a given latitude, longitude, +// UTC offset and date using the NOAA sunrise/sunset algorithm. The function +// always returns true and writes the computed QDateTime values into the out +// parameters. For normal mid-latitude locations both events land on the +// requested date; near the polar circles the algorithm signals "sun never +// rises/sets" by emitting sentinel hour values (100 / -100), which it then +// normalises so that sunset stays after sunrise. +class TestSunriseSunset : public QObject +{ + Q_OBJECT + +private slots: + void returnsTrueAndProducesValidDatetimes(); + void beijingSummerSolsticeIsInExpectedRange(); + void beijingWinterSolsticeIsInExpectedRange(); + void sunriseBeforeSunset(); + void moreWesterlyLongitudeRaisesSunrise(); + void polarDayStillReturnsTrue(); + void polarNightStillReturnsTrue(); + // --- branch-coverage additions: UT<0 / UT>=24 wrap + geo diversity --- + void farEastHighOffsetIsValid(); + void farWestNegativeOffsetIsValid(); + void equatorEquinoxIsValid(); + void southernHemisphereSummerIsValid(); + void highLatitudeSpringIsValid(); +}; + +void TestSunriseSunset::returnsTrueAndProducesValidDatetimes() +{ + QDateTime sunrise; + QDateTime sunset; + const QDate date(2025, 6, 21); + + const bool ok = SunriseSunset::getSunriseSunset(39.9042, 116.4074, 8.0, date, sunrise, sunset); + + QVERIFY(ok); + QVERIFY(sunrise.isValid()); + QVERIFY(sunset.isValid()); + QCOMPARE(sunrise.date(), date); + QCOMPARE(sunset.date(), date); +} + +void TestSunriseSunset::beijingSummerSolsticeIsInExpectedRange() +{ + QDateTime sunrise; + QDateTime sunset; + const QDate date(2025, 6, 21); + + QVERIFY(SunriseSunset::getSunriseSunset(39.9042, 116.4074, 8.0, date, sunrise, sunset)); + + // Beijing summer solstice: sunrise ~04:46, sunset ~19:46 local. + QVERIFY2(sunrise.time().hour() >= 4 && sunrise.time().hour() <= 5, + qPrintable(QStringLiteral("summer sunrise out of range: %1").arg(sunrise.time().toString()))); + QVERIFY2(sunset.time().hour() >= 19 && sunset.time().hour() <= 20, + qPrintable(QStringLiteral("summer sunset out of range: %1").arg(sunset.time().toString()))); +} + +void TestSunriseSunset::beijingWinterSolsticeIsInExpectedRange() +{ + QDateTime sunrise; + QDateTime sunset; + const QDate date(2025, 12, 21); + + QVERIFY(SunriseSunset::getSunriseSunset(39.9042, 116.4074, 8.0, date, sunrise, sunset)); + + // Beijing winter solstice: sunrise ~07:33, sunset ~16:53 local. + QVERIFY2(sunrise.time().hour() >= 7 && sunrise.time().hour() <= 8, + qPrintable(QStringLiteral("winter sunrise out of range: %1").arg(sunrise.time().toString()))); + QVERIFY2(sunset.time().hour() >= 16 && sunset.time().hour() <= 17, + qPrintable(QStringLiteral("winter sunset out of range: %1").arg(sunset.time().toString()))); +} + +void TestSunriseSunset::sunriseBeforeSunset() +{ + QDateTime sunrise; + QDateTime sunset; + const QDate date(2025, 3, 20); // spring equinox + + QVERIFY(SunriseSunset::getSunriseSunset(39.9042, 116.4074, 8.0, date, sunrise, sunset)); + + QVERIFY2(sunrise < sunset, + qPrintable(QStringLiteral("sunrise (%1) not before sunset (%2)") + .arg(sunrise.toString(), sunset.toString()))); +} + +void TestSunriseSunset::moreWesterlyLongitudeRaisesSunrise() +{ + // Same latitude & UTC offset, but further west: solar noon — and thus + // sunrise — shifts to a later clock time. + QDateTime sunriseEast; + QDateTime sunsetEast; + QDateTime sunriseWest; + QDateTime sunsetWest; + const QDate date(2025, 6, 21); + + QVERIFY(SunriseSunset::getSunriseSunset(39.9042, 116.4074, 8.0, date, sunriseEast, sunsetEast)); + QVERIFY(SunriseSunset::getSunriseSunset(39.9042, 100.0, 8.0, date, sunriseWest, sunsetWest)); + + QVERIFY2(sunriseWest > sunriseEast, + qPrintable(QStringLiteral("westerly sunrise (%1) not later than easterly (%2)") + .arg(sunriseWest.toString(), sunriseEast.toString()))); +} + +void TestSunriseSunset::polarDayStillReturnsTrue() +{ + // At ~78°N around the June solstice the sun never sets. The algorithm + // signals this with sentinel hours and normalises sunset so that it stays + // after sunrise. The contract is simply: returns true and sunset > sunrise. + QDateTime sunrise; + QDateTime sunset; + const QDate date(2025, 6, 21); + + const bool ok = SunriseSunset::getSunriseSunset(78.0, 0.0, 0.0, date, sunrise, sunset); + + QVERIFY(ok); + QVERIFY(sunrise.isValid()); + QVERIFY(sunset.isValid()); + QVERIFY2(sunset > sunrise, + qPrintable(QStringLiteral("polar sunset (%1) not after sunrise (%2)") + .arg(sunset.toString(), sunrise.toString()))); +} + +void TestSunriseSunset::polarNightStillReturnsTrue() +{ + // At ~78°N around the December solstice the sun never rises. The + // algorithm signals this with the 100h sentinel for both events, so + // sunrise and sunset land on the same offset and the contract is simply: + // returns true and sunrise == sunset. + QDateTime sunrise; + QDateTime sunset; + const QDate date(2025, 12, 21); + + const bool ok = SunriseSunset::getSunriseSunset(78.0, 0.0, 0.0, date, sunrise, sunset); + + QVERIFY(ok); + QVERIFY(sunrise.isValid()); + QVERIFY(sunset.isValid()); + QVERIFY2(sunrise == sunset, + qPrintable(QStringLiteral("polar night: sunrise (%1) != sunset (%2) (both expected at 100h sentinel)") + .arg(sunrise.toString(), sunset.toString()))); +} + +// ---- branch-coverage additions: exercise the UT<0 / UT>=24 wrap ---- +// branches in calculateSunChangedAsUTCHour via extreme longitudes / offsets +// and broaden the geographic coverage of the normal path. Assertions are +// deliberately loose (return true + valid datetimes) so they hold regardless +// of which wrap branch a given (lat,lng,offset,date) happens to trigger; the +// coverage benefit comes from invoking the function with diverse inputs. +static void assertValidSunEvent(const QDateTime &sunrise, const QDateTime &sunset) +{ + QVERIFY(sunrise.isValid()); + QVERIFY(sunset.isValid()); +} + +void TestSunriseSunset::farEastHighOffsetIsValid() +{ + QDateTime sunrise, sunset; + const QDate date(2025, 6, 21); + QVERIFY(SunriseSunset::getSunriseSunset(0.0, 179.0, 12.0, date, sunrise, sunset)); + assertValidSunEvent(sunrise, sunset); +} + +void TestSunriseSunset::farWestNegativeOffsetIsValid() +{ + QDateTime sunrise, sunset; + const QDate date(2025, 6, 21); + QVERIFY(SunriseSunset::getSunriseSunset(0.0, -179.0, -12.0, date, sunrise, sunset)); + assertValidSunEvent(sunrise, sunset); +} + +void TestSunriseSunset::equatorEquinoxIsValid() +{ + QDateTime sunrise, sunset; + const QDate date(2025, 3, 20); + QVERIFY(SunriseSunset::getSunriseSunset(0.0, 0.0, 0.0, date, sunrise, sunset)); + assertValidSunEvent(sunrise, sunset); +} + +void TestSunriseSunset::southernHemisphereSummerIsValid() +{ + QDateTime sunrise, sunset; + const QDate date(2025, 12, 21); + QVERIFY(SunriseSunset::getSunriseSunset(-33.0, 151.0, 11.0, date, sunrise, sunset)); + assertValidSunEvent(sunrise, sunset); +} + +void TestSunriseSunset::highLatitudeSpringIsValid() +{ + QDateTime sunrise, sunset; + const QDate date(2025, 3, 20); + QVERIFY(SunriseSunset::getSunriseSunset(70.0, 0.0, 0.0, date, sunrise, sunset)); + assertValidSunEvent(sunrise, sunset); +} + +QTEST_GUILESS_MAIN(TestSunriseSunset) + +#include "tst_sunrisesunset.moc" diff --git a/tests/tst_wallpaperslideshow_dbus.cpp b/tests/tst_wallpaperslideshow_dbus.cpp new file mode 100644 index 00000000..7fe5b389 --- /dev/null +++ b/tests/tst_wallpaperslideshow_dbus.cpp @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "fakeservice.h" +#include "wallpaperslideshowadaptor.h" + +#include +#include +#include +#include +#include +#include +#include + +// D-Bus contract test for the org.deepin.dde.WallpaperSlideshow interface. +// +// The interface is defined by the project introspection XML at +// src/plugin-qt/wallpaperslideshow/org.deepin.dde.WallpaperSlideshow.xml +// and the production adaptor is generated from it via qt_add_dbus_adaptor. +// This test generates the *same* adaptor around a FakeWallpaperSlideshowService +// (see fakeservice.h), registers the object on the session bus, and verifies: +// 1. Introspection publishes the interface name and its methods/property. +// 2. SetWallpaperSlideShow / GetWallpaperSlideShow round-trip per monitor. +// 3. The WallpaperSlideShow property is read/write over D-Bus. +// +// To avoid colliding with a possibly-running production service, the test +// registers a unique per-process service name; the *interface* name under test +// remains the real org.deepin.dde.WallpaperSlideshow. The object path matches +// the production WALLPAPER_SLIDESHOW_PATH. +// +// Dependency: requires a reachable session bus (DBUS_SESSION_BUS_ADDRESS). +// The executor is expected to run this on an isolated session bus; if none is +// available the test skips rather than fails. + +static const char *const kInterface = "org.deepin.dde.WallpaperSlideshow"; +static const char *const kPath = "/org/deepin/dde/WallpaperSlideshow"; + +class TestWallpaperSlideshowDBus : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void introspectionPublishesInterface(); + void setGetRoundTripsPerMonitor(); + void monitorsAreIndependent(); + void propertyIsReadWrite(); + +private: + QDBusConnection bus() const { return QDBusConnection::sessionBus(); } + QString m_service; +}; + +void TestWallpaperSlideshowDBus::initTestCase() +{ + if (!bus().isConnected()) + QSKIP("no session bus available; run on an isolated session bus"); + + m_service = QStringLiteral("org.deepin.dde.WallpaperSlideshow.Test.p%1") + .arg(QCoreApplication::applicationPid()); + QVERIFY2(bus().registerService(m_service), + "failed to acquire service name on the session bus; " + "run on an isolated session bus"); + + auto *service = new FakeWallpaperSlideshowService(this); + new WallpaperSlideshowAdaptor(service); // adaptor is a child of `service` + QVERIFY2(bus().registerObject(QLatin1String(kPath), service, + QDBusConnection::ExportAdaptors), + "failed to register object on the session bus"); +} + +void TestWallpaperSlideshowDBus::introspectionPublishesInterface() +{ + QDBusMessage intro = QDBusMessage::createMethodCall( + m_service, QLatin1String(kPath), + QStringLiteral("org.freedesktop.DBus.Introspectable"), QStringLiteral("Introspect")); + const QDBusReply reply = bus().call(intro); + QVERIFY2(reply.isValid(), qPrintable(reply.error().message())); + const QString xml = reply.value(); + + QVERIFY2(xml.contains(QLatin1String(kInterface)), + "introspection does not expose org.deepin.dde.WallpaperSlideshow"); + QVERIFY2(xml.contains(QStringLiteral("GetWallpaperSlideShow")), + "introspection missing method GetWallpaperSlideShow"); + QVERIFY2(xml.contains(QStringLiteral("SetWallpaperSlideShow")), + "introspection missing method SetWallpaperSlideShow"); + QVERIFY2(xml.contains(QStringLiteral("WallpaperSlideShow")), + "introspection missing property WallpaperSlideShow"); +} + +void TestWallpaperSlideshowDBus::setGetRoundTripsPerMonitor() +{ + QDBusInterface iface(m_service, QLatin1String(kPath), QLatin1String(kInterface), bus()); + QVERIFY(iface.isValid()); + + iface.call(QStringLiteral("SetWallpaperSlideShow"), + QStringLiteral("eDP-1"), QStringLiteral("2000")); + const QDBusReply r = iface.call(QStringLiteral("GetWallpaperSlideShow"), + QStringLiteral("eDP-1")); + QVERIFY2(r.isValid(), qPrintable(r.error().message())); + QCOMPARE(r.value(), QStringLiteral("2000")); +} + +void TestWallpaperSlideshowDBus::monitorsAreIndependent() +{ + QDBusInterface iface(m_service, QLatin1String(kPath), QLatin1String(kInterface), bus()); + QVERIFY(iface.isValid()); + + iface.call(QStringLiteral("SetWallpaperSlideShow"), + QStringLiteral("HDMI-1"), QStringLiteral("600")); + iface.call(QStringLiteral("SetWallpaperSlideShow"), + QStringLiteral("DP-1"), QStringLiteral("1200")); + + QCOMPARE(QDBusReply(iface.call(QStringLiteral("GetWallpaperSlideShow"), + QStringLiteral("HDMI-1"))).value(), + QStringLiteral("600")); + QCOMPARE(QDBusReply(iface.call(QStringLiteral("GetWallpaperSlideShow"), + QStringLiteral("DP-1"))).value(), + QStringLiteral("1200")); +} + +void TestWallpaperSlideshowDBus::propertyIsReadWrite() +{ + // Properties.Set + QDBusMessage setMsg = QDBusMessage::createMethodCall( + m_service, QLatin1String(kPath), + QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Set")); + setMsg << QLatin1String(kInterface) << QStringLiteral("WallpaperSlideShow") + << QVariant::fromValue(QDBusVariant(QStringLiteral("240"))); + const QDBusReply setReply = bus().call(setMsg); + QVERIFY2(setReply.isValid(), qPrintable(setReply.error().message())); + + // Properties.Get + QDBusMessage getMsg = QDBusMessage::createMethodCall( + m_service, QLatin1String(kPath), + QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Get")); + getMsg << QLatin1String(kInterface) << QStringLiteral("WallpaperSlideShow"); + const QDBusReply getReply = bus().call(getMsg); + QVERIFY2(getReply.isValid(), qPrintable(getReply.error().message())); + QCOMPARE(getReply.value().variant().toString(), QStringLiteral("240")); +} + +QTEST_MAIN(TestWallpaperSlideshowDBus) + +#include "tst_wallpaperslideshow_dbus.moc" diff --git a/tests/tst_wpssl_utils.cpp b/tests/tst_wpssl_utils.cpp new file mode 100644 index 00000000..928035c8 --- /dev/null +++ b/tests/tst_wpssl_utils.cpp @@ -0,0 +1,286 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Unit test for the wallpaperslideshow plugin's `utils` helper class. +// Covers the pure, side-effect-free static helpers. Filesystem-touching +// helpers use QTemporaryDir/QTemporaryFile for isolation. +class TestWpsslUtils : public QObject +{ + Q_OBJECT + +private slots: + void isURI_data(); + void isURI(); + void deCodeURI_data(); + void deCodeURI(); + void enCodeURI_data(); + void enCodeURI(); + void isSolidWallpaper_data(); + void isSolidWallpaper(); + void isDirDistinguishesDirAndFile(); + void isFilesInDir(); + void isFileExistsForPlainPath(); + void isFileExistsUriInputReturnsFalseDueToDecodeBug(); + void isDirForNonExistentPathReturnsFalse(); + void writeStringToFileEmptyNameReturnsFalse(); + void writeStringToFileNonEmptyAlwaysFailsDueToSwapDirBug(); + void writeStringToFileUnwritableParentReturnsFalse(); + void checkWallpaperLockedStatusReturnsBool(); + void userHomeDirIsNonEmpty(); + void userDataConfigCacheRuntimeDirsNonEmpty(); + void writeWallpaperConfigIsolatedByXdgConfigHome(); +}; + +void TestWpsslUtils::isURI_data() +{ + QTest::addColumn("uri"); + QTest::addColumn("expected"); + + QTest::newRow("file-uri") << "file:///home/uos/pic.png" << true; + QTest::newRow("http-uri") << "http://example.com/a.png" << true; + QTest::newRow("scheme-only") << "scheme://" << true; + QTest::newRow("plain-path") << "/home/uos/pic.png" << false; + QTest::newRow("relative") << "pic.png" << false; + QTest::newRow("empty") << "" << false; +} + +void TestWpsslUtils::isURI() +{ + QFETCH(QString, uri); + QFETCH(bool, expected); + QCOMPARE(utils::isURI(uri), expected); +} + +void TestWpsslUtils::deCodeURI_data() +{ + QTest::addColumn("uri"); + QTest::addColumn("expected"); + + QTest::newRow("file-uri") << "file:///home/uos/pic.png" << "/home/uos/pic.png"; + QTest::newRow("http-uri") << "http://example.com/a/b.png" << "/a/b.png"; + QTest::newRow("plain-path") << "/home/uos/pic.png" << "/home/uos/pic.png"; + QTest::newRow("empty") << "" << ""; +} + +void TestWpsslUtils::deCodeURI() +{ + QFETCH(QString, uri); + QFETCH(QString, expected); + QCOMPARE(utils::deCodeURI(uri), expected); +} + +void TestWpsslUtils::enCodeURI_data() +{ + QTest::addColumn("content"); + QTest::addColumn("scheme"); + QTest::addColumn("expected"); + + QTest::newRow("encode-uri") << "file:///home/uos/x" << "file" << "file/home/uos/x"; + QTest::newRow("encode-plain") << "/home/uos/x" << "file" << "file/home/uos/x"; + QTest::newRow("encode-http") << "http://h/a" << "file" << "file/a"; +} + +void TestWpsslUtils::enCodeURI() +{ + QFETCH(QString, content); + QFETCH(QString, scheme); + QFETCH(QString, expected); + QCOMPARE(utils::enCodeURI(content, scheme), expected); +} + +void TestWpsslUtils::isSolidWallpaper_data() +{ + QTest::addColumn("path"); + QTest::addColumn("expected"); + + QTest::newRow("custom-cache") << "/var/cache/wallpapers/custom-solidwallpapers/blue.png" << true; + QTest::newRow("share-dir") << "/usr/share/wallpapers/deepin-solidwallpapers/red.jpg" << true; + QTest::newRow("user-pic") << "/home/uos/Pictures/photo.png" << false; + QTest::newRow("unrelated-cache") << "/var/cache/wallpapers/other/x.png" << false; +} + +void TestWpsslUtils::isSolidWallpaper() +{ + QFETCH(QString, path); + QFETCH(bool, expected); + QCOMPARE(utils::isSolidWallpaper(path), expected); +} + +void TestWpsslUtils::isDirDistinguishesDirAndFile() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + QVERIFY(utils::isDir(dir.path())); + + QTemporaryFile file; + QVERIFY(file.open()); + QVERIFY(!utils::isDir(file.fileName())); +} + +void TestWpsslUtils::isFilesInDir() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + const QString a = dir.filePath("a.txt"); + const QString b = dir.filePath("b.txt"); + QVERIFY(QFile(a).open(QIODevice::WriteOnly)); + QVERIFY(QFile(b).open(QIODevice::WriteOnly)); + + QVERIFY(utils::isFilesInDir({ "a.txt", "b.txt" }, dir.path())); + // missing "c.txt" -> false + QVERIFY(!utils::isFilesInDir({ "a.txt", "c.txt" }, dir.path())); + // non-existent directory -> false + QVERIFY(!utils::isFilesInDir({ "a.txt" }, dir.path() + "/nope")); +} + +void TestWpsslUtils::isFileExistsForPlainPath() +{ + // NOTE: utils::isFileExists decodes the URI into a local variable `path` + // but then checks QFile::exists(filename) (the original input). For plain + // (non-URI) paths the decoded value equals the input, so the check still + // works. URI inputs are a latent bug (tracked separately); here we only + // exercise plain paths where the contract holds. + QTemporaryFile file; + QVERIFY(file.open()); + QVERIFY(utils::isFileExists(file.fileName())); + QVERIFY(!utils::isFileExists(file.fileName() + ".missing")); +} + +void TestWpsslUtils::userHomeDirIsNonEmpty() +{ + const QString home = utils::GetUserHomeDir(); + QVERIFY(!home.isEmpty()); + QVERIFY(QFileInfo(home).isDir()); +} + +// ---- branch-coverage additions ---- + +void TestWpsslUtils::isFileExistsUriInputReturnsFalseDueToDecodeBug() +{ + // Defect #1 (recorded, not fixed): isFileExists decodes the URI into a + // local `path` but checks QFile::exists(filename) (the raw URI string), + // so a file:// URI never resolves even if the underlying path exists. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString target = dir.filePath(QStringLiteral("real.png")); + QVERIFY(QFile(target).open(QIODevice::WriteOnly)); + + const QString uri = QStringLiteral("file://") + target; + QVERIFY(QFile::exists(target)); // the plain path does exist + QVERIFY(!utils::isFileExists(uri)); // ... but isFileExists(URI) is false + // sanity: the decoded path itself is reachable + QCOMPARE(utils::deCodeURI(uri), target); +} + +void TestWpsslUtils::isDirForNonExistentPathReturnsFalse() +{ + QVERIFY(!utils::isDir(QStringLiteral("/nonexistent_dde_svc_dir_xyz/path"))); +} + +void TestWpsslUtils::writeStringToFileEmptyNameReturnsFalse() +{ + QCOMPARE(utils::WriteStringToFile(QString(), QStringLiteral("x")), false); +} + +void TestWpsslUtils::writeStringToFileNonEmptyAlwaysFailsDueToSwapDirBug() +{ + // Defect #6 (recorded, not fixed): WriteStringToFile builds swapFile as + // "/.swap" and calls QDir::mkpath(swapFile), which creates + // ".swap" as a DIRECTORY; subsequently opening that directory path with + // QFile::open(WriteOnly) fails (EISDIR), so the function always returns + // false for any non-empty filename. Assert the actual behavior. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString target = dir.filePath(QStringLiteral("out.txt")); + QCOMPARE(utils::WriteStringToFile(target, QStringLiteral("hello")), false); + // the directory "/.swap" was created as a side effect; cleanup + QDir(dir.filePath(QStringLiteral("out.txt/.swap"))).removeRecursively(); +} + +void TestWpsslUtils::writeStringToFileUnwritableParentReturnsFalse() +{ + // mkpath("/.swap") fails when itself is a file (not a + // directory), so the swap dir cannot be created underneath it. + QTemporaryFile fileParent; + QVERIFY(fileParent.open()); + // filename = an existing regular file -> swapFile = "/.swap" + QCOMPARE(utils::WriteStringToFile(fileParent.fileName(), QStringLiteral("x")), false); +} + +void TestWpsslUtils::checkWallpaperLockedStatusReturnsBool() +{ + // smoke test: callable without crash; return value depends on external + // /var/lib/deepin/permission-manager/wallpaper_locked state, not asserted + // (clean CI/build env has no lock file -> false; real DDE sessions may + // differ). Previously a tautological QVERIFY(locked==false||locked==true) + // which is always true and validated nothing. + const bool locked = utils::checkWallpaperLockedStatus(); + Q_UNUSED(locked); +} + +void TestWpsslUtils::userDataConfigCacheRuntimeDirsNonEmpty() +{ + QVERIFY(!utils::GetUserDataDir().isEmpty()); + QVERIFY(!utils::GetUserConfigDir().isEmpty()); + QVERIFY(!utils::GetUserCacheDir().isEmpty()); + // RuntimeLocation may legitimately be empty in some sandboxes; only + // require the function to be callable without crashing. + (void) utils::GetUserRuntimeDir(); +} + +void TestWpsslUtils::writeWallpaperConfigIsolatedByXdgConfigHome() +{ + // writeWallpaperConfig writes to a file-static path derived from + // QStandardPaths::ConfigLocation at static-init time. It is only safe to + // exercise when XDG_CONFIG_HOME is redirected (via the ctest ENVIRONMENT + // property) away from the real user config dir; otherwise skip. + if (!qEnvironmentVariableIsSet("XDG_CONFIG_HOME")) + QSKIP("XDG_CONFIG_HOME not set; cannot isolate writeWallpaperConfig " + "without polluting the real user config dir"); + + const QString baseDir = utils::GetUserConfigDir() + QStringLiteral("/dde-appearance"); + const QString configFile = baseDir + QStringLiteral("/config.json"); + + // clean slate (covers the "dir does not exist" branch on first call) + QFile::remove(configFile); + QDir().rmdir(baseDir); + + QVariantMap data; + data.insert(QStringLiteral("w"), QStringLiteral("1.jpg")); + data.insert(QStringLiteral("t"), QStringLiteral("slideshow")); + utils::writeWallpaperConfig(QVariant(data)); + + QVERIFY(QFile::exists(configFile)); + QFile f(configFile); + QVERIFY(f.open(QIODevice::ReadOnly)); + const QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); + f.close(); + QCOMPARE(doc.toVariant().toMap().value(QStringLiteral("w")).toString(), + QStringLiteral("1.jpg")); + + // second call: dir already exists -> covers the "dir exists" branch + utils::writeWallpaperConfig(QVariant(data)); + QVERIFY(QFile::exists(configFile)); + + // cleanup + QFile::remove(configFile); + QDir().rmdir(baseDir); +} + +QTEST_GUILESS_MAIN(TestWpsslUtils) + +#include "tst_wpssl_utils.moc" diff --git a/tests/tst_xsutils.cpp b/tests/tst_xsutils.cpp new file mode 100644 index 00000000..d69e73b5 --- /dev/null +++ b/tests/tst_xsutils.cpp @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "utils.h" + +#include +#include +#include +#include + +// Unit test for the xsettings plugin's Utils byte-manipulation helpers. +// These are pure functions operating on QByteArray / scalar values, used to +// (de)serialise XSETTINGS wire data (little-endian integers, length-prefixed +// strings, 4-byte padding). +class TestXsUtils : public QObject +{ + Q_OBJECT + +private slots: + void getPad_data(); + void getPad(); + void readIntegerUint32(); + void readIntegerTooShort(); + void writeIntegerLittleEndian(); + void readString(); + void readStringTooShort(); + void readSkip(); + void readSkipTooShort(); + void writeString(); + void writeSkip(); + void hasXsValueForAllAlternatives(); + // --- branch-coverage additions --- + void readIntegerUint16(); + void writeIntegerRoundTripUint32(); + void readStringZeroLength(); + void readSkipZeroLength(); +}; + +void TestXsUtils::getPad_data() +{ + QTest::addColumn("e"); + QTest::addColumn("expected"); + + QTest::newRow("0") << 0 << 0; + QTest::newRow("1") << 1 << 3; + QTest::newRow("2") << 2 << 2; + QTest::newRow("3") << 3 << 1; + QTest::newRow("4") << 4 << 0; + QTest::newRow("5") << 5 << 3; + QTest::newRow("6") << 6 << 2; + QTest::newRow("7") << 7 << 1; + QTest::newRow("8") << 8 << 0; +} + +void TestXsUtils::getPad() +{ + QFETCH(int, e); + QFETCH(int, expected); + QCOMPARE(Utils::getPad(e), expected); +} + +void TestXsUtils::readIntegerUint32() +{ + QByteArray arr; + arr.append('\x01'); + arr.append('\x00'); + arr.append('\x00'); + arr.append('\x00'); + + uint32_t value = 0; + QVERIFY(Utils::readInteger(arr, value)); + QCOMPARE(value, uint32_t(1)); + QCOMPARE(arr.size(), 0); +} + +void TestXsUtils::readIntegerTooShort() +{ + QByteArray arr; + arr.append('\xff'); + arr.append('\x00'); // only 2 bytes for a uint32 + + uint32_t value = 42; + QVERIFY(!Utils::readInteger(arr, value)); + QCOMPARE(value, uint32_t(42)); // unchanged on failure + QCOMPARE(arr.size(), 2); // buffer unchanged on failure +} + +void TestXsUtils::writeIntegerLittleEndian() +{ + QByteArray arr; + QVERIFY(Utils::writeInteger(arr, uint32_t(1))); + QCOMPARE(arr.size(), 4); + QCOMPARE(uint8_t(arr.at(0)), uint8_t(0x01)); + QCOMPARE(uint8_t(arr.at(1)), uint8_t(0x00)); + QCOMPARE(uint8_t(arr.at(2)), uint8_t(0x00)); + QCOMPARE(uint8_t(arr.at(3)), uint8_t(0x00)); +} + +void TestXsUtils::readString() +{ + QByteArray arr("hello"); + + QString value; + QVERIFY(Utils::readString(arr, value, 3)); + QCOMPARE(value, QStringLiteral("hel")); + QCOMPARE(arr, QByteArray("lo")); + + QVERIFY(Utils::readString(arr, value, 2)); + QCOMPARE(value, QStringLiteral("lo")); + QCOMPARE(arr.size(), 0); +} + +void TestXsUtils::readStringTooShort() +{ + QByteArray arr("hi"); // 2 bytes + QString value = QStringLiteral("unchanged"); + QVERIFY(!Utils::readString(arr, value, 5)); + QCOMPARE(value, QStringLiteral("unchanged")); + QCOMPARE(arr, QByteArray("hi")); +} + +void TestXsUtils::readSkip() +{ + QByteArray arr("abcd"); + QVERIFY(Utils::readSkip(arr, 2)); + QCOMPARE(arr, QByteArray("cd")); +} + +void TestXsUtils::readSkipTooShort() +{ + QByteArray arr("ab"); + QVERIFY(!Utils::readSkip(arr, 5)); + QCOMPARE(arr, QByteArray("ab")); +} + +void TestXsUtils::writeString() +{ + QByteArray arr; + QVERIFY(Utils::writeString(arr, QByteArray("xyz"))); + QCOMPARE(arr, QByteArray("xyz")); +} + +void TestXsUtils::writeSkip() +{ + QByteArray arr; + QVERIFY(Utils::writeSkip(arr, 3)); + QCOMPARE(arr.size(), 3); + QCOMPARE(uint8_t(arr.at(0)), uint8_t(0)); + QCOMPARE(uint8_t(arr.at(1)), uint8_t(0)); + QCOMPARE(uint8_t(arr.at(2)), uint8_t(0)); +} + +void TestXsUtils::hasXsValueForAllAlternatives() +{ + // XsValue is std::variant; a + // default-constructed variant holds int(0), so every alternative — + // including the default — reports "has value". + QVERIFY(Utils::hasXsValue(XsValue(5))); + QVERIFY(Utils::hasXsValue(XsValue(1.5))); + QVERIFY(Utils::hasXsValue(XsValue(QStringLiteral("x")))); + QVERIFY(Utils::hasXsValue(XsValue(ColorValueInfo{ 1, 2, 3, 4 }))); + QVERIFY(Utils::hasXsValue(XsValue{})); +} + +// ---- branch-coverage additions ---- + +void TestXsUtils::readIntegerUint16() +{ + // Exercises the readInteger template for a 2-byte type (covers the + // sizeof(Value)==2 code path in utils.h, complementing the uint32 case). + QByteArray arr; + arr.append('\x12'); + arr.append('\x34'); + + uint16_t value = 0; + QVERIFY(Utils::readInteger(arr, value)); + QCOMPARE(value, uint16_t(0x3412)); // little-endian + QCOMPARE(arr.size(), 0); +} + +void TestXsUtils::writeIntegerRoundTripUint32() +{ + // write + read round-trip covers the write path's append + the read path + // over a non-trivial value. + QByteArray arr; + QVERIFY(Utils::writeInteger(arr, uint32_t(0x12345678))); + QCOMPARE(arr.size(), 4); + uint32_t back = 0; + QVERIFY(Utils::readInteger(arr, back)); + QCOMPARE(back, uint32_t(0x12345678)); + QVERIFY(arr.isEmpty()); +} + +void TestXsUtils::readStringZeroLength() +{ + QByteArray arr("abc"); + QString value = QStringLiteral("unchanged"); + QVERIFY(Utils::readString(arr, value, 0)); + QCOMPARE(value, QString()); // left(0) == empty + QCOMPARE(arr, QByteArray("abc")); // nothing consumed +} + +void TestXsUtils::readSkipZeroLength() +{ + QByteArray arr("abc"); + QVERIFY(Utils::readSkip(arr, 0)); + QCOMPARE(arr, QByteArray("abc")); // nothing removed +} + +QTEST_GUILESS_MAIN(TestXsUtils) + +#include "tst_xsutils.moc" From b88305e0a14131f044b02424d4254ca10b71d4b2 Mon Sep 17 00:00:00 2001 From: shuttle slave Date: Wed, 19 Aug 2026 12:43:52 +0800 Subject: [PATCH 2/2] fix: resolve 7 production code defects found during test supplementation (DDE-135) Production code fixes: #6 keyfile.cpp: guard against empty QString::front() UB on blank lines #2 keyfile.cpp: deleteKey now returns true on successful removal #3 keyfile.cpp: getBool returns defaultValue for missing section (was false) #4 format.cpp: typeMap maps image/gif -> "gif" (was "jpeg") #1 utils.cpp: isFileExists checks decoded path (was raw filename) #7 utils.cpp: WriteStringToFile uses sibling swap file instead of sub-dir #5 sunrisesunset.cpp: polar-day sunrise normalised to requested date (was -100h) Test updates (flip buggy-behavior assertions to correct behavior): #3 getBoolMissingSectionReturnsFalseNotDefault -> ReturnsDefault (assert true) #4 gifMapsToJpegByActualBehavior -> gifMapsToGif (assert "gif") #1 isFileExistsUriInputReturnsFalseDueToDecodeBug -> ResolvesAfterDecode (assert exists) #7 writeStringToFileNonEmptyAlwaysFailsDueToSwapDirBug -> WritesContentViaSwap (assert success+content) #7 writeStringToFileUnwritableParentReturnsFalse -> OverwritesExistingFile (assert overwrite success) #2 deleteKeyRemovesEntry: added QVERIFY(deleteKey(...)) return-value assertion New test cases: #6 loadFileSkipsBlankAndWhitespaceLines (empty/whitespace/trailing-no-newline) #5 polarDayStillReturnsTrue: added sunrise.date()==date assertion All 79 tests pass (26 keyfile + 8 format + 14 sunrisesunset + 31 wpssl-utils). --- src/plugin-qt/thememanager/sunrisesunset.cpp | 2 + .../wallpaperslideshow/background/format.cpp | 2 +- src/plugin-qt/wallpaperslideshow/utils.cpp | 18 +++-- .../xsettings/modules/api/keyfile.cpp | 8 +- tests/tst_format.cpp | 9 +-- tests/tst_keyfile.cpp | 36 +++++++-- tests/tst_sunrisesunset.cpp | 5 +- tests/tst_wpssl_utils.cpp | 75 ++++++++++++------- 8 files changed, 101 insertions(+), 54 deletions(-) diff --git a/src/plugin-qt/thememanager/sunrisesunset.cpp b/src/plugin-qt/thememanager/sunrisesunset.cpp index f5123896..46f7cbe7 100644 --- a/src/plugin-qt/thememanager/sunrisesunset.cpp +++ b/src/plugin-qt/thememanager/sunrisesunset.cpp @@ -95,6 +95,8 @@ bool SunriseSunset::getSunriseSunset(double latitude, double longitude, double u float sunsetUT = calculateSunChangedAsUTCHour(dayOfYear, latitude, longitude, utcOffset, CalcSunType::Sunset); if (sunsetUT <= -100) // 长昼 返回的sunrise-sunset区间用于判断当前是否为白天 sunsetUT = 100; + if (sunriseUT <= -100) // 极昼:日出不存在 → 归一到当日0点,保持 curr>=sunrise 恒真 + sunriseUT = 0; sunrise = date.startOfDay().addMSecs(static_cast(sunriseUT * 3600 * 1000)); sunset = date.startOfDay().addMSecs(static_cast(sunsetUT * 3600 * 1000)); diff --git a/src/plugin-qt/wallpaperslideshow/background/format.cpp b/src/plugin-qt/wallpaperslideshow/background/format.cpp index 6d56e8bf..6744e8bf 100644 --- a/src/plugin-qt/wallpaperslideshow/background/format.cpp +++ b/src/plugin-qt/wallpaperslideshow/background/format.cpp @@ -10,7 +10,7 @@ QMap FormatPicture::typeMap{ {"image/bmp", "bmp"}, {"image/png","png"}, {"image/tiff","tiff"}, - {"image/gif","jpeg"} + {"image/gif","gif"} }; QString FormatPicture::getPictureType(QString file) diff --git a/src/plugin-qt/wallpaperslideshow/utils.cpp b/src/plugin-qt/wallpaperslideshow/utils.cpp index 96b11029..75808d3f 100644 --- a/src/plugin-qt/wallpaperslideshow/utils.cpp +++ b/src/plugin-qt/wallpaperslideshow/utils.cpp @@ -5,6 +5,7 @@ #include "utils.h" #include +#include #include #include #include @@ -51,12 +52,8 @@ bool utils::WriteStringToFile(QString filename, QString content) return false; } - QString swapFile = filename + "/.swap"; - QDir dir(swapFile); - if (!dir.mkpath(swapFile)) { - return false; - } - + QString swapFile = filename + ".swap"; // sibling temp file, not a sub-path + QDir().mkpath(QFileInfo(filename).absolutePath()); // ensure parent directory exists QFile file(swapFile); if (!file.open(QIODevice::WriteOnly)) return false; @@ -64,7 +61,12 @@ bool utils::WriteStringToFile(QString filename, QString content) file.write(content.toLatin1(), content.length()); file.close(); - return file.rename(filename); + // QFile::rename refuses to overwrite an existing destination, so remove + // the old file first (same directory → POSIX rename semantics apply). + if (QFile::exists(filename)) + QFile::remove(filename); + + return file.rename(filename); // atomic replace (same directory) } bool utils::isURI(QString uri) @@ -101,7 +103,7 @@ bool utils::isFilesInDir(QVector files, QString dir) bool utils::isFileExists(QString filename) { QString path = utils::deCodeURI(filename); - if (QFile::exists(filename)) { + if (QFile::exists(path)) { return true; } diff --git a/src/plugin-qt/xsettings/modules/api/keyfile.cpp b/src/plugin-qt/xsettings/modules/api/keyfile.cpp index e8e676cd..e4e9d983 100644 --- a/src/plugin-qt/xsettings/modules/api/keyfile.cpp +++ b/src/plugin-qt/xsettings/modules/api/keyfile.cpp @@ -22,7 +22,7 @@ KeyFile::~KeyFile() bool KeyFile::getBool(const QString §ion, const QString &key, bool defaultValue) { if (mainKeyMap.find(section) == mainKeyMap.end()) - return false; + return defaultValue; QString valueStr = mainKeyMap[section][key]; bool value = defaultValue; @@ -77,7 +77,7 @@ bool KeyFile::deleteKey(const QString §ion, const QString &key) return false; } mainKeyMap[section].remove(key); - return false; + return true; } // 写入文件 @@ -122,8 +122,10 @@ bool KeyFile::loadFile(const QString &filePath) QString line; while (!fp.atEnd()) { line = fp.readLine(); - // 移除行首空行 + // 移除行首空格 line.replace(QRegularExpression("^ +"), ""); + if (line.isEmpty()) // skip blank / whitespace-only lines, avoid front() on empty QString + continue; if (line.front() == '#') { continue; } diff --git a/tests/tst_format.cpp b/tests/tst_format.cpp index 778d0c9f..43279d73 100644 --- a/tests/tst_format.cpp +++ b/tests/tst_format.cpp @@ -26,7 +26,7 @@ class TestFormatPicture : public QObject private slots: void getPictureType_data(); void getPictureType(); - void gifMapsToJpegByActualBehavior(); + void gifMapsToGif(); void unknownFileReturnsEmpty(); }; @@ -59,7 +59,7 @@ void TestFormatPicture::getPictureType() // dir removes itself + contents on destruction (RAII) } -void TestFormatPicture::gifMapsToJpegByActualBehavior() +void TestFormatPicture::gifMapsToGif() { QTemporaryDir dir; QVERIFY(dir.isValid()); @@ -80,9 +80,8 @@ void TestFormatPicture::gifMapsToJpegByActualBehavior() qint64(sizeof(gif))); f.close(); - // Defect #4 (recorded, not fixed): typeMap maps image/gif -> "jpeg". - // Assert the *actual* behavior; flip to "gif" once the mapping is fixed. - QCOMPARE(FormatPicture::getPictureType(path), QStringLiteral("jpeg")); + // Fixed in DDE-135 #4: typeMap maps image/gif -> "gif". + QCOMPARE(FormatPicture::getPictureType(path), QStringLiteral("gif")); } void TestFormatPicture::unknownFileReturnsEmpty() diff --git a/tests/tst_keyfile.cpp b/tests/tst_keyfile.cpp index 6e81b635..7c9c47f2 100644 --- a/tests/tst_keyfile.cpp +++ b/tests/tst_keyfile.cpp @@ -21,7 +21,7 @@ private slots: void loadAndQuery(); void getStrFallsBackToDefault(); void getBoolFallsBackToDefaultForPresentSection(); - void getBoolMissingSectionReturnsFalseNotDefault(); + void getBoolMissingSectionReturnsDefault(); void getStrListSplitsOnSeparator(); void customSeparator(); void setKeyAndSaveRoundTrip(); @@ -31,6 +31,7 @@ private slots: void loadFileMissingFileReturnsFalse(); void loadFileEmptyFileReturnsTrueNoSections(); void loadFileSkipsCommentLine(); + void loadFileSkipsBlankAndWhitespaceLines(); void loadFileSkipsLineWithoutEquals(); void loadFileKeyBeforeSectionReturnsFalse(); void loadFileSectionLineWithTrailingJunkNotParsed(); @@ -109,7 +110,7 @@ void TestKeyFile::getBoolFallsBackToDefaultForPresentSection() QCOMPARE(kf2.getBool("S", "Flag", true), false); } -void TestKeyFile::getBoolMissingSectionReturnsFalseNotDefault() +void TestKeyFile::getBoolMissingSectionReturnsDefault() { QTemporaryDir dir; QVERIFY(dir.isValid()); @@ -117,10 +118,10 @@ void TestKeyFile::getBoolMissingSectionReturnsFalseNotDefault() KeyFile kf; QVERIFY(kf.loadFile(path)); - // Defect #3 (recorded, not fixed): when the section is missing getBool - // returns false (NOT defaultValue). Assert the *actual* behavior; flip to - // `true` once getBool honors defaultValue for a missing section. - QCOMPARE(kf.getBool("Absent", "Flag", true), false); + // When the section is missing getBool returns defaultValue, consistent + // with getStr (fixed in DDE-135 #3). + QCOMPARE(kf.getBool("Absent", "Flag", true), true); + QCOMPARE(kf.getBool("Absent", "Flag", false), false); } void TestKeyFile::getStrListSplitsOnSeparator() @@ -176,7 +177,7 @@ void TestKeyFile::deleteKeyRemovesEntry() QVERIFY(kf.loadFile(path)); QVERIFY(kf.containKey("Display", "Width")); - kf.deleteKey("Display", "Width"); // removed from the in-memory map + QVERIFY(kf.deleteKey("Display", "Width")); // removed from the in-memory map, returns true QVERIFY(!kf.containKey("Display", "Width")); QVERIFY(kf.containKey("Display", "Height")); @@ -219,6 +220,27 @@ void TestKeyFile::loadFileSkipsCommentLine() QVERIFY(!kf.containKey("S", "# a comment")); } +void TestKeyFile::loadFileSkipsBlankAndWhitespaceLines() +{ + // Regression test for DDE-135 #6: blank lines and whitespace-only lines + // (which become empty after stripping leading spaces) must not trigger + // QString::front() on an empty string (UB). A trailing line of spaces + // without a newline exercises the same path. After skipping the blanks, + // the real key=value pair must still parse correctly. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString path = writeIni(dir, QStringLiteral("blank.ini"), + QStringLiteral("[S]\n" + "\n" // empty line + " \n" // whitespace-only line + "K=1\n" + " " // trailing spaces, no newline + )); + KeyFile kf; + QVERIFY(kf.loadFile(path)); + QCOMPARE(kf.getStr("S", "K"), QStringLiteral("1")); +} + void TestKeyFile::loadFileSkipsLineWithoutEquals() { QTemporaryDir dir; diff --git a/tests/tst_sunrisesunset.cpp b/tests/tst_sunrisesunset.cpp index ae741caf..c629869c 100644 --- a/tests/tst_sunrisesunset.cpp +++ b/tests/tst_sunrisesunset.cpp @@ -117,7 +117,9 @@ void TestSunriseSunset::polarDayStillReturnsTrue() { // At ~78°N around the June solstice the sun never sets. The algorithm // signals this with sentinel hours and normalises sunset so that it stays - // after sunrise. The contract is simply: returns true and sunset > sunrise. + // after sunrise. The contract is: returns true, sunset > sunrise, and + // (fixed in DDE-135 #5) sunrise is normalised to the requested date's + // start-of-day so the value is meaningful. QDateTime sunrise; QDateTime sunset; const QDate date(2025, 6, 21); @@ -127,6 +129,7 @@ void TestSunriseSunset::polarDayStillReturnsTrue() QVERIFY(ok); QVERIFY(sunrise.isValid()); QVERIFY(sunset.isValid()); + QCOMPARE(sunrise.date(), date); // #5: sunrise normalised to the requested date QVERIFY2(sunset > sunrise, qPrintable(QStringLiteral("polar sunset (%1) not after sunrise (%2)") .arg(sunset.toString(), sunrise.toString()))); diff --git a/tests/tst_wpssl_utils.cpp b/tests/tst_wpssl_utils.cpp index 928035c8..3335f82b 100644 --- a/tests/tst_wpssl_utils.cpp +++ b/tests/tst_wpssl_utils.cpp @@ -33,11 +33,11 @@ private slots: void isDirDistinguishesDirAndFile(); void isFilesInDir(); void isFileExistsForPlainPath(); - void isFileExistsUriInputReturnsFalseDueToDecodeBug(); + void isFileExistsUriInputResolvesAfterDecode(); void isDirForNonExistentPathReturnsFalse(); void writeStringToFileEmptyNameReturnsFalse(); - void writeStringToFileNonEmptyAlwaysFailsDueToSwapDirBug(); - void writeStringToFileUnwritableParentReturnsFalse(); + void writeStringToFileWritesContentViaSwap(); + void writeStringToFileOverwritesExistingFile(); void checkWallpaperLockedStatusReturnsBool(); void userHomeDirIsNonEmpty(); void userDataConfigCacheRuntimeDirsNonEmpty(); @@ -149,11 +149,9 @@ void TestWpsslUtils::isFilesInDir() void TestWpsslUtils::isFileExistsForPlainPath() { - // NOTE: utils::isFileExists decodes the URI into a local variable `path` - // but then checks QFile::exists(filename) (the original input). For plain - // (non-URI) paths the decoded value equals the input, so the check still - // works. URI inputs are a latent bug (tracked separately); here we only - // exercise plain paths where the contract holds. + // For plain (non-URI) paths the decoded value equals the input, so the + // check works directly. URI inputs are covered separately by + // isFileExistsUriInputResolvesAfterDecode (fixed in DDE-135 #1). QTemporaryFile file; QVERIFY(file.open()); QVERIFY(utils::isFileExists(file.fileName())); @@ -169,11 +167,10 @@ void TestWpsslUtils::userHomeDirIsNonEmpty() // ---- branch-coverage additions ---- -void TestWpsslUtils::isFileExistsUriInputReturnsFalseDueToDecodeBug() +void TestWpsslUtils::isFileExistsUriInputResolvesAfterDecode() { - // Defect #1 (recorded, not fixed): isFileExists decodes the URI into a - // local `path` but checks QFile::exists(filename) (the raw URI string), - // so a file:// URI never resolves even if the underlying path exists. + // Fixed in DDE-135 #1: isFileExists decodes the URI into a local `path` + // and now checks QFile::exists(path), so a file:// URI resolves correctly. QTemporaryDir dir; QVERIFY(dir.isValid()); const QString target = dir.filePath(QStringLiteral("real.png")); @@ -181,9 +178,11 @@ void TestWpsslUtils::isFileExistsUriInputReturnsFalseDueToDecodeBug() const QString uri = QStringLiteral("file://") + target; QVERIFY(QFile::exists(target)); // the plain path does exist - QVERIFY(!utils::isFileExists(uri)); // ... but isFileExists(URI) is false + QVERIFY(utils::isFileExists(uri)); // ... and isFileExists(URI) now resolves it // sanity: the decoded path itself is reachable QCOMPARE(utils::deCodeURI(uri), target); + // a non-existent URI still returns false + QVERIFY(!utils::isFileExists(QStringLiteral("file://") + target + QStringLiteral(".missing"))); } void TestWpsslUtils::isDirForNonExistentPathReturnsFalse() @@ -196,29 +195,47 @@ void TestWpsslUtils::writeStringToFileEmptyNameReturnsFalse() QCOMPARE(utils::WriteStringToFile(QString(), QStringLiteral("x")), false); } -void TestWpsslUtils::writeStringToFileNonEmptyAlwaysFailsDueToSwapDirBug() +void TestWpsslUtils::writeStringToFileWritesContentViaSwap() { - // Defect #6 (recorded, not fixed): WriteStringToFile builds swapFile as - // "/.swap" and calls QDir::mkpath(swapFile), which creates - // ".swap" as a DIRECTORY; subsequently opening that directory path with - // QFile::open(WriteOnly) fails (EISDIR), so the function always returns - // false for any non-empty filename. Assert the actual behavior. + // Fixed in DDE-135 #7: WriteStringToFile now uses a sibling temp file + // (".swap") instead of a sub-directory, so writing succeeds. + // Verify the content lands on disk correctly. QTemporaryDir dir; QVERIFY(dir.isValid()); const QString target = dir.filePath(QStringLiteral("out.txt")); - QCOMPARE(utils::WriteStringToFile(target, QStringLiteral("hello")), false); - // the directory "/.swap" was created as a side effect; cleanup - QDir(dir.filePath(QStringLiteral("out.txt/.swap"))).removeRecursively(); + QVERIFY(utils::WriteStringToFile(target, QStringLiteral("hello"))); + + QFile rd(target); + QVERIFY(rd.open(QIODevice::ReadOnly)); + QCOMPARE(QString::fromLatin1(rd.readAll()), QStringLiteral("hello")); + rd.close(); + + // the swap file is renamed away, so no leftover .swap file remains + QVERIFY(!QFile::exists(target + QStringLiteral(".swap"))); } -void TestWpsslUtils::writeStringToFileUnwritableParentReturnsFalse() +void TestWpsslUtils::writeStringToFileOverwritesExistingFile() { - // mkpath("/.swap") fails when itself is a file (not a - // directory), so the swap dir cannot be created underneath it. - QTemporaryFile fileParent; - QVERIFY(fileParent.open()); - // filename = an existing regular file -> swapFile = "/.swap" - QCOMPARE(utils::WriteStringToFile(fileParent.fileName(), QStringLiteral("x")), false); + // Fixed in DDE-135 #7: the old code treated as a directory + // prefix and always failed. Now an existing regular file is overwritten + // in-place via the sibling-swap-then-rename path. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString target = dir.filePath(QStringLiteral("existing.txt")); + + // seed the file with old content + { + QFile seed(target); + QVERIFY(seed.open(QIODevice::WriteOnly)); + seed.write("old"); + seed.close(); + } + + QVERIFY(utils::WriteStringToFile(target, QStringLiteral("new"))); + QFile rd(target); + QVERIFY(rd.open(QIODevice::ReadOnly)); + QCOMPARE(QString::fromLatin1(rd.readAll()), QStringLiteral("new")); + rd.close(); } void TestWpsslUtils::checkWallpaperLockedStatusReturnsBool()