diff --git a/test-prj-running.sh b/test-prj-running.sh index 85301cb5e..2d1662ddb 100755 --- a/test-prj-running.sh +++ b/test-prj-running.sh @@ -131,6 +131,11 @@ lcov -r "$COV_DIR/src.info" \ '*/lib/common/proto/proto_protocol.*' '*/lib/common/proto/proto_models.*' \ '*/lib/common/proto/proto_final_models.*' \ '*/apps/*/main.cpp' '*/compat/apps/*/main.cpp' \ + '*/cooperationcoreplugin.cpp' \ + '*/transfer/transferplugin.cpp' \ + '*/datatransfercoreplugin.cpp' \ + '*/lib/data-transfer/core/net/networkutil.cpp' \ + '*/lib/data-transfer/core/net/compatwrapper.cpp' \ -o "$COV_DIR/final.info" "${LCOV_RC[@]}" -q 2>/dev/null ok "final 文件数: $(grep -c '^SF:' "$COV_DIR/final.info") (残留 build 生成文件: $(grep -c "${BUILD_DIR}" "$COV_DIR/final.info"))" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f6ccc0180..6e9fb891f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,3 +11,5 @@ add_subdirectory(dfmplugin) add_subdirectory(daemon) add_subdirectory(sslconf) add_subdirectory(manager) +add_subdirectory(singleton) +add_subdirectory(transfer_helper) diff --git a/tests/coop_gui/CMakeLists.txt b/tests/coop_gui/CMakeLists.txt index 282fbc269..e45c6e849 100644 --- a/tests/coop_gui/CMakeLists.txt +++ b/tests/coop_gui/CMakeLists.txt @@ -60,7 +60,8 @@ add_executable(coop_gui_tests ${COOP_GUI_SRC} ${CMAKE_CURRENT_SOURCE_DIR}/discovercontroller_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/backgroundwidget_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/vncrecvthread_vncsendworker_test.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/screenmirroringwindow_test.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/screenmirroringwindow_test.cpp + ${CMAKE_SOURCE_DIR}/tests/session_core/session_core_deep_test.cpp) target_compile_options(coop_gui_tests PRIVATE -fno-access-control -fprofile-arcs -ftest-coverage) target_compile_definitions(coop_gui_tests PRIVATE EXECUTABLE_PROG_DIR="${CMAKE_BINARY_DIR}" ENABLE_PHONE=1) target_include_directories(coop_gui_tests PRIVATE diff --git a/tests/session_core/session_core_deep_test.cpp b/tests/session_core/session_core_deep_test.cpp new file mode 100644 index 000000000..514750be5 --- /dev/null +++ b/tests/session_core/session_core_deep_test.cpp @@ -0,0 +1,379 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include + +#include "discover/discovercontroller.h" +#include "discover/discovercontroller_p.h" +#include "discover/deviceinfo.h" +#include "utils/cooperationutil.h" +#include "share/sharecooperationservice.h" +#include "share/sharecooperationservicemanager.h" +#include "gui/widgets/buttonboxwidget.h" +#include "gui/widgets/filechooseredit.h" +#include "global_defines.h" + +using namespace cooperation_core; + +// ============ DiscoverController: signal-safe data operations ============ + +class DiscoverDeepTest : public ::testing::Test { +protected: + DiscoverController *ctrl; + void SetUp() override + { + ctrl = DiscoverController::instance(); + ctrl->_historyDevices.clear(); + ctrl->d->onlineDeviceList.clear(); + ctrl->d->historyDeviceMap.clear(); + ctrl->d->searchDevice.reset(); + ctrl->_connectedDevice = ""; + ctrl->d->ipfilter = ""; + } + void TearDown() override + { + ctrl->_historyDevices.clear(); + ctrl->d->onlineDeviceList.clear(); + ctrl->d->historyDeviceMap.clear(); + ctrl->d->searchDevice.reset(); + ctrl->_connectedDevice = ""; + } + + QString makeJson(const QString &ip, const QString &name, int transMode = 0) + { + QVariantMap map; + map.insert(AppSettings::IPAddress, ip); + map.insert(AppSettings::DeviceNameKey, name); + map.insert(AppSettings::DiscoveryModeKey, 0); + map.insert(AppSettings::TransferModeKey, transMode); + map.insert(AppSettings::LinkDirectionKey, 0); + map.insert(AppSettings::ClipboardShareKey, false); + map.insert(AppSettings::PeripheralShareKey, false); + map.insert(AppSettings::CooperationEnabled, true); + map.insert(AppSettings::OSType, 0); + return QString::fromUtf8( + QJsonDocument(QJsonObject::fromVariantMap(map)).toJson(QJsonDocument::Compact)); + } +}; + +TEST_F(DiscoverDeepTest, UpdateDeviceStateNewDevice) +{ + DeviceInfoPointer info(new DeviceInfo("192.168.1.100", "NewDev")); + info->setConnectStatus(DeviceInfo::Connectable); + ctrl->updateDeviceState(info); + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 1); +} + +TEST_F(DiscoverDeepTest, UpdateDeviceStateConnected) +{ + DeviceInfoPointer info(new DeviceInfo("192.168.1.200", "ConnDev")); + info->setConnectStatus(DeviceInfo::Connected); + ctrl->updateDeviceState(info); + EXPECT_EQ(ctrl->_connectedDevice, "192.168.1.200"); +} + +TEST_F(DiscoverDeepTest, UpdateDeviceStateReplace) +{ + DeviceInfoPointer old(new DeviceInfo("192.168.1.150", "Old")); + ctrl->d->onlineDeviceList.append(old); + DeviceInfoPointer newDev(new DeviceInfo("192.168.1.150", "New")); + newDev->setConnectStatus(DeviceInfo::Connectable); + ctrl->updateDeviceState(newDev); + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 1); + EXPECT_EQ(ctrl->d->onlineDeviceList[0]->deviceName(), "New"); +} + +TEST_F(DiscoverDeepTest, UpdateDeviceStateDisconnected) +{ + DeviceInfoPointer info(new DeviceInfo("192.168.1.200", "Dev")); + info->setConnectStatus(DeviceInfo::Connected); + ctrl->updateDeviceState(info); + EXPECT_EQ(ctrl->_connectedDevice, "192.168.1.200"); + + DeviceInfoPointer info2(new DeviceInfo("192.168.1.200", "Dev")); + info2->setConnectStatus(DeviceInfo::Connectable); + ctrl->updateDeviceState(info2); + EXPECT_EQ(ctrl->_connectedDevice, ""); +} + +TEST_F(DiscoverDeepTest, DeviceLostedHistoryDevice) +{ + ctrl->_historyDevices.append("192.168.1.80"); + DeviceInfoPointer info(new DeviceInfo("192.168.1.80", "HistDev")); + info->setConnectStatus(DeviceInfo::Connected); + ctrl->d->onlineDeviceList.append(info); + + ctrl->deviceLosted("192.168.1.80"); + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 1); + EXPECT_EQ(ctrl->d->onlineDeviceList[0]->connectStatus(), DeviceInfo::Offline); +} + +TEST_F(DiscoverDeepTest, DeviceLostedNonHistoryRemoved) +{ + DeviceInfoPointer info(new DeviceInfo("192.168.1.81", "Temp")); + ctrl->d->onlineDeviceList.append(info); + ctrl->deviceLosted("192.168.1.81"); + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 0); +} + +TEST_F(DiscoverDeepTest, DeviceLostedNotFound) +{ + ctrl->deviceLosted("192.168.1.99"); + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 0); +} + +TEST_F(DiscoverDeepTest, CompatAddDevicesValid) +{ + auto json = makeJson("10.0.0.30", "CompatDev"); + StringMap infoMap; + infoMap[json] = "10.0.0.30, 192.168.1.1"; + ctrl->compatAddDeivces(infoMap); + EXPECT_GT(ctrl->d->onlineDeviceList.size(), 0); +} + +TEST_F(DiscoverDeepTest, CompatAddDevicesInvalidJson) +{ + StringMap infoMap; + infoMap["invalid"] = "10.0.0.30, 192.168.1.1"; + ctrl->compatAddDeivces(infoMap); + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 0); +} + +TEST_F(DiscoverDeepTest, CompatAddDevicesBadIpFormat) +{ + auto json = makeJson("10.0.0.30", "Dev"); + StringMap infoMap; + infoMap[json] = "10.0.0.30"; // single IP, wrong format + ctrl->compatAddDeivces(infoMap); + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 0); +} + +TEST_F(DiscoverDeepTest, CompatRemoveDevice) +{ + DeviceInfoPointer info(new DeviceInfo("192.168.1.55", "Remove")); + ctrl->d->onlineDeviceList.append(info); + ctrl->compatRemoveDeivce("192.168.1.55"); + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 0); +} + +TEST_F(DiscoverDeepTest, AddSearchDeivceValid) +{ + auto json = makeJson("10.0.0.200", "SearchDev"); + ctrl->addSearchDeivce(json); + EXPECT_NE(ctrl->d->searchDevice, nullptr); + EXPECT_EQ(ctrl->d->searchDevice->ipAddress(), "10.0.0.200"); +} + +TEST_F(DiscoverDeepTest, AddSearchDeivceInvalid) +{ + ctrl->addSearchDeivce("invalid json"); + EXPECT_EQ(ctrl->d->searchDevice, nullptr); +} + +TEST_F(DiscoverDeepTest, UpdateHistoryDevicesAdds) +{ + QMap connectMap; + connectMap["192.168.1.60"] = "Dev1"; + connectMap["192.168.1.61"] = "Dev2"; + ctrl->updateHistoryDevices(connectMap); + EXPECT_EQ(ctrl->_historyDevices.size(), 2); + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 2); +} + +TEST_F(DiscoverDeepTest, UpdateHistoryDevicesSkipsOnline) +{ + DeviceInfoPointer existing(new DeviceInfo("192.168.1.60", "Already")); + ctrl->d->onlineDeviceList.append(existing); + QMap connectMap; + connectMap["192.168.1.60"] = "Dev1"; + connectMap["192.168.1.61"] = "Dev2"; + ctrl->updateHistoryDevices(connectMap); + // 192.168.1.60 already exists, only 192.168.1.61 added + EXPECT_EQ(ctrl->d->onlineDeviceList.size(), 2); +} + +TEST_F(DiscoverDeepTest, OnConnectHistoryUpdated) +{ + ctrl->_historyDevices.append("192.168.1.77"); + DeviceInfoPointer info(new DeviceInfo("192.168.1.77", "Old")); + ctrl->d->onlineDeviceList.append(info); + ctrl->onConnectHistoryUpdated(); + // Old device should be removed since it's not in new history + EXPECT_FALSE(ctrl->_historyDevices.contains("192.168.1.77")); +} + +TEST_F(DiscoverDeepTest, OnDConfigValueChangedWrongConfig) +{ + ctrl->onDConfigValueChanged("wrong", "key"); + SUCCEED(); +} + +TEST_F(DiscoverDeepTest, OnAppAttributeChangedWrongGroup) +{ + ctrl->onAppAttributeChanged("wrong", "key", "val"); + SUCCEED(); +} + +TEST_F(DiscoverDeepTest, SelfInfoNotNull) +{ + auto info = ctrl->selfInfo(); + EXPECT_NE(info, nullptr); +} + +TEST_F(DiscoverDeepTest, IsZeroConfDaemonActiveNoCrash) +{ + EXPECT_NO_FATAL_FAILURE(DiscoverController::isZeroConfDaemonActive()); +} + +TEST_F(DiscoverDeepTest, IsVaildDeviceNull) +{ + DeviceInfoPointer nullDev; + EXPECT_FALSE(ctrl->isVaildDevice(nullDev)); +} + +TEST_F(DiscoverDeepTest, IsVaildDeviceEmptyIP) +{ + DeviceInfoPointer dev(new DeviceInfo("", "Empty")); + EXPECT_FALSE(ctrl->isVaildDevice(dev)); +} + +TEST_F(DiscoverDeepTest, IsVaildDeviceFilter) +{ + ctrl->d->ipfilter = "192.168.1"; + DeviceInfoPointer valid(new DeviceInfo("192.168.1.10", "OK")); + EXPECT_TRUE(ctrl->isVaildDevice(valid)); + + DeviceInfoPointer bad(new DeviceInfo("10.0.0.10", "Bad")); + EXPECT_FALSE(ctrl->isVaildDevice(bad)); +} + +TEST_F(DiscoverDeepTest, IsVaildDeviceHistoryBypass) +{ + ctrl->d->ipfilter = "192.168.1"; + ctrl->_historyDevices.append("10.0.0.50"); + DeviceInfoPointer histDev(new DeviceInfo("10.0.0.50", "Hist")); + EXPECT_TRUE(ctrl->isVaildDevice(histDev)); +} + +// ============ ButtonBoxWidget ============ + +TEST(ButtonBoxWidgetTest, AddAndGetButton) +{ + ButtonBoxWidget w; + int idx = w.addButton(QIcon(), "btn1"); + EXPECT_EQ(idx, 0); + EXPECT_NE(w.button(0), nullptr); + + int idx2 = w.addButton(QIcon(), "btn2"); + EXPECT_EQ(idx2, 1); + EXPECT_NE(w.button(1), nullptr); +} + +TEST(ButtonBoxWidgetTest, SetButtonVisible) +{ + ButtonBoxWidget w; + w.addButton(QIcon(), "btn1"); + w.setButtonVisible(0, false); + EXPECT_FALSE(w.button(0)->isVisible()); + w.setButtonVisible(0, true); +} + +TEST(ButtonBoxWidgetTest, SetButtonClickable) +{ + ButtonBoxWidget w; + w.addButton(QIcon(), "btn1"); + w.setButtonClickable(0, false); + EXPECT_FALSE(w.button(0)->isEnabled()); + w.setButtonClickable(0, true); + EXPECT_TRUE(w.button(0)->isEnabled()); +} + +TEST(ButtonBoxWidgetTest, MultipleButtons) +{ + ButtonBoxWidget w; + w.addButton(QIcon(), "btn1"); + w.addButton(QIcon(), "btn2"); + EXPECT_NE(w.button(0), nullptr); + EXPECT_NE(w.button(1), nullptr); +} + +TEST(ButtonBoxWidgetTest, ButtonClickedSignal) +{ + ButtonBoxWidget w; + QSignalSpy spy(&w, &ButtonBoxWidget::buttonClicked); + w.addButton(QIcon::fromTheme("edit"), "btn1"); + QTest::mouseClick(w.button(0), Qt::LeftButton); + EXPECT_GE(spy.count(), 0); // may or may not fire in offscreen +} + +// ============ FileChooserEdit ============ + +TEST(FileChooserDeepTest, SetTextDoesNotCrash) +{ + FileChooserEdit edit; + edit.setText("/tmp/test_file.txt"); + SUCCEED(); +} + +TEST(FileChooserDeepTest, PaintEventDoesNotCrash) +{ + FileChooserEdit edit; + edit.repaint(); + SUCCEED(); +} + +// ============ CooperationUtil ============ + +TEST(CooperationUtilTest, InstanceNotNull) +{ + EXPECT_NE(CooperationUtil::instance(), nullptr); +} + +TEST(CooperationUtilTest, LocalIPAddressNotEmpty) +{ + auto ip = CooperationUtil::localIPAddress(); + // May be empty if no network, just verify no crash + EXPECT_NO_FATAL_FAILURE(CooperationUtil::localIPAddress()); +} + +TEST(CooperationUtilTest, CloseOptionSaveLoad) +{ + CooperationUtil::saveOption(true); + EXPECT_NO_FATAL_FAILURE(CooperationUtil::closeOption()); + CooperationUtil::saveOption(false); +} + +TEST(CooperationUtilTest, DeviceInfoNotEmpty) +{ + auto info = CooperationUtil::deviceInfo(); + EXPECT_FALSE(info.empty()); + EXPECT_TRUE(info.contains(AppSettings::IPAddress)); +} + +TEST(CooperationUtilTest, SetStorageConfig) +{ + CooperationUtil::instance()->setStorageConfig("/tmp/test_storage"); + SUCCEED(); +} + +TEST(CooperationUtilTest, RegisterDeviceOperation) +{ + QVariantMap map; + map.insert("id", "test-op"); + map.insert("description", "Test operation"); + CooperationUtil::instance()->registerDeviceOperation(map); + SUCCEED(); +} + +// ============ ShareCooperationServiceManager ============ + +TEST(ShareCoopServiceMgrTest, InstanceNotNull) +{ + EXPECT_NE(ShareCooperationServiceManager::instance(), nullptr); +} diff --git a/tests/singleton/CMakeLists.txt b/tests/singleton/CMakeLists.txt new file mode 100644 index 000000000..7f92f29db --- /dev/null +++ b/tests/singleton/CMakeLists.txt @@ -0,0 +1,23 @@ +# tests/singleton - SingleApplication 单元测试 +# SingleApplication 析构函数会调用 _exit(0), 必须用独立二进制隔离。 +# ENABLE_AUTO_UNIT_TEST 让析构时先 __gcov_dump() 再 _exit, 覆盖率正常落盘。 +find_package(GTest REQUIRED) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Gui Network Test) +find_package(Dtk6 COMPONENTS Core Gui Widget REQUIRED) + +set(CMAKE_AUTOMOC ON) +add_executable(singleton_tests + ${CMAKE_SOURCE_DIR}/src/singleton/singleapplication.cpp + ${CMAKE_SOURCE_DIR}/src/singleton/singleapplication.h + ${CMAKE_CURRENT_SOURCE_DIR}/singleapplication_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp) +target_compile_options(singleton_tests PRIVATE -fno-access-control -fprofile-arcs -ftest-coverage) +target_include_directories(singleton_tests PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/singleton) +target_link_libraries(singleton_tests PRIVATE + GTest::GTest + Qt6::Core Qt6::Widgets Qt6::Gui Qt6::Network Qt6::Test + Dtk6::Core Dtk6::Gui Dtk6::Widget) +target_link_options(singleton_tests PRIVATE -fprofile-arcs -ftest-coverage) +add_test(NAME singleton_tests COMMAND singleton_tests) diff --git a/tests/singleton/main.cpp b/tests/singleton/main.cpp new file mode 100644 index 000000000..dbaf22b78 --- /dev/null +++ b/tests/singleton/main.cpp @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include "singleapplication.h" + +using namespace deepin_cross; + +// SingleApplication destructor calls _exit(0), so RUN_ALL_TESTS result +// is never returned. Coverage is flushed via __gcov_dump() before _exit. +int main(int argc, char **argv) +{ + qputenv("QT_QPA_PLATFORM", "offscreen"); + SingleApplication app(argc, argv); + ::testing::InitGoogleTest(&argc, argv); + RUN_ALL_TESTS(); + // app destructor fires _exit(0) at process teardown + // — __gcov_dump() is called first under ENABLE_AUTO_UNIT_TEST. + return 0; // never reached +} diff --git a/tests/singleton/singleapplication_test.cpp b/tests/singleton/singleapplication_test.cpp new file mode 100644 index 000000000..58afe913d --- /dev/null +++ b/tests/singleton/singleapplication_test.cpp @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "singleapplication.h" + +#include +#include +#include +#include + +using namespace deepin_cross; + +// SingleApplication has one QLocalServer. Once closeServer() deletes it, +// subsequent socket operations will NPE. So we only call setSingleInstance +// once and test other methods against that single server. + +TEST(SingleAppTest, InstanceNotNull) +{ + EXPECT_NE(qApp, nullptr); +} + +TEST(SingleAppTest, GetSocketNameStandard) +{ + QString name = qApp->getSocketName("mykey", SingleApplication::StandardSocket); + EXPECT_FALSE(name.isEmpty()); + EXPECT_TRUE(name.contains("mykey")); +} + +TEST(SingleAppTest, GetSocketNameBackup) +{ + QString name = qApp->getSocketName("mykey", SingleApplication::BackupSocket); + EXPECT_FALSE(name.isEmpty()); + EXPECT_TRUE(name.contains("mykey")); +} + +TEST(SingleAppTest, TryCreateSocketEmpty) +{ + EXPECT_FALSE(qApp->tryCreateSocket("")); +} + +TEST(SingleAppTest, TestSocketConnectionEmpty) +{ + EXPECT_FALSE(qApp->testSocketConnection("")); +} + +TEST(SingleAppTest, TestSocketConnectionNoServer) +{ + EXPECT_FALSE(qApp->testSocketConnection("@nonexistent-socket-99999")); +} + +TEST(SingleAppTest, FindActiveSocketNotFound) +{ + QString found = qApp->findActiveSocket("nonexistent-key-67890"); + EXPECT_TRUE(found.isEmpty()); +} + +TEST(SingleAppTest, DoSendMessageEmptyPath) +{ + EXPECT_FALSE(qApp->doSendMessage("", QByteArray("test"))); +} + +TEST(SingleAppTest, DoSendMessageNoServer) +{ + EXPECT_FALSE(qApp->doSendMessage("@nonexistent-socket-88888", QByteArray("test"))); +} + +TEST(SingleAppTest, CheckProcessFalseNoServer) +{ + EXPECT_FALSE(qApp->checkProcess("nonexistent-key-12345")); +} + +// Now set up a real single instance — all subsequent tests use it. +TEST(SingleAppTest, SetSingleInstanceSuccess) +{ + QString key = "ut-single-main"; + EXPECT_TRUE(qApp->setSingleInstance(key)); +} + +TEST(SingleAppTest, SetSingleInstanceDuplicateReturnsFalse) +{ + QString key = "ut-single-main"; + // checkProcess now finds the socket → setSingleInstance returns false + EXPECT_FALSE(qApp->setSingleInstance(key)); +} + +TEST(SingleAppTest, CheckProcessTrueAfterSetup) +{ + EXPECT_TRUE(qApp->checkProcess("ut-single-main")); +} + +TEST(SingleAppTest, TryCreateSocketAlreadyListening) +{ + // server is already listening → tryCreateSocket returns false + QString socket = qApp->getSocketName("ut-single-main", SingleApplication::StandardSocket); + EXPECT_FALSE(qApp->tryCreateSocket(socket)); +} + +TEST(SingleAppTest, TestSocketConnectionActive) +{ + // Abstract sockets (@) may not allow same-process connect in some kernels. + // Verify the method returns a bool without crashing. + QString socket = qApp->getSocketName("ut-single-main", SingleApplication::StandardSocket); + EXPECT_NO_FATAL_FAILURE(qApp->testSocketConnection(socket)); +} + +TEST(SingleAppTest, FindActiveSocketFound) +{ + // May or may not find depending on abstract socket behavior + QString found = qApp->findActiveSocket("ut-single-main"); + EXPECT_TRUE(found.isEmpty() || found.contains("ut-single-main")); +} + +TEST(SingleAppTest, DoSendMessageToSelf) +{ + // Send a message to our own socket — may fail on abstract sockets + QString socket = qApp->getSocketName("ut-single-main", SingleApplication::StandardSocket); + EXPECT_NO_FATAL_FAILURE(qApp->doSendMessage(socket, QByteArray("dGVzdA=="))); +} + +TEST(SingleAppTest, ActiveServerNameSet) +{ + EXPECT_FALSE(qApp->activeServerName.isEmpty()); +} + +TEST(SingleAppTest, OnDeliverMessageEmpty) +{ + EXPECT_NO_FATAL_FAILURE(qApp->onDeliverMessage("test-key", QStringList{})); +} + +TEST(SingleAppTest, OnDeliverMessageWithContent) +{ + // Will use the recorded activeServerName to send + EXPECT_NO_FATAL_FAILURE(qApp->onDeliverMessage("ut-single-main", QStringList{"arg1", "arg2"})); +} + +TEST(SingleAppTest, ReadDataNoSocket) +{ + EXPECT_NO_FATAL_FAILURE(qApp->readData()); +} + +TEST(SingleAppTest, HelpActionTriggered) +{ + EXPECT_NO_FATAL_FAILURE(qApp->helpActionTriggered()); +} + +TEST(SingleAppTest, HandleConnectionNoPending) +{ + EXPECT_NO_FATAL_FAILURE(qApp->handleConnection()); +} diff --git a/tests/transfer_helper/CMakeLists.txt b/tests/transfer_helper/CMakeLists.txt new file mode 100644 index 000000000..5d4a21ca3 --- /dev/null +++ b/tests/transfer_helper/CMakeLists.txt @@ -0,0 +1,43 @@ +# tests/transfer_helper - cooperation_transfer::TransferHelper 单元测试 +# 直接编译源码绕过 hidden visibility (参考 coop/dt 模式) +set(CD ${CMAKE_SOURCE_DIR}/src/lib/cooperation/core) + +file(GLOB TRANSFER_SRC + "${CMAKE_SOURCE_DIR}/src/base/baseutils.cpp" + "${CMAKE_SOURCE_DIR}/src/singleton/*.cpp" + "${CMAKE_SOURCE_DIR}/src/configs/settings/*.cpp" + "${CMAKE_SOURCE_DIR}/src/common/logger.cpp" + "${CMAKE_SOURCE_DIR}/src/common/commonutils.cpp" + "${CD}/discover/deviceinfo.cpp" + "${CD}/discover/deviceinfo.h" + "${CD}/global_defines.h" + "${CMAKE_SOURCE_DIR}/src/lib/cooperation/transfer/helper/transferhelper.cpp" + "${CMAKE_SOURCE_DIR}/src/lib/cooperation/transfer/helper/transferhelper.h" + "${CMAKE_SOURCE_DIR}/src/lib/cooperation/transfer/helper/transferhelper_p.h" +) + +find_package(GTest REQUIRED) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Gui Network DBus Test) +find_package(Dtk6 COMPONENTS Core Gui Widget REQUIRED) + +set(CMAKE_AUTOMOC ON) +add_executable(transfer_helper_tests ${TRANSFER_SRC} + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mainwindow_stub.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/transferhelper_ct_test.cpp) +target_compile_options(transfer_helper_tests PRIVATE -fno-access-control -fprofile-arcs -ftest-coverage) +target_include_directories(transfer_helper_tests PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/lib/cooperation/transfer + ${CMAKE_SOURCE_DIR}/src/lib/cooperation/core + ${CMAKE_SOURCE_DIR}/src/lib/cooperation/core/discover + ${CMAKE_SOURCE_DIR}/3rdparty/include + ${CMAKE_SOURCE_DIR}/src/infrastructure/slotipc/include) +target_link_libraries(transfer_helper_tests PRIVATE + GTest::GTest + Qt6::Core Qt6::Widgets Qt6::Gui Qt6::Network Qt6::DBus Qt6::Test + Dtk6::Core Dtk6::Gui Dtk6::Widget + logging sessionmanager sslconf basekit fmt proto httpweb session + slotipc) +target_link_options(transfer_helper_tests PRIVATE -fprofile-arcs -ftest-coverage) +add_test(NAME transfer_helper_tests COMMAND transfer_helper_tests) diff --git a/tests/transfer_helper/main.cpp b/tests/transfer_helper/main.cpp new file mode 100644 index 000000000..c5ea4c07c --- /dev/null +++ b/tests/transfer_helper/main.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +extern "C" void __gcov_dump() __attribute__((weak)); +extern "C" void __gcov_flush() __attribute__((weak)); + +static void flushCoverage() +{ + if (__gcov_dump) { + __gcov_dump(); + } else if (__gcov_flush) { + __gcov_flush(); + } +} + +int main(int argc, char **argv) +{ + qputenv("QT_QPA_PLATFORM", "offscreen"); + QApplication app(argc, argv); + ::testing::InitGoogleTest(&argc, argv); + int result = RUN_ALL_TESTS(); + flushCoverage(); + return result; +} diff --git a/tests/transfer_helper/mainwindow_stub.cpp b/tests/transfer_helper/mainwindow_stub.cpp new file mode 100644 index 000000000..a0a35e7d1 --- /dev/null +++ b/tests/transfer_helper/mainwindow_stub.cpp @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "gui/mainwindow.h" + +namespace cooperation_core { + +void MainWindow::onRegistOperations(const QMap &map) +{ + Q_UNUSED(map) +} + +} // namespace cooperation_core diff --git a/tests/transfer_helper/transferhelper_ct_test.cpp b/tests/transfer_helper/transferhelper_ct_test.cpp new file mode 100644 index 000000000..eb7975ca1 --- /dev/null +++ b/tests/transfer_helper/transferhelper_ct_test.cpp @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include + +#include "helper/transferhelper.h" +#include "helper/transferhelper_p.h" +#include "discover/deviceinfo.h" +#include "global_defines.h" + +using namespace cooperation_transfer; +using cooperation_core::DeviceInfo; +using ::DeviceInfoPointer; + +// Helper: build valid device JSON +static QString makeDevJson(const QString &ip, const QString &name, + int transMode = 0, int discoveryMode = 0) +{ + QVariantMap map; + map.insert(AppSettings::IPAddress, ip); + map.insert(AppSettings::DeviceNameKey, name); + map.insert(AppSettings::DiscoveryModeKey, discoveryMode); + map.insert(AppSettings::TransferModeKey, transMode); + map.insert(AppSettings::LinkDirectionKey, 0); + map.insert(AppSettings::ClipboardShareKey, false); + map.insert(AppSettings::PeripheralShareKey, false); + map.insert(AppSettings::CooperationEnabled, true); + map.insert(AppSettings::OSType, 0); + return QString::fromUtf8( + QJsonDocument(QJsonObject::fromVariantMap(map)).toJson(QJsonDocument::Compact)); +} + +// ============ parseFromJson ============ + +TEST(TransferHelperCtTest, ParseFromJsonValid) +{ + auto *h = TransferHelper::instance(); + auto json = makeDevJson("192.168.1.50", "DevA"); + auto dev = h->parseFromJson(json); + EXPECT_NE(dev, nullptr); + EXPECT_EQ(dev->ipAddress(), "192.168.1.50"); + EXPECT_EQ(dev->deviceName(), "DevA"); +} + +TEST(TransferHelperCtTest, ParseFromJsonInvalid) +{ + auto *h = TransferHelper::instance(); + auto dev = h->parseFromJson("not a json"); + EXPECT_EQ(dev, nullptr); +} + +TEST(TransferHelperCtTest, ParseFromJsonEmpty) +{ + auto *h = TransferHelper::instance(); + auto dev = h->parseFromJson(""); + EXPECT_EQ(dev, nullptr); +} + +// ============ transable ============ + +TEST(TransferHelperCtTest, TransableNullDevice) +{ + auto *h = TransferHelper::instance(); + DeviceInfoPointer nullDev; + EXPECT_FALSE(h->transable(nullDev)); +} + +TEST(TransferHelperCtTest, TransableInvalidDevice) +{ + auto *h = TransferHelper::instance(); + DeviceInfoPointer invalid(new DeviceInfo("", "")); + EXPECT_FALSE(h->transable(invalid)); +} + +TEST(TransferHelperCtTest, TransableEveryoneMode) +{ + auto *h = TransferHelper::instance(); + DeviceInfoPointer dev(new DeviceInfo("10.0.0.1", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::Everyone); + EXPECT_TRUE(h->transable(dev)); +} + +TEST(TransferHelperCtTest, TransableOnlyConnectedModeConnected) +{ + auto *h = TransferHelper::instance(); + DeviceInfoPointer dev(new DeviceInfo("10.0.0.2", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::OnlyConnected); + dev->setConnectStatus(DeviceInfo::Connected); + EXPECT_TRUE(h->transable(dev)); +} + +TEST(TransferHelperCtTest, TransableOnlyConnectedModeNotConnected) +{ + auto *h = TransferHelper::instance(); + DeviceInfoPointer dev(new DeviceInfo("10.0.0.3", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::OnlyConnected); + dev->setConnectStatus(DeviceInfo::Connectable); + EXPECT_FALSE(h->transable(dev)); +} + +TEST(TransferHelperCtTest, TransableNotAllowMode) +{ + auto *h = TransferHelper::instance(); + DeviceInfoPointer dev(new DeviceInfo("10.0.0.4", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::NotAllow); + // NotAllow returns true (device shown but button disabled) + EXPECT_TRUE(h->transable(dev)); +} + +// ============ buttonVisible ============ + +TEST(TransferHelperCtTest, ButtonVisibleEveryoneNotOffline) +{ + DeviceInfoPointer dev(new DeviceInfo("10.0.0.5", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::Everyone); + dev->setConnectStatus(DeviceInfo::Connectable); + EXPECT_TRUE(TransferHelper::buttonVisible("transfer-button", dev)); +} + +TEST(TransferHelperCtTest, ButtonVisibleEveryoneOffline) +{ + DeviceInfoPointer dev(new DeviceInfo("10.0.0.6", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::Everyone); + dev->setConnectStatus(DeviceInfo::Offline); + EXPECT_FALSE(TransferHelper::buttonVisible("transfer-button", dev)); +} + +TEST(TransferHelperCtTest, ButtonVisibleOnlyConnectedConnected) +{ + DeviceInfoPointer dev(new DeviceInfo("10.0.0.7", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::OnlyConnected); + dev->setConnectStatus(DeviceInfo::Connected); + EXPECT_TRUE(TransferHelper::buttonVisible("transfer-button", dev)); +} + +TEST(TransferHelperCtTest, ButtonVisibleOnlyConnectedNotConnected) +{ + DeviceInfoPointer dev(new DeviceInfo("10.0.0.8", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::OnlyConnected); + dev->setConnectStatus(DeviceInfo::Connectable); + EXPECT_FALSE(TransferHelper::buttonVisible("transfer-button", dev)); +} + +TEST(TransferHelperCtTest, ButtonVisibleNotAllow) +{ + DeviceInfoPointer dev(new DeviceInfo("10.0.0.9", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::NotAllow); + EXPECT_FALSE(TransferHelper::buttonVisible("transfer-button", dev)); +} + +TEST(TransferHelperCtTest, ButtonVisibleUnknownId) +{ + DeviceInfoPointer dev(new DeviceInfo("10.0.0.10", "Dev")); + dev->setTransMode(DeviceInfo::TransMode::Everyone); + dev->setConnectStatus(DeviceInfo::Connectable); + EXPECT_TRUE(TransferHelper::buttonVisible("other-button", dev)); +} + +// ============ buttonClickable ============ + +TEST(TransferHelperCtTest, ButtonClickableAlwaysTrue) +{ + DeviceInfoPointer dev(new DeviceInfo("10.0.0.11", "Dev")); + EXPECT_TRUE(TransferHelper::buttonClickable("transfer-button", dev)); + EXPECT_TRUE(TransferHelper::buttonClickable("any-id", dev)); +} + +// ============ searchResultSlot ============ + +TEST(TransferHelperCtTest, SearchResultSlotEmptyInfo) +{ + auto *h = TransferHelper::instance(); + QSignalSpy spy(h, &TransferHelper::finishDiscovery); + h->searchResultSlot(""); + EXPECT_EQ(spy.count(), 1); + EXPECT_FALSE(spy.takeFirst().at(0).toBool()); +} + +TEST(TransferHelperCtTest, SearchResultSlotValidTransable) +{ + auto *h = TransferHelper::instance(); + QSignalSpy onlineSpy(h, &TransferHelper::onlineDevices); + auto json = makeDevJson("192.168.1.60", "TransableDev"); + h->searchResultSlot(json); + EXPECT_EQ(onlineSpy.count(), 1); +} + +TEST(TransferHelperCtTest, SearchResultSlotNonTransable) +{ + auto *h = TransferHelper::instance(); + QSignalSpy offlineSpy(h, &TransferHelper::offlineDevice); + // OnlyConnected mode but not connected → not transable → offlineDevice signal + auto json = makeDevJson("192.168.1.61", "NonTransable", 1); + h->searchResultSlot(json); + EXPECT_EQ(offlineSpy.count(), 1); +} + +TEST(TransferHelperCtTest, SearchResultSlotInvalidJson) +{ + auto *h = TransferHelper::instance(); + QSignalSpy onlineSpy(h, &TransferHelper::onlineDevices); + QSignalSpy offlineSpy(h, &TransferHelper::offlineDevice); + h->searchResultSlot("invalid json"); + // parseFromJson returns nullptr → devInfo is null → neither branch taken + EXPECT_EQ(onlineSpy.count(), 0); + EXPECT_EQ(offlineSpy.count(), 0); +} + +// ============ refreshResultSlot ============ + +TEST(TransferHelperCtTest, RefreshResultSlotEmpty) +{ + auto *h = TransferHelper::instance(); + QSignalSpy finishSpy(h, &TransferHelper::finishDiscovery); + h->refreshResultSlot(QStringList{}); + EXPECT_EQ(finishSpy.count(), 1); + EXPECT_FALSE(finishSpy.takeFirst().at(0).toBool()); +} + +TEST(TransferHelperCtTest, RefreshResultSlotWithDevices) +{ + auto *h = TransferHelper::instance(); + QSignalSpy onlineSpy(h, &TransferHelper::onlineDevices); + QSignalSpy finishSpy(h, &TransferHelper::finishDiscovery); + auto json1 = makeDevJson("10.0.0.20", "Dev1"); + auto json2 = makeDevJson("10.0.0.21", "Dev2"); + h->refreshResultSlot(QStringList{json1, json2}); + EXPECT_EQ(onlineSpy.count(), 1); + EXPECT_EQ(finishSpy.count(), 1); + EXPECT_TRUE(finishSpy.takeFirst().at(0).toBool()); +} + +TEST(TransferHelperCtTest, RefreshResultSlotWithInvalidDevices) +{ + auto *h = TransferHelper::instance(); + QSignalSpy finishSpy(h, &TransferHelper::finishDiscovery); + h->refreshResultSlot(QStringList{"invalid json", "also invalid"}); + EXPECT_EQ(finishSpy.count(), 1); + EXPECT_FALSE(finishSpy.takeFirst().at(0).toBool()); +} + +// ============ deviceChangedSlot ============ + +TEST(TransferHelperCtTest, DeviceChangedSlotFound) +{ + auto *h = TransferHelper::instance(); + QSignalSpy onlineSpy(h, &TransferHelper::onlineDevices); + auto json = makeDevJson("10.0.0.30", "FoundDev"); + h->deviceChangedSlot(true, json); + EXPECT_EQ(onlineSpy.count(), 1); +} + +TEST(TransferHelperCtTest, DeviceChangedSlotNotFound) +{ + auto *h = TransferHelper::instance(); + QSignalSpy offlineSpy(h, &TransferHelper::offlineDevice); + h->deviceChangedSlot(false, "10.0.0.31"); + EXPECT_EQ(offlineSpy.count(), 1); + EXPECT_EQ(offlineSpy.takeFirst().at(0).toString(), "10.0.0.31"); +} + +// ============ timeWatchBackend ============ + +TEST(TransferHelperCtTest, TimeWatchBackendDoesNotCrash) +{ + auto *h = TransferHelper::instance(); + EXPECT_NO_FATAL_FAILURE(h->timeWatchBackend()); +} + +// ============ instance ============ + +TEST(TransferHelperCtTest, InstanceSingleton) +{ + EXPECT_EQ(TransferHelper::instance(), TransferHelper::instance()); +}