Skip to content
Open
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
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.10)

project(dde-launchpad VERSION 0.7.0)

option(BUILD_TEST "Whether or not to build the tests" OFF)
option(BUILD_TESTING "Whether or not to build the tests" OFF)
Comment thread
BLumia marked this conversation as resolved.
option(CMAKE_EXPORT_COMPILE_COMMANDS "clangd support" ON)

set(CMAKE_CXX_STANDARD 17) # blurhash requires 17, otherwish we can still use 14
Expand Down Expand Up @@ -65,7 +65,7 @@ add_subdirectory(src/treelandintegration)
add_subdirectory(src/models)
add_subdirectory(qml/windowed)

if(BUILD_TEST)
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
Expand Down
134 changes: 132 additions & 2 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,21 +1,151 @@
# SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
# SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd.
#
# SPDX-License-Identifier: CC0-1.0

find_package(Qt6 REQUIRED COMPONENTS Core Test)
find_package(Qt6 REQUIRED COMPONENTS Gui Qml Svg) # needed by tested-source OBJECT libs
find_package(Qt6Core CONFIG REQUIRED Private) # sortproxymodel.cpp uses QAbstractItemModelPrivate

qt_standard_project_setup()

# Coverage instrumentation (on by default so `coverage` produces a report).
# Only the test-directory targets (OBJECT libs + test executables) are affected;
# the production `launchpadcommon` shared library is left untouched.
option(ENABLE_COVERAGE "Enable coverage instrumentation for tests" ON)
if(ENABLE_COVERAGE)
add_compile_options(--coverage)
add_link_options(--coverage)
endif()

# ---------------------------------------------------------------------------
# Tested-source OBJECT libraries
#
# Per the long-term coverage rule: the tested source files (src/models/*.cpp,
# src/utils/*.cpp) are compiled as OBJECT libraries and linked *directly*
# into every test executable, instead of being reached only through the
# launchpadcommon.so shared library. This ensures gcov .gcda runtime data
# is written to the test build directory and can be captured even in
# cross-prefix environments where the shared-library build path is not
# writable at runtime.
#
# launchpadcommon is still linked afterwards for transitive dependencies
# (D-Bus adaptors, dde-integration AppMgr, etc.). Symbols defined by the
# OBJECT libs take precedence over the same symbols in the shared library,
# so the gcov-instrumented copies in the test executable are the ones that
# actually execute.
# ---------------------------------------------------------------------------

add_library(launchpad-test-models OBJECT
${CMAKE_SOURCE_DIR}/src/models/appsmodel.cpp
${CMAKE_SOURCE_DIR}/src/models/categorizedsortproxymodel.cpp
${CMAKE_SOURCE_DIR}/src/models/countlimitproxymodel.cpp
${CMAKE_SOURCE_DIR}/src/models/favoritedproxymodel.cpp
${CMAKE_SOURCE_DIR}/src/models/freesortproxymodel.cpp
${CMAKE_SOURCE_DIR}/src/models/frequentlyusedproxymodel.cpp
${CMAKE_SOURCE_DIR}/src/models/itemarrangementproxymodel.cpp
${CMAKE_SOURCE_DIR}/src/models/itemspage.cpp
${CMAKE_SOURCE_DIR}/src/models/itemspagemodel.cpp
${CMAKE_SOURCE_DIR}/src/models/multipagesortfilterproxymodel.cpp
${CMAKE_SOURCE_DIR}/src/models/recentlyinstalledproxymodel.cpp
${CMAKE_SOURCE_DIR}/src/models/searchfilterproxymodel.cpp
${CMAKE_SOURCE_DIR}/src/models/sortproxymodel.cpp
)
target_include_directories(launchpad-test-models PRIVATE
${CMAKE_SOURCE_DIR}/src/models
${CMAKE_SOURCE_DIR}/src/utils
${CMAKE_SOURCE_DIR}/src/ddeintegration
)
target_link_libraries(launchpad-test-models PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Qml # QML_NAMED_ELEMENT / QML_SINGLETON in model headers (moc-generated registration)
Qt6::CorePrivate # sortproxymodel.cpp -> QAbstractItemModelPrivate
${DTK_NS}::Core # DConfig, DFileWatcherManager, DPinyin (appsmodel.cpp, frequentlyusedproxymodel.cpp)
)

add_library(launchpad-test-utils OBJECT
${CMAKE_SOURCE_DIR}/src/utils/blurhash.cpp
${CMAKE_SOURCE_DIR}/src/utils/categoryutils.cpp
${CMAKE_SOURCE_DIR}/src/utils/iconutils.cpp
)
target_include_directories(launchpad-test-utils PRIVATE
${CMAKE_SOURCE_DIR}/src/utils
)
target_link_libraries(launchpad-test-utils PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Svg
${DTK_NS}::Gui # DIcon, DIconTheme, DSvgRenderer (iconutils.cpp)
)

macro(launchpad_add_tests)
foreach(_testname ${ARGN})
qt_add_executable(launchpad-${_testname} ${_testname}.cpp)
target_link_libraries(launchpad-${_testname} PRIVATE Qt6::Core Qt6::Test launchpadcommon)
# Test sources include the headers under test by bare name (e.g.
# `#include "categoryutils.h"`), matching how src/ includes its own
# headers — no `../src/...` relative paths.
target_include_directories(launchpad-${_testname} PRIVATE
${CMAKE_SOURCE_DIR}/src/models
${CMAKE_SOURCE_DIR}/src/utils
${CMAKE_SOURCE_DIR}/src/gioutils
)
# OBJECT libs first: their symbols take precedence over launchpadcommon.so,
# so the gcov-instrumented copies are the ones that execute at test time.
target_link_libraries(launchpad-${_testname} PRIVATE
Qt6::Core
Qt6::Test
launchpad-test-models
launchpad-test-utils
gio-utils # already OBJECT; covers src/gioutils/ (e.g. gioappinfotest)
launchpadcommon
)
add_test(NAME launchpad-${_testname} COMMAND launchpad-${_testname})
list(APPEND LAUNCHPAD_TEST_TARGETS launchpad-${_testname})
endforeach()
endmacro()

launchpad_add_tests(
itemspagetest
gioappinfotest
searchfilterproxymodeltest
countlimitproxymodeltest
sortproxymodeltest
freesortproxymodeltest
recentlyinstalledproxymodeltest
multipagesortfilterproxymodeltest
categoryutilstest
blurhashtest
favoritedproxymodeltest
frequentlyusedproxymodeltest
itemarrangementproxymodeltest
iconutilstest
itemspagemodeltest
)

# ---- coverage: execute all tests and generate a coverage report ----
# Produces: ${CMAKE_BINARY_DIR}/coverage_report/index.html
find_program(LCOV_BIN lcov)
find_program(GENHTML_BIN genhtml)
set(_run_test_coverage "")
if(LCOV_BIN AND GENHTML_BIN)
set(_run_test_coverage
COMMAND ${LCOV_BIN} --capture --directory ${CMAKE_BINARY_DIR}
--rc lcov_branch_coverage=1 --output-file ${CMAKE_BINARY_DIR}/coverage.info
COMMAND ${LCOV_BIN} --extract ${CMAKE_BINARY_DIR}/coverage.info
--rc lcov_branch_coverage=1
"${CMAKE_SOURCE_DIR}/src/models/*"
"${CMAKE_SOURCE_DIR}/src/utils/*"
"${CMAKE_SOURCE_DIR}/src/gioutils/*"
--output-file ${CMAKE_BINARY_DIR}/coverage_filtered.info
COMMAND ${GENHTML_BIN} ${CMAKE_BINARY_DIR}/coverage_filtered.info
--branch-coverage --output-directory ${CMAKE_BINARY_DIR}/coverage_report
)
endif()
add_custom_target(coverage
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
${_run_test_coverage}
DEPENDS ${LAUNCHPAD_TEST_TARGETS}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Run all tests and generate coverage report"
VERBATIM
)
Comment on lines +131 to 151

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The coverage target succeeds after running the tests without generating any report when either lcov or genhtml is unavailable, because _run_test_coverage is left empty and no failure or warning is emitted. Consumers therefore receive a green coverage build with the promised coverage_report/index.html missing.

Triggers: When lcov or genhtml is not installed on the build host.

Suggested fix: Make the coverage target fail with a clear dependency error, or explicitly report that coverage generation was skipped instead of presenting the target as successful.

151 changes: 151 additions & 0 deletions tests/blurhashtest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include <QTest>

Check warning on line 5 in tests/blurhashtest.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 "blurhash.hpp"

Check warning on line 7 in tests/blurhashtest.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "blurhash.hpp" not found.

#include <vector>

Check warning on line 9 in tests/blurhashtest.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

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

namespace {

std::vector<unsigned char> solidImage(size_t width, size_t height,
unsigned char r, unsigned char g, unsigned char b)
{
std::vector<unsigned char> image(width * height * 3, 0);
for (size_t i = 0; i < width * height; ++i) {
image[i * 3 + 0] = r;
image[i * 3 + 1] = g;
image[i * 3 + 2] = b;
}
return image;
}

// blurhash layout: 1 (components) + 1 (maxAC) + 4 (DC) + 2 bytes per AC component
size_t expectedHashLength(int cx, int cy)
{
return size_t(1 + 1 + 4 + (cx * cy - 1) * 2);
}
}

class TestBlurhash : public QObject
{
Q_OBJECT
private slots:

Check warning on line 35 in tests/blurhashtest.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it.
void decodeEmptyStringReturnsEmpty();
void decodeInvalidHashReturnsEmpty();
void encodeRejectsInvalidInput();
void encodedHashHasExpectedLength();
void decodedImageHasExpectedDimensions();
void roundTripPreservesAverageColor();
void decodeWithFourBytesPerPixel();
void decodeTooShortHashReturnsEmpty();
void decodeWrongSizeForComponentsReturnsEmpty();
};

void TestBlurhash::decodeEmptyStringReturnsEmpty()
{
const auto img = blurhash::decode(std::string_view{}, 8, 8);
QVERIFY(img.image.empty());
QCOMPARE(img.width, size_t(0));
QCOMPARE(img.height, size_t(0));
}

void TestBlurhash::decodeInvalidHashReturnsEmpty()
{
const auto img = blurhash::decode(std::string_view("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"), 8, 8);
QVERIFY(img.image.empty());
}

void TestBlurhash::encodeRejectsInvalidInput()
{
std::vector<unsigned char> image = solidImage(4, 4, 255, 0, 0);
QVERIFY(blurhash::encode(image.data(), 0, 4, 1, 1).empty()); // width 0
QVERIFY(blurhash::encode(nullptr, 4, 4, 1, 1).empty()); // null image
QVERIFY(blurhash::encode(image.data(), 4, 4, 0, 1).empty()); // components_x < 1
QVERIFY(blurhash::encode(image.data(), 4, 4, 1, 10).empty()); // components_y > 9
}

void TestBlurhash::encodedHashHasExpectedLength()
{
std::vector<unsigned char> image = solidImage(8, 8, 128, 64, 200);
QCOMPARE(blurhash::encode(image.data(), 8, 8, 1, 1).size(), size_t(6)); // no AC components
QCOMPARE(blurhash::encode(image.data(), 8, 8, 1, 3).size(), expectedHashLength(1, 3)); // 10
QCOMPARE(blurhash::encode(image.data(), 8, 8, 2, 2).size(), expectedHashLength(2, 2)); // 12
QCOMPARE(blurhash::encode(image.data(), 8, 8, 4, 4).size(), expectedHashLength(4, 4)); // 36
}

void TestBlurhash::decodedImageHasExpectedDimensions()
{
constexpr size_t width = 16, height = 16;
std::vector<unsigned char> image = solidImage(width, height, 128, 64, 200);
const std::string hash = blurhash::encode(image.data(), width, height, 2, 2);
QVERIFY(!hash.empty());

const auto decoded = blurhash::decode(hash, width, height);
QCOMPARE(decoded.width, width);
QCOMPARE(decoded.height, height);
QCOMPARE(decoded.image.size(), width * height * 3);
}

void TestBlurhash::roundTripPreservesAverageColor()
{
constexpr size_t width = 16, height = 16;
const unsigned char r = 128, g = 64, b = 200;
std::vector<unsigned char> image = solidImage(width, height, r, g, b);

const std::string hash = blurhash::encode(image.data(), width, height, 3, 3);
QVERIFY(!hash.empty());

const auto decoded = blurhash::decode(hash, width, height);
QCOMPARE(decoded.image.size(), width * height * 3);

long rs = 0, gs = 0, bs = 0;
for (size_t i = 0; i < width * height; ++i) {
rs += decoded.image[i * 3 + 0];
gs += decoded.image[i * 3 + 1];
bs += decoded.image[i * 3 + 2];
}
const long n = long(width * height);
const long avgR = rs / n, avgG = gs / n, avgB = bs / n;
// blurhash is lossy per-pixel, but the average (DC) stays close to the source color
QVERIFY(avgR >= long(r) - 15 && avgR <= long(r) + 15);
QVERIFY(avgG >= long(g) - 15 && avgG <= long(g) + 15);
QVERIFY(avgB >= long(b) - 15 && avgB <= long(b) + 15);
}

void TestBlurhash::decodeWithFourBytesPerPixel()
{
constexpr size_t width = 16, height = 16;
std::vector<unsigned char> image = solidImage(width, height, 128, 64, 200);
const std::string hash = blurhash::encode(image.data(), width, height, 3, 3);
QVERIFY(!hash.empty());

const auto decoded = blurhash::decode(hash, width, height, 4);
QCOMPARE(decoded.width, width);
QCOMPARE(decoded.height, height);
// buffer should be width * height * 4 (not 3)
QCOMPARE(decoded.image.size(), width * height * 4);
// first 3 bytes of each pixel should still be the color channels
QVERIFY(decoded.image[0] != 255 || decoded.image[1] != 255 || decoded.image[2] != 255);
}

void TestBlurhash::decodeTooShortHashReturnsEmpty()
{
// hash must be >= 10 chars (1 + 1 + 4 + at least 4 for 2x2 components - 1 AC = 2 bytes -> 8, but 10 is the minimum check)
const auto img = blurhash::decode(std::string_view("short"), 8, 8);
QVERIFY(img.image.empty());
}

void TestBlurhash::decodeWrongSizeForComponentsReturnsEmpty()
{
// 1 component char says 4x4 = 16 components -> needs 1+1+4+(16-1)*2 = 36 chars
// but we'll give only 12 chars
const auto img = blurhash::decode(std::string_view("LFE.}?a]a]a]a]a]a]a]"), 8, 8);
// this hash has wrong length for its declared components -> should return empty
QVERIFY(img.image.empty());
}

QTEST_MAIN(TestBlurhash)
#include "blurhashtest.moc"

Check warning on line 151 in tests/blurhashtest.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "blurhashtest.moc" not found.
Loading
Loading