diff --git a/CMakeLists.txt b/CMakeLists.txt index 38b60ccc..40f0990c 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) @@ -7,11 +7,14 @@ set (VERSION "1.0.0" CACHE STRING "define project version") project(dde-services) +include(CTest) + if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX /usr) endif() include(GNUInstallDirs) +include(CTest) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") # 设置二进制输出目录 方便调试 @@ -23,4 +26,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..5f19d623 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,143 @@ +# 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) + +# --------------------------------------------------------------------------- +# xsettings: XSItemInfo/XSDataInfo marshal/unmarshal unit test +# --------------------------------------------------------------------------- +# Pure byte-serialization layer (XSETTINGS wire format). Depends on the +# already-tested Utils byte helpers; no D-Bus/DConfig/XCB dependencies. +add_executable(tst-xsdatainfo + tst_xsdatainfo.cpp + ${SRC_DIR}/plugin-qt/xsettings/impl/xsdatainfo.cpp + ${SRC_DIR}/plugin-qt/xsettings/modules/api/utils.cpp +) +target_include_directories(tst-xsdatainfo PRIVATE + ${SRC_DIR}/plugin-qt/xsettings/impl + ${SRC_DIR}/plugin-qt/xsettings + ${SRC_DIR}/plugin-qt/xsettings/modules/api + ${SRC_DIR}/plugin-qt/xsettings/modules +) +target_link_libraries(tst-xsdatainfo PRIVATE Qt6::Core Qt6::Test) +add_test(NAME xsettings-xsdatainfo COMMAND tst-xsdatainfo) 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_xsdatainfo.cpp b/tests/tst_xsdatainfo.cpp new file mode 100644 index 00000000..c53286a8 --- /dev/null +++ b/tests/tst_xsdatainfo.cpp @@ -0,0 +1,427 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "xsdatainfo.h" + +#include +#include +#include +#include +#include +#include + +// Unit test for xsettings XSItemInfo / XSDataInfo — the XSETTINGS wire +// marshal/unmarshal layer (src/plugin-qt/xsettings/impl/xsdatainfo.cpp). +// These are pure byte-serialization classes with no external (D-Bus / DConfig / +// XCB) dependencies; they build on the already-tested Utils byte helpers. +// +// Core strategy: round-trip. Construct an XSItemInfo from (prop, XsValue), +// marshal it to a QByteArray, unmarshal a fresh XSItemInfo from those bytes, +// and verify the name/value survive. This exercises both write and read paths +// for every supported type (Integer / String / Color). Additional slots cover +// type-mismatch marshal failures (via modifyProperty), the default/unknown-type +// no-op branch, the double-variant constructor's else branch, XSDataInfo +// collection operations, and the free function xsValueToString. +class TestXSDataInfo : public QObject +{ + Q_OBJECT + +private slots: + // --- XSItemInfo construction + round-trip --- + void integerConstructorAndRoundTrip(); + void stringConstructorAndRoundTrip(); + void colorConstructorAndRoundTrip(); + void doubleConstructorHitsElseBranch(); + void modifyPropertyUpdatesValueAndSerial(); + + // --- XSItemInfo marshal failure branches --- + void marshalTypeMismatchIntToStringReturnsFalse(); + void marshalTypeMismatchStringToIntReturnsFalse(); + void marshalTypeMismatchColorToIntReturnsFalse(); + void unMarshalUnknownTypeIsNoOp(); + void marshalUnknownTypeReturnsFalse(); + + // --- XSDataInfo --- + void emptyDataUnmarshalZerosAllFields(); + void nonEmptyZeroItemsUnmarshalPreservesHeader(); + void insertAndListProps(); + void getPropItemFound(); + void getPropItemNotFoundReturnsNull(); + void increaseSerialAndNumSettings(); + void fullRoundTripMultipleItems(); + void marshalFailureReturnsEmpty(); + + // --- xsValueToString --- + void xsValueToStringInteger(); + void xsValueToStringString(); + void xsValueToStringColor(); + void xsValueToStringTypeMismatchNullptr(); + void xsValueToStringUnknownType(); +}; + +// ---- XSItemInfo construction + round-trip ---- + +void TestXSDataInfo::integerConstructorAndRoundTrip() +{ + XSItemInfo src(QStringLiteral("Dpi"), XsValue(96)); + QCOMPARE(src.getHeadName(), QStringLiteral("Dpi")); + + QByteArray bytes; + QVERIFY(src.marshalXSItemInfoData(bytes)); + QVERIFY(!bytes.isEmpty()); + + XSItemInfo dst(bytes); // unmarshal + QCOMPARE(dst.getHeadName(), QStringLiteral("Dpi")); + const XsValue v = dst.getValue(); + const auto *ival = std::get_if(&v); + QVERIFY(ival != nullptr); + QCOMPARE(*ival, 96); +} + +void TestXSDataInfo::stringConstructorAndRoundTrip() +{ + XSItemInfo src(QStringLiteral("Name"), XsValue(QStringLiteral("hello"))); + QCOMPARE(src.getHeadName(), QStringLiteral("Name")); + + QByteArray bytes; + QVERIFY(src.marshalXSItemInfoData(bytes)); + + XSItemInfo dst(bytes); + QCOMPARE(dst.getHeadName(), QStringLiteral("Name")); + const XsValue v = dst.getValue(); + const auto *sval = std::get_if(&v); + QVERIFY(sval != nullptr); + QCOMPARE(*sval, QStringLiteral("hello")); +} + +void TestXSDataInfo::colorConstructorAndRoundTrip() +{ + const ColorValueInfo color{ 10, 20, 30, 40 }; + XSItemInfo src(QStringLiteral("Color"), XsValue(color)); + QCOMPARE(src.getHeadName(), QStringLiteral("Color")); + + QByteArray bytes; + QVERIFY(src.marshalXSItemInfoData(bytes)); + + XSItemInfo dst(bytes); + QCOMPARE(dst.getHeadName(), QStringLiteral("Color")); + const XsValue v = dst.getValue(); + const auto *cval = std::get_if(&v); + QVERIFY(cval != nullptr); + QCOMPARE((*cval)[0], uint16_t(10)); + QCOMPARE((*cval)[1], uint16_t(20)); + QCOMPARE((*cval)[2], uint16_t(30)); + QCOMPARE((*cval)[3], uint16_t(40)); +} + +void TestXSDataInfo::doubleConstructorHitsElseBranch() +{ + // XsValue holds a double, which matches none of int/QString/ColorValueInfo + // in the (prop, value) constructor → falls to the else branch (qDebug, no + // init). head.name stays default-constructed (empty QString). + XSItemInfo src(QStringLiteral("Ignored"), XsValue(1.5)); + QCOMPARE(src.getHeadName(), QString()); // not initialized by any init path +} + +void TestXSDataInfo::modifyPropertyUpdatesValueAndSerial() +{ + XSItemInfo src(QStringLiteral("Dpi"), XsValue(96)); + // modifyProperty increments lastChangeSerial and replaces value. + XsSetting setting; + setting.type = HeadTypeInteger; + setting.prop = QStringLiteral("Dpi"); + setting.value = XsValue(144); + src.modifyProperty(setting); + + const XsValue v = src.getValue(); + const auto *ival = std::get_if(&v); + QVERIFY(ival != nullptr); + QCOMPARE(*ival, 144); + + // Round-trip survives the modify (header type still Integer, value is int). + QByteArray bytes; + QVERIFY(src.marshalXSItemInfoData(bytes)); + XSItemInfo dst(bytes); + QCOMPARE(dst.getHeadName(), QStringLiteral("Dpi")); + const XsValue v2 = dst.getValue(); + const auto *ival2 = std::get_if(&v2); + QVERIFY(ival2 != nullptr); + QCOMPARE(*ival2, 144); +} + +// ---- XSItemInfo marshal failure branches ---- + +void TestXSDataInfo::marshalTypeMismatchIntToStringReturnsFalse() +{ + // Create an Integer item, then change value to a QString via modifyProperty. + // head.type is still HeadTypeInteger but value holds QString → + // std::get_if returns nullptr → break → marshal returns false. + XSItemInfo item(QStringLiteral("Dpi"), XsValue(96)); + XsSetting setting; + setting.type = HeadTypeString; + setting.value = XsValue(QStringLiteral("mismatch")); + item.modifyProperty(setting); + + QByteArray out; + QVERIFY(!item.marshalXSItemInfoData(out)); // header written, but ret=false +} + +void TestXSDataInfo::marshalTypeMismatchStringToIntReturnsFalse() +{ + XSItemInfo item(QStringLiteral("Name"), XsValue(QStringLiteral("hi"))); + XsSetting setting; + setting.type = HeadTypeInteger; + setting.value = XsValue(42); + item.modifyProperty(setting); + + QByteArray out; + QVERIFY(!item.marshalXSItemInfoData(out)); // str==nullptr → break → false +} + +void TestXSDataInfo::marshalTypeMismatchColorToIntReturnsFalse() +{ + XSItemInfo item(QStringLiteral("Color"), XsValue(ColorValueInfo{ 1, 2, 3, 4 })); + XsSetting setting; + setting.type = HeadTypeColor; + setting.value = XsValue(42); + item.modifyProperty(setting); + + QByteArray out; + QVERIFY(!item.marshalXSItemInfoData(out)); // colorValue==nullptr → break → false +} + +void TestXSDataInfo::unMarshalUnknownTypeIsNoOp() +{ + // Marshal a valid integer item, corrupt the type byte to an unknown value, + // then unmarshal — the default branch in unMarshalXSItemInfoData is a no-op + // (value stays default-constructed). + XSItemInfo src(QStringLiteral("X"), XsValue(1)); + QByteArray bytes; + QVERIFY(src.marshalXSItemInfoData(bytes)); + QCOMPARE(uint8_t(bytes.at(0)), uint8_t(HeadTypeInteger)); + + bytes[0] = static_cast(99); // unknown type + XSItemInfo item(bytes); // unmarshal; default branch no-op + QCOMPARE(item.getHeadName(), QStringLiteral("X")); +} + +void TestXSDataInfo::marshalUnknownTypeReturnsFalse() +{ + // Build on the unknown-type item from above: marshalling it hits the + // default branch in marshalXSItemInfoData → ret stays false. + XSItemInfo src(QStringLiteral("X"), XsValue(1)); + QByteArray tmp; + QVERIFY(src.marshalXSItemInfoData(tmp)); + tmp[0] = static_cast(99); + XSItemInfo item(tmp); // head.type is now 99 + + QByteArray out; + QVERIFY(!item.marshalXSItemInfoData(out)); +} + +// ---- XSDataInfo ---- + +void TestXSDataInfo::emptyDataUnmarshalZerosAllFields() +{ + QByteArray empty; + XSDataInfo data(empty); // datas.isEmpty() → zeros, return + + // Marshal back: header should be 12 zero bytes (byteOrder=0, pad=3, serial=0, numSettings=0). + QByteArray marshaled = data.marshalSettingData(); + QCOMPARE(marshaled.size(), 12); + for (int i = 0; i < marshaled.size(); i++) + QCOMPARE(uint8_t(marshaled.at(i)), uint8_t(0)); + QCOMPARE(data.listProps(), QStringLiteral("[]")); +} + +void TestXSDataInfo::nonEmptyZeroItemsUnmarshalPreservesHeader() +{ + // Marshal an empty XSDataInfo (12-byte header, numSettings=0), then + // unmarshal from non-empty data → covers the !isEmpty + loop-not-entered path. + QByteArray empty; + XSDataInfo src(empty); + src.increaseSerial(); // serial becomes 1 + QByteArray marshaled = src.marshalSettingData(); + QVERIFY(!marshaled.isEmpty()); + QCOMPARE(marshaled.size(), 12); + + XSDataInfo dst(marshaled); + QCOMPARE(dst.listProps(), QStringLiteral("[]")); + // serial=1 should survive the round-trip; verify by re-marshalling and + // checking the serial field (bytes 4-7, little-endian uint32). + QByteArray remarshaled = dst.marshalSettingData(); + uint32_t serial = uint8_t(remarshaled.at(4)) + | (uint32_t(uint8_t(remarshaled.at(5))) << 8) + | (uint32_t(uint8_t(remarshaled.at(6))) << 16) + | (uint32_t(uint8_t(remarshaled.at(7))) << 24); + QCOMPARE(serial, uint32_t(1)); +} + +void TestXSDataInfo::insertAndListProps() +{ + QByteArray empty; + XSDataInfo data(empty); + + auto item1 = QSharedPointer(new XSItemInfo(QStringLiteral("Dpi"), XsValue(96))); + auto item2 = QSharedPointer(new XSItemInfo(QStringLiteral("Name"), XsValue(QStringLiteral("x")))); + data.inserItem(item1); + data.inserItem(item2); + + QCOMPARE(data.listProps(), QStringLiteral("[\"Dpi\",\"Name\"]")); +} + +void TestXSDataInfo::getPropItemFound() +{ + QByteArray empty; + XSDataInfo data(empty); + auto item = QSharedPointer(new XSItemInfo(QStringLiteral("Dpi"), XsValue(96))); + data.inserItem(item); + + QSharedPointer found = data.getPropItem(QStringLiteral("Dpi")); + QVERIFY(found != nullptr); + QCOMPARE(found->getHeadName(), QStringLiteral("Dpi")); +} + +void TestXSDataInfo::getPropItemNotFoundReturnsNull() +{ + QByteArray empty; + XSDataInfo data(empty); + auto item = QSharedPointer(new XSItemInfo(QStringLiteral("Dpi"), XsValue(96))); + data.inserItem(item); + + QVERIFY(data.getPropItem(QStringLiteral("Absent")) == nullptr); +} + +void TestXSDataInfo::increaseSerialAndNumSettings() +{ + QByteArray empty; + XSDataInfo data(empty); + data.increaseSerial(); + data.increaseSerial(); + data.increaseNumSettings(); + data.increaseNumSettings(); + data.increaseNumSettings(); + + // Verify via re-marshal: serial=2 at bytes 4-7, numSettings=3 at bytes 8-11. + QByteArray marshaled = data.marshalSettingData(); + uint32_t serial = uint8_t(marshaled.at(4)) + | (uint32_t(uint8_t(marshaled.at(5))) << 8) + | (uint32_t(uint8_t(marshaled.at(6))) << 16) + | (uint32_t(uint8_t(marshaled.at(7))) << 24); + uint32_t numSettings = uint8_t(marshaled.at(8)) + | (uint32_t(uint8_t(marshaled.at(9))) << 8) + | (uint32_t(uint8_t(marshaled.at(10))) << 16) + | (uint32_t(uint8_t(marshaled.at(11))) << 24); + QCOMPARE(serial, uint32_t(2)); + QCOMPARE(numSettings, uint32_t(3)); +} + +void TestXSDataInfo::fullRoundTripMultipleItems() +{ + // Build a setting with 3 items of different types, marshal, unmarshal, verify. + QByteArray empty; + XSDataInfo src(empty); + src.increaseSerial(); + + auto itemInt = QSharedPointer(new XSItemInfo(QStringLiteral("Dpi"), XsValue(96))); + auto itemStr = QSharedPointer(new XSItemInfo(QStringLiteral("Name"), XsValue(QStringLiteral("hello")))); + auto itemColor = QSharedPointer(new XSItemInfo(QStringLiteral("Color"), XsValue(ColorValueInfo{ 10, 20, 30, 40 }))); + src.inserItem(itemInt); + src.inserItem(itemStr); + src.inserItem(itemColor); + src.increaseNumSettings(); + src.increaseNumSettings(); + src.increaseNumSettings(); + + QByteArray marshaled = src.marshalSettingData(); + QVERIFY(!marshaled.isEmpty()); + + XSDataInfo dst(marshaled); + QCOMPARE(dst.listProps(), QStringLiteral("[\"Dpi\",\"Name\",\"Color\"]")); + + // Verify each item's value survived the round-trip. + auto di = dst.getPropItem(QStringLiteral("Dpi")); + QVERIFY(di != nullptr); + const XsValue vi = di->getValue(); + QCOMPARE(*std::get_if(&vi), 96); + + auto dn = dst.getPropItem(QStringLiteral("Name")); + QVERIFY(dn != nullptr); + const XsValue vn = dn->getValue(); + QCOMPARE(*std::get_if(&vn), QStringLiteral("hello")); + + auto dc = dst.getPropItem(QStringLiteral("Color")); + QVERIFY(dc != nullptr); + const XsValue vc = dc->getValue(); + const auto *cval = std::get_if(&vc); + QVERIFY(cval != nullptr); + QCOMPARE((*cval)[0], uint16_t(10)); + QCOMPARE((*cval)[3], uint16_t(40)); +} + +void TestXSDataInfo::marshalFailureReturnsEmpty() +{ + // Insert an item whose marshal will fail (type mismatch via modifyProperty). + // marshalSettingData should detect the failure and return an empty QByteArray. + QByteArray empty; + XSDataInfo data(empty); + + auto badItem = QSharedPointer(new XSItemInfo(QStringLiteral("Dpi"), XsValue(96))); + XsSetting setting; + setting.value = XsValue(QStringLiteral("mismatch")); // int→string mismatch + badItem->modifyProperty(setting); + data.inserItem(badItem); + data.increaseNumSettings(); + + QByteArray marshaled = data.marshalSettingData(); + QVERIFY(marshaled.isEmpty()); // marshalXSItemInfoData returned false → return {} +} + +// ---- xsValueToString ---- + +void TestXSDataInfo::xsValueToStringInteger() +{ + XsValue v(42); + QCOMPARE(xsValueToString(v, HeadTypeInteger), QStringLiteral("42")); +} + +void TestXSDataInfo::xsValueToStringString() +{ + XsValue v(QStringLiteral("hi")); + QCOMPARE(xsValueToString(v, HeadTypeString), QStringLiteral("hi")); +} + +void TestXSDataInfo::xsValueToStringColor() +{ + XsValue v(ColorValueInfo{ 1, 2, 3, 4 }); + QCOMPARE(xsValueToString(v, HeadTypeColor), QStringLiteral("1,2,3,4")); +} + +void TestXSDataInfo::xsValueToStringTypeMismatchNullptr() +{ + // value holds int, but type asked is String → get_if==nullptr → "nullptr" + XsValue vInt(42); + QCOMPARE(xsValueToString(vInt, HeadTypeString), QStringLiteral("nullptr")); + + // value holds int, type asked is Color → get_if==nullptr → "nullptr" + QCOMPARE(xsValueToString(vInt, HeadTypeColor), QStringLiteral("nullptr")); + + // value holds QString, type asked is Integer → get_if==nullptr → break → "" + XsValue vStr(QStringLiteral("x")); + QCOMPARE(xsValueToString(vStr, HeadTypeInteger), QString()); + + // value holds Color, type asked is Integer → get_if==nullptr → break → "" + XsValue vColor(ColorValueInfo{ 1, 2, 3, 4 }); + QCOMPARE(xsValueToString(vColor, HeadTypeInteger), QString()); +} + +void TestXSDataInfo::xsValueToStringUnknownType() +{ + XsValue v(42); + QCOMPARE(xsValueToString(v, 99), QStringLiteral("unknown")); +} + +QTEST_GUILESS_MAIN(TestXSDataInfo) + +#include "tst_xsdatainfo.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"