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
2 changes: 1 addition & 1 deletion dtkcore.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ set (DSYSINFO_PREFIX "" CACHE STRING "PREFIX of DSysInfo")

set (BUILD_EXAMPLES ON CACHE BOOL "Build examples")
set (BUILD_VERSION "0" CACHE STRING "buildversion")
option(BUIILD_TESTING "Build tests" OFF)
option(BUILD_TESTING "Build tests" OFF)

if(UNIX AND NOT APPLE)
set(LINUX TRUE)
Expand Down
130 changes: 129 additions & 1 deletion tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,62 @@ include(GoogleTest)
set(TEST_SO_NAME vtabletest${DTK_NAME_SUFFIX})
add_subdirectory(./testso)

# ---------------------------------------------------------------------------
# DSysInfo mock infrastructure
# ---------------------------------------------------------------------------
# The shared library (compiled in src/ before tests/) always has
# DSYSINFO_PREFIX="" so inTest() is false and singleton caches never clear.
# Two complementary mechanisms solve this:
#
# 1. OBJECT library: compile dsysinfo.cpp with DSYSINFO_PREFIX="/tmp" so
# inTest()==true, paths point to /tmp/etc/, and caches clear on re-read.
# Linked BEFORE the shared library so the test binary resolves DSysInfo
# symbols from this copy (link-time stub, not compile-time macro in the
# shared library).
#
# 2. LD_PRELOAD stub (dsysinfo_stub.cpp): redirects /etc/ -> /tmp/etc/ for
# any reads originating from the shared library's internal code.
#
# Test-mode prefixes (also kept for the test binary's own compilation):
set(DSYSINFO_PREFIX "/tmp" CACHE STRING "PREFIX of DSysInfo" FORCE)
set(DSG_PREFIX_PATH "/tmp" CACHE STRING "PREFIX of DSG_DATA_DIRS" FORCE)

# OBJECT library — test-mode copy of dsysinfo.cpp
add_library(dsysinfo_test_obj OBJECT ../src/dsysinfo.cpp)
target_compile_definitions(dsysinfo_test_obj PRIVATE
DSYSINFO_PREFIX="${DSYSINFO_PREFIX}"
PREFIX="${DSG_PREFIX_PATH}"
)
target_include_directories(dsysinfo_test_obj PRIVATE
../include/global/
../include/
${PROJECT_BINARY_DIR} # generated dtkcore_config.h lives at the build root
)
target_link_libraries(dsysinfo_test_obj PRIVATE
Qt${QT_VERSION_MAJOR}::Core
)

# Instrument the OBJECT library so dsysinfo.cpp gets coverage data.
# dtk_setup_code_coverage only applies to the shared library; the OBJECT
# library needs its own flags. Coverage is opt-in (EnableCov, see
# src/CMakeLists.txt) — building the tests alone must not instrument them.
if(EnableCov AND BUILD_TESTING)
target_compile_options(dsysinfo_test_obj PRIVATE -fprofile-arcs -ftest-coverage)
endif()

# LD_PRELOAD stub shared library
add_library(dsysinfo_stub SHARED dsysinfo_stub.cpp)
target_link_libraries(dsysinfo_stub PRIVATE dl)

# ---------------------------------------------------------------------------

# test
file(GLOB TEST_HEADER ut_.*h)
file(GLOB TEST_SOURCE *.cpp)

# Exclude the LD_PRELOAD stub from the test binary sources
list(REMOVE_ITEM TEST_SOURCE "${CMAKE_CURRENT_LIST_DIR}/dsysinfo_stub.cpp")

file(GLOB FackDBus
"./fakedbus/*.h"
"./fakedbus/*.cpp"
Expand All @@ -54,6 +106,7 @@ file(GLOB FackDBus
if(NOT DTK5)
list(REMOVE_ITEM TEST_SOURCE "${CMAKE_CURRENT_LIST_DIR}/ut_gsettingsbackend.cpp")
list(REMOVE_ITEM TEST_SOURCE "${CMAKE_CURRENT_LIST_DIR}/ut_dasync.cpp")
list(REMOVE_ITEM TEST_SOURCE "${CMAKE_CURRENT_LIST_DIR}/ut_dtimedloop.cpp")
endif()

set(test_SRC
Expand All @@ -73,9 +126,22 @@ target_compile_definitions(${BIN_NAME} PUBLIC
DSYSINFO_PREFIX="${DSYSINFO_PREFIX}"
)

# Instrument the test binary so that source compiled into the test
# executable (e.g. LogManager.cpp #included by ut_dlog.cpp, and inline
# functions from headers) generates coverage data. Without this, lcov
# reports 0% for LogManager.cpp and low coverage for header inline code.
# Opt-in, like the OBJECT library above.
if(EnableCov AND BUILD_TESTING)
target_compile_options(${BIN_NAME} PRIVATE -fprofile-arcs -ftest-coverage)
endif()

add_dependencies(${BIN_NAME} ${TEST_SO_NAME})

# Link the OBJECT library FIRST so its DSysInfo symbols take precedence
# over the shared library's copies (which have DSYSINFO_PREFIX="").
target_link_libraries(
${BIN_NAME} PRIVATE
dsysinfo_test_obj
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::CorePrivate
Expand Down Expand Up @@ -118,6 +184,9 @@ target_include_directories( ${BIN_NAME} PUBLIC
../include/filesystem/
../include/
../src/log/
../src/dci/private/
../src/util/
../src/filesystem/
./testso/
)

Expand All @@ -126,4 +195,63 @@ set_target_properties(${BIN_NAME} PROPERTIES
SKIP_BUILD_RPATH FALSE
)

gtest_discover_tests(${BIN_NAME})
# Register tests with LD_PRELOAD environment so the stub redirects /etc/
# reads for any shared-library internal code paths. (DTK_DISABLED_LOGGING_RULES
# is set inside ut_dlog.cpp's SetUpTestCase: gtest_discover_tests only keeps the
# first ENVIRONMENT entry, so a second one here would be silently dropped.)
#
# Debug builds add -fsanitize=address (see the top of this file), and ASan
# refuses to run unless its runtime comes FIRST in LD_PRELOAD — otherwise every
# test aborts with "ASan runtime does not come first in initial library list".
# Prepend the ASan runtime ahead of the stub so both stay in the preload list.
set(_test_preload "$<TARGET_FILE:dsysinfo_stub>")
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
execute_process(
COMMAND ${CMAKE_CXX_COMPILER} -print-file-name=libasan.so
OUTPUT_VARIABLE _asan_runtime
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(_asan_runtime AND EXISTS "${_asan_runtime}")
set(_test_preload "${_asan_runtime}:$<TARGET_FILE:dsysinfo_stub>")
else()
message(WARNING "Debug build but ASan runtime not found; tests may abort with "
"\"ASan runtime does not come first in initial library list\"")
endif()
endif()
gtest_discover_tests(${BIN_NAME}
PROPERTIES
ENVIRONMENT "LD_PRELOAD=${_test_preload}"
)

# ---- coverage: execute all tests and generate a coverage report ----
# Gated by EnableCov (the project's coverage switch, src/CMakeLists.txt, default
# OFF): the instrumentation above and this target only make sense when coverage
# is explicitly requested:
# cmake -B build -DBUILD_TESTING=ON -DEnableCov=ON
if(NOT EnableCov)
message(STATUS "coverage target disabled (EnableCov=OFF); pass -DEnableCov=ON to enable it")
else()
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
"${PROJECT_SOURCE_DIR}/src/*"
--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 || true
${_run_test_coverage}
DEPENDS ${BIN_NAME}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Run all tests and generate coverage report"
VERBATIM
)
endif()
133 changes: 133 additions & 0 deletions tests/dsysinfo_stub.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later

// LD_PRELOAD stub that redirects the specific DSysInfo fixture files
// (/etc/os-version, /etc/lsb-release, /etc/os-release, /etc/deepin-version,
// /usr/lib/os-release) to /tmp/etc/ and /tmp/usr/lib/ so that DSysInfo reads
// mock fixture files instead of real system files.
//
// The redirect is intentionally NARROW (exact-path only), NOT all of /etc/:
// a broad /etc/ redirect breaks unrelated accesses the tests rely on —
// * getpwuid()'s /etc/passwd read -> redirected to a non-existent
// /tmp/etc/passwd -> null passwd entry -> SEGFAULT in the empty-HOME
// DLogManager tests;
// * QSaveFile's O_TMPFILE-backed writes (mode arg dropped) -> EACCES in the
// DCI FileEngine tests.
//
// This stub is compiled as a shared library and loaded via LD_PRELOAD
// (configured in gtest_discover_tests PROPERTIES ENVIRONMENT).
// It complements the OBJECT-library approach: the OBJECT copy of
// dsysinfo.cpp already reads from /tmp/etc/ via DSYSINFO_PREFIX="/tmp",
// while this stub catches any /etc/ reads originating from the shared
// library's internal code paths.

#define _GNU_SOURCE
#include <dlfcn.h>

Check warning on line 26 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 27 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 28 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 29 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 30 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <stdarg.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <sys/stat.h>

Check warning on line 31 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <sys/stat.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <unistd.h>

Check warning on line 32 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 33 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

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

// Exact-path redirects only. Returns either `path` or a pointer to a string
// literal (immutable, reentrant, thread-safe) — never a shared mutable buffer.
static const char *redirect_path(const char *path)
{
if (!path)
return path;

if (strcmp(path, "/etc/os-version") == 0)
return "/tmp/etc/os-version";
if (strcmp(path, "/etc/lsb-release") == 0)
return "/tmp/etc/lsb-release";
if (strcmp(path, "/etc/os-release") == 0)
return "/tmp/etc/os-release";
if (strcmp(path, "/etc/deepin-version") == 0)
return "/tmp/etc/deepin-version";
if (strcmp(path, "/usr/lib/os-release") == 0)
return "/tmp/usr/lib/os-release";

return path;
}

typedef int (*orig_open_t)(const char *, int, ...);
typedef int (*orig_open64_t)(const char *, int, ...);
typedef int (*orig_access_t)(const char *, int);
typedef FILE *(*orig_fopen_t)(const char *, const char *);
typedef FILE *(*orig_fopen64_t)(const char *, const char *);
typedef int (*orig_stat_t)(const char *, struct stat *);
typedef int (*orig_lstat_t)(const char *, struct stat *);
typedef int (*orig_xstat_t)(int, const char *, struct stat *);

extern "C" int open(const char *path, int flags, ...)
{
mode_t mode = 0;
// mode is significant for O_CREAT AND O_TMPFILE (O_TMPFILE does not set
// O_CREAT; dropping it made QSaveFile create 0000-mode files -> EACCES).
if (flags & (O_CREAT | O_TMPFILE)) {
va_list args;
va_start(args, flags);
mode = va_arg(args, mode_t);
va_end(args);
}
orig_open_t orig = (orig_open_t)dlsym(RTLD_NEXT, "open");

Check warning on line 76 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

C-style pointer casting detected. C++ offers four different kinds of casts as replacements: static_cast, const_cast, dynamic_cast and reinterpret_cast. A C-style cast could evaluate to any of those automatically, thus it is considered safer if the programmer explicitly states which kind of cast is expected.
return orig(redirect_path(path), flags, mode);
}

extern "C" int open64(const char *path, int flags, ...)
{
mode_t mode = 0;
if (flags & (O_CREAT | O_TMPFILE)) {
va_list args;
va_start(args, flags);
mode = va_arg(args, mode_t);
va_end(args);
}
orig_open64_t orig = (orig_open64_t)dlsym(RTLD_NEXT, "open64");

Check warning on line 89 in tests/dsysinfo_stub.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

C-style pointer casting detected. C++ offers four different kinds of casts as replacements: static_cast, const_cast, dynamic_cast and reinterpret_cast. A C-style cast could evaluate to any of those automatically, thus it is considered safer if the programmer explicitly states which kind of cast is expected.
return orig(redirect_path(path), flags, mode);
}

extern "C" int access(const char *path, int mode)
{
orig_access_t orig = (orig_access_t)dlsym(RTLD_NEXT, "access");
return orig(redirect_path(path), mode);
}

extern "C" FILE *fopen(const char *path, const char *mode)
{
orig_fopen_t orig = (orig_fopen_t)dlsym(RTLD_NEXT, "fopen");
return orig(redirect_path(path), mode);
}

extern "C" FILE *fopen64(const char *path, const char *mode)
{
orig_fopen64_t orig = (orig_fopen64_t)dlsym(RTLD_NEXT, "fopen64");
return orig(redirect_path(path), mode);
}

extern "C" int stat(const char *path, struct stat *buf)
{
orig_stat_t orig = (orig_stat_t)dlsym(RTLD_NEXT, "stat");
return orig(redirect_path(path), buf);
}

extern "C" int lstat(const char *path, struct stat *buf)
{
orig_lstat_t orig = (orig_lstat_t)dlsym(RTLD_NEXT, "lstat");
return orig(redirect_path(path), buf);
}

extern "C" int __xstat(int ver, const char *path, struct stat *buf)
{
orig_xstat_t orig = (orig_xstat_t)dlsym(RTLD_NEXT, "__xstat");
return orig(ver, redirect_path(path), buf);
}

extern "C" int __lxstat(int ver, const char *path, struct stat *buf)
{
orig_xstat_t orig = (orig_xstat_t)dlsym(RTLD_NEXT, "__lxstat");
return orig(ver, redirect_path(path), buf);
}
34 changes: 16 additions & 18 deletions tests/test-recoverage.sh
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
#!/bin/bash

# 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
set -ex

BUILD_DIR=`pwd`/../build/tests/
HTML_DIR=${BUILD_DIR}/html
XML_DIR=${BUILD_DIR}/report
# Thin wrapper around the cmake "coverage" target. The target already runs the
# tests, collects the profile data and generates the HTML report, so this script
# only configures the build and forwards the artifacts the CI job expects -
# keeping the lcov/genhtml invocations here in sync with the target would just
# duplicate them.
BUILD_DIR=$(pwd)/../build
REPORT_DIR=${BUILD_DIR}/tests

export ASAN_OPTIONS="halt_on_error=0"

# back to project directroy
cd ..

cmake -Bbuild -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_EXAMPLES=OFF -DBUILD_DOCS=OFF -DEnableCov=ON -DDSYSINFO_PREFIX=/tmp
cmake -Bbuild -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr \
-DBUILD_EXAMPLES=OFF -DBUILD_DOCS=OFF -DBUILD_TESTING=ON \
-DEnableCov=ON -DDSYSINFO_PREFIX=/tmp
cmake --build build --target coverage

cmake --build build -j$(nproc)

cd $BUILD_DIR

./ut-DtkCore --gtest_output=xml:${XML_DIR}/report_dtkcore.xml

# find *.gcda from build dir
lcov -d ../ -c -o coverage_all.info
lcov --remove coverage_all.info "*/tests/*" "*/usr/include*" "*build/src*" "*build-ut/src*" --output-file coverage.info
cd ..
genhtml -o $HTML_DIR $BUILD_DIR/coverage.info && mv ${BUILD_DIR}/html/index.html ${BUILD_DIR}/html/cov_dtkcore.html

test -e ${BUILD_DIR}/asan.log* && mv ${BUILD_DIR}/asan.log* ${BUILD_DIR}/asan_dtkcore.log || touch ${BUILD_DIR}/asan.log
mkdir -p ${REPORT_DIR}
rm -rf ${REPORT_DIR}/html
mv ${BUILD_DIR}/coverage_report ${REPORT_DIR}/html

test -e ${REPORT_DIR}/asan.log* && mv ${REPORT_DIR}/asan.log* ${REPORT_DIR}/asan_dtkcore.log || touch ${REPORT_DIR}/asan.log
Loading
Loading