Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/plugin-qt/wallpapercache/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ set(CMAKE_AUTORCC ON)

include(GNUInstallDirs)
file(GLOB_RECURSE SRCS "*.h" "*.cpp")
list(FILTER SRCS EXCLUDE REGEX "/tests/")

find_package(PkgConfig REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui DBus)
Expand Down
34 changes: 23 additions & 11 deletions src/plugin-qt/wallpapercache/cachedwallpaper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,13 @@ QStringList CachedWallpaper::getCachedImagePaths(const QString &originalPath, co

QString pathMd5 = ScaleImageThread::pathMd5(originalPath, isMd5Path);

if (m_cachedImages.contains(pathMd5)) {
const QMap<QString, QString> &map = m_cachedImages[pathMd5];
for (const QSize &size : sizes) {
QString strSize = ScaleImageThread::sizeToString(size);
if (map.contains(strSize)) {
results.append(map[strSize]);
} else {
noCachedSizes.append(size);
}
for (const QSize &size : sizes) {
const QStringList cached = findCachedImagePaths(pathMd5, {size});
if (cached.isEmpty()) {
noCachedSizes.append(size);
} else {
results.append(cached);
}
} else {
noCachedSizes = sizes;
}

if (!noCachedSizes.isEmpty()) {
Expand All @@ -59,6 +54,23 @@ QStringList CachedWallpaper::getCachedImagePaths(const QString &originalPath, co
return results;
}

QStringList CachedWallpaper::findCachedImagePaths(const QString &pathMd5, const QList<QSize> &sizes) const
{
QStringList results;
const auto it = m_cachedImages.constFind(pathMd5);
if (it == m_cachedImages.constEnd())
return results;

for (const QSize &size : sizes) {
const QString path = it->value(ScaleImageThread::sizeToString(size));
const QFileInfo info(path);
if (path.isEmpty() || !info.isFile() || !info.isReadable() || info.size() == 0)
return {};
results.append(path);
}
return results;
}

void CachedWallpaper::cacheImage(const QString &originalPathMd5, const QString &size, const QString &processedPath)
{
qDebug() << "cache Image:" << processedPath;
Expand Down
6 changes: 5 additions & 1 deletion src/plugin-qt/wallpapercache/cachedwallpaper.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
#include <QString>
#include <QMap>
#include <QSize>
#include <QMutex>

Check warning on line 12 in src/plugin-qt/wallpapercache/cachedwallpaper.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QMutex> not found. Please note: Cppcheck does not need standard library headers to get proper results.

// Shared cache path constants
inline const QString kWallpaperCacheDir = QStringLiteral("/var/cache/dde-wallpaper-cache");
#ifndef WALLPAPER_CACHE_DIR
#define WALLPAPER_CACHE_DIR "/var/cache/dde-wallpaper-cache"
#endif
inline const QString kWallpaperCacheDir = QStringLiteral(WALLPAPER_CACHE_DIR);
inline const QString kBlurCacheDir = kWallpaperCacheDir + QStringLiteral("/blur");

class CachedWallpaper : public QObject
Expand All @@ -29,6 +32,7 @@
public:
static CachedWallpaper *instance();
QStringList getCachedImagePaths(const QString &originalPath, const QList<QSize> &sizes, bool isMd5Path = false);
QStringList findCachedImagePaths(const QString &pathMd5, const QList<QSize> &sizes) const;
void cacheImage(const QString &originalPathMd5, const QString &size, const QString &processedPath);

// Blur wallpaper interfaces
Expand Down
15 changes: 6 additions & 9 deletions src/plugin-qt/wallpapercache/scaleimagethread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,14 @@ bool ScaleImageThread::isIdle()

void ScaleImageThread::run()
{
while (!m_stop) {
for (;;) {
QMutexLocker locker(&m_mutex);

if (m_tasks.isEmpty()) {
while (m_tasks.isEmpty() && !m_stop) {
m_waitCondition.wait(&m_mutex);
}
if (m_stop)
return;

if (!m_tasks.isEmpty()) {
TaskData task = m_tasks.head();
Expand All @@ -128,15 +130,10 @@ void ScaleImageThread::executeTask(const TaskData &task)
QString originalPathMd5 = pathMd5(task.originalPath, task.isMd5Path);
QString cachedFilePath = cacheImageToDisk(pixmap, task, originalPathMd5);

// Keep the imported source as a cache entry. Other queued sizes and
// clients receiving it as the immediate fallback may still need it.
if (!cachedFilePath.isEmpty()) {
Q_EMIT imageScaled(originalPathMd5, sizeToString(task.targetSize), cachedFilePath);

if (task.isMd5Path) {
QFile file(task.originalPath);
if (file.exists()) {
file.remove();
}
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions src/plugin-qt/wallpapercache/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,25 @@ target_compile_definitions(test_wallpaper_cache PRIVATE QT_MESSAGELOGCONTEXT)

message(STATUS "Test build: test_wallpaper_cache")
message(STATUS "Run: sudo ./test_wallpaper_cache [wallpaper_path]")

include(CTest)
if(BUILD_TESTING)
find_package(Qt6 REQUIRED COMPONENTS Test)
find_package(PkgConfig REQUIRED)
pkg_check_modules(DtkGui REQUIRED IMPORTED_TARGET dtk6gui)
set(WALLPAPER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/..")
add_executable(test_wallpaper_cache_unit
test_wallpaper_cache_unit.cpp
${WALLPAPER_SOURCE_DIR}/wallpapercacheservice.cpp
${WALLPAPER_SOURCE_DIR}/cachedwallpaper.cpp
${WALLPAPER_SOURCE_DIR}/wallpapercachemanager.cpp
${WALLPAPER_SOURCE_DIR}/scaleimagethread.cpp
${WALLPAPER_SOURCE_DIR}/imageeffectprocessor.cpp
)
target_include_directories(test_wallpaper_cache_unit PRIVATE ${WALLPAPER_SOURCE_DIR})
target_compile_definitions(test_wallpaper_cache_unit PRIVATE
WALLPAPER_CACHE_DIR="${CMAKE_CURRENT_BINARY_DIR}/unit-cache")
target_link_libraries(test_wallpaper_cache_unit PRIVATE
Qt6::Core Qt6::DBus Qt6::Gui Qt6::Test PkgConfig::DtkGui)
add_test(NAME wallpaper-cache-unit COMMAND test_wallpaper_cache_unit)
endif()
196 changes: 196 additions & 0 deletions src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later

#include "cachedwallpaper.h"

Check warning on line 5 in src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "cachedwallpaper.h" not found.
#include "scaleimagethread.h"

Check warning on line 6 in src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "scaleimagethread.h" not found.
#include "wallpapercacheservice.h"

Check warning on line 7 in src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "wallpapercacheservice.h" not found.

#include <QDir>

Check warning on line 9 in src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDir> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QFile>

Check warning on line 10 in src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QFile> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QImage>

Check warning on line 11 in src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QImage> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QSignalSpy>

Check warning on line 12 in src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QSignalSpy> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QTemporaryDir>

Check warning on line 13 in src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QTemporaryDir> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QTest>

Check warning on line 14 in src/plugin-qt/wallpapercache/tests/test_wallpaper_cache_unit.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QTest> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <fcntl.h>
#include <unistd.h>

class WallpaperCacheTest : public QObject
{
Q_OBJECT

private slots:
void initTestCase()
{
QVERIFY(kWallpaperCacheDir.contains("/unit-cache"));
QVERIFY(QDir().mkpath(kWallpaperCacheDir));
}

void repeatedImportReplacesLongerImage()
{
QTemporaryDir dir;
QVERIFY(dir.isValid());
const QString input = dir.filePath("input.bmp");
const QString key(32, '1');
WallpaperCacheService service;
QImage large(300, 200, QImage::Format_RGB32);
large.fill(Qt::red);
QVERIFY(large.save(input));
QFile file(input);
QVERIFY(file.open(QIODevice::ReadOnly));
const auto first = service.GetProcessedImagePathByFd(QDBusUnixFileDescriptor(file.handle()), key, {});
QCOMPARE(first.size(), 1);
const qint64 largeSize = QFileInfo(first.first()).size();
file.close();

QImage small(30, 20, QImage::Format_RGB32);
small.fill(Qt::blue);
QVERIFY(small.save(input));
QVERIFY(file.open(QIODevice::ReadOnly));
const auto second = service.GetProcessedImagePathByFd(QDBusUnixFileDescriptor(file.handle()), key, {});
QCOMPARE(second, first);
QCOMPARE(QFileInfo(second.first()).size(), file.size());
QVERIFY(QFileInfo(second.first()).size() < largeSize);
QCOMPARE(QImage(second.first()).size(), small.size());
QCOMPARE(QImage(second.first()).pixelColor(0, 0), QColor(Qt::blue));
}

void invalidImportPreservesPublishedImage()
{
QTemporaryDir dir;
const QString input = dir.filePath("input.png");
QImage image(20, 20, QImage::Format_RGB32);
image.fill(Qt::green);
QVERIFY(image.save(input));
QFile file(input);
QVERIFY(file.open(QIODevice::ReadOnly));
WallpaperCacheService service;
const QString key(32, '2');
const auto first = service.GetProcessedImagePathByFd(QDBusUnixFileDescriptor(file.handle()), key, {});
QCOMPARE(first.size(), 1);
file.close();
QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Truncate));
QCOMPARE(file.write("invalid image"), qint64(13));
file.close();
QVERIFY(file.open(QIODevice::ReadOnly));
QVERIFY(service.GetProcessedImagePathByFd(QDBusUnixFileDescriptor(file.handle()), key, {}).isEmpty());
QCOMPARE(QImage(first.first()).pixelColor(0, 0), QColor(Qt::green));
QCOMPARE(QDir(kWallpaperCacheDir).entryList({".fd-import-*"}, QDir::Files | QDir::Hidden).size(), 0);
}

void descriptorReadFailureLeavesNoImport()
{
const int directoryFd = ::open("/tmp", O_RDONLY | O_DIRECTORY);
QVERIFY(directoryFd >= 0);
QDBusUnixFileDescriptor fd;
fd.giveFileDescriptor(directoryFd);
WallpaperCacheService service;
QVERIFY(service.GetProcessedImagePathByFd(fd, QString(32, '3'), {}).isEmpty());
QVERIFY(!QFile::exists(kWallpaperCacheDir + "/" + QString(32, '3')));
QCOMPARE(QDir(kWallpaperCacheDir).entryList({".fd-import-*"}, QDir::Files | QDir::Hidden).size(), 0);
}

void rejectInvalidCacheKey()
{
QFile file("/dev/null");
QVERIFY(file.open(QIODevice::ReadOnly));
WallpaperCacheService service;
QVERIFY(service.GetProcessedImagePathByFd(QDBusUnixFileDescriptor(file.handle()), "../outside", {}).isEmpty());
}

void completeCacheHitDoesNotReadDescriptor()
{
QTemporaryDir dir;
const QString cached = dir.filePath("cached.png");
QImage image(40, 30, QImage::Format_RGB32);
image.fill(Qt::yellow);
QVERIFY(image.save(cached));
const QString key(32, '4');
CachedWallpaper::instance()->cacheImage(key, "40x30", cached);
QFile input(cached);
QVERIFY(input.open(QIODevice::ReadOnly));
const QVariantList sizes{QVariant::fromValue(QSize(40, 30))};
WallpaperCacheService service;
QDBusUnixFileDescriptor fd(input.handle());
QCOMPARE(service.GetProcessedImagePathByFd(fd, key, sizes), QStringList{cached});
QCOMPARE(::lseek(fd.fileDescriptor(), 0, SEEK_CUR), off_t(0));
QCOMPARE(service.GetProcessedImagePathByFdWithBlur(fd, key, sizes, false), QStringList{cached});
QCOMPARE(::lseek(fd.fileDescriptor(), 0, SEEK_CUR), off_t(0));
QVERIFY(!QFile::exists(kWallpaperCacheDir + "/" + key + ".png"));
}

void partialCacheHitStillQueuesMissingSizes()
{
QTemporaryDir dir;
const QString original = dir.filePath("input.png");
QImage image(80, 60, QImage::Format_RGB32);
image.fill(Qt::cyan);
QVERIFY(image.save(original));
const QString key(32, '5');
const QString cached = dir.filePath("cached.png");
QVERIFY(image.save(cached));
CachedWallpaper::instance()->cacheImage(key, "40x30", cached);
QFile input(original);
QVERIFY(input.open(QIODevice::ReadOnly));
QSignalSpy pending(CachedWallpaper::instance(), &CachedWallpaper::needHandleImage);
WallpaperCacheService service;
QCOMPARE(service.GetProcessedImagePathByFd(QDBusUnixFileDescriptor(input.handle()), key,
{QVariant::fromValue(QSize(40, 30)), QVariant::fromValue(QSize(20, 15))}), QStringList{cached});
QCOMPARE(pending.size(), 1);
QCOMPARE(pending.first().at(1).value<QList<QSize>>(), QList<QSize>{QSize(20, 15)});
QVERIFY(QFile::exists(pending.first().at(0).toString()));
}

void removedCacheEntryIsRegenerated()
{
QTemporaryDir dir;
const QString missing = dir.filePath("removed.png");
QImage image(20, 20, QImage::Format_RGB32);
QVERIFY(image.save(missing));
const QString source = dir.filePath("source.png");
const QString key = ScaleImageThread::pathMd5(source, false);
CachedWallpaper::instance()->cacheImage(key, "20x20", missing);
QVERIFY(QFile::remove(missing));
QSignalSpy pending(CachedWallpaper::instance(), &CachedWallpaper::needHandleImage);
QVERIFY(CachedWallpaper::instance()->getCachedImagePaths(source, {QSize(20, 20)}).isEmpty());
QCOMPARE(pending.size(), 1);
}

void multipleSizesKeepFallbackSourceReadable()
{
QTemporaryDir dir;
const QString source = dir.filePath(QString(32, '6') + ".png");
QImage image(200, 150, QImage::Format_RGB32);
image.fill(Qt::magenta);
QVERIFY(image.save(source));
ScaleImageThread worker;
worker.setCachePath(dir.path());
QSignalSpy scaled(&worker, &ScaleImageThread::imageScaled);
worker.addTasks(source, {QSize(80, 60), QSize(40, 30)}, true);
QTRY_COMPARE_WITH_TIMEOUT(scaled.size(), 2, 5000);
for (const auto &reply : scaled) {
const QString path = reply.at(2).toString();
const QImage result(path);
QVERIFY(!result.isNull());
QCOMPARE(ScaleImageThread::sizeToString(result.size()), reply.at(1).toString());
}
QVERIFY(!QImage(source).isNull());
worker.addTasks(source, {QSize(20, 15)}, true);
QTRY_COMPARE_WITH_TIMEOUT(scaled.size(), 3, 5000);
QCOMPARE(QImage(scaled.last().at(2).toString()).size(), QSize(20, 15));
worker.stopThread();
QVERIFY(worker.wait(5000));
}

void idleWorkerStops()
{
ScaleImageThread worker;
worker.start();
worker.stopThread();
QVERIFY(worker.wait(5000));
}
};

QTEST_GUILESS_MAIN(WallpaperCacheTest)
#include "test_wallpaper_cache_unit.moc"
Loading
Loading