Skip to content

Commit 70bbc61

Browse files
committed
test: add unit tests for valid DConfig configuration
- Add comprehensive test suite for TreelandUserConfig with valid DConfig - Test immediate destroy, initialization completion, and property changes - Add stress tests for high-frequency creation/destruction cycles - Verify safety of signal handling using QPointer mechanism - Tests confirm crash fix prevents SIGSEGV in production scenarios
1 parent c6580fc commit 70bbc61

3 files changed

Lines changed: 389 additions & 0 deletions

File tree

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ add_subdirectory(test_protocol_shortcut)
77
add_subdirectory(test_protocol_virtual-output)
88
add_subdirectory(test_protocol_wallpaper-color)
99
add_subdirectory(test_protocol_window-management)
10+
add_subdirectory(test_treeland_userconfig)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
set(CMAKE_INCLUDE_CURRENT_DIR ON)
2+
set(CMAKE_AUTOMOC ON)
3+
4+
# Test with VALID DConfig - reproduces real machine crash (SIGSEGV)
5+
add_executable(test_dconfig_valid_config test_dconfig_valid_config.cpp)
6+
target_link_libraries(test_dconfig_valid_config
7+
Qt6::Core
8+
Qt6::Test
9+
libtreeland
10+
)
11+
target_include_directories(test_dconfig_valid_config PRIVATE
12+
${CMAKE_BINARY_DIR}/src
13+
)
14+
add_test(NAME test_dconfig_valid_config COMMAND test_dconfig_valid_config)
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
// Copyright (C) 2026 UnionTech Software Technology Co., Ltd.
2+
// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3+
4+
// Test: Reproduce SIGSEGV crash using VALID DConfig
5+
// The config schema is at: /usr/share/dsg/configs/org.deepin.dde.treeland/org.deepin.dde.treeland.user.json
6+
//
7+
// The crash mechanism:
8+
// 1. initializeInConfigThread connects to DConfig::valueChanged signal
9+
// QObject::connect(config, &DConfig::valueChanged, this, [this](key) { updateValue(key); })
10+
// 2. updateValue calls: QMetaObject::invokeMethod(this, [this]() { updateProperty(...) })
11+
// 3. Data object destroyed while lambda is queued
12+
// 4. Lambda tries to access destroyed 'this' -> SIGSEGV
13+
14+
#include "treelanduserconfig.hpp"
15+
16+
#include <QObject>
17+
#include <QTest>
18+
#include <QThread>
19+
#include <QEventLoop>
20+
#include <QCoreApplication>
21+
#include <QSignalSpy>
22+
23+
#include <chrono>
24+
#include <atomic>
25+
26+
class ValidConfigCrashTest : public QObject
27+
{
28+
Q_OBJECT
29+
30+
private:
31+
// Static flag to track if DConfig is available
32+
static bool s_dConfigAvailable;
33+
static bool s_initialized;
34+
35+
private Q_SLOTS:
36+
37+
void initTestCase()
38+
{
39+
qDebug() << "=== ValidConfigCrashTest ===";
40+
qDebug() << "Using VALID DConfig from /usr/share/dsg/configs/";
41+
qDebug() << "Config name: org.deepin.dde.treeland.user";
42+
43+
// One-time check for DConfig availability
44+
if (!s_initialized) {
45+
TreelandUserConfig *testConfig = TreelandUserConfig::createByName("org.deepin.dde.treeland.user",
46+
"org.deepin.dde.treeland",
47+
"/dde");
48+
if (testConfig) {
49+
QTRY_VERIFY_WITH_TIMEOUT(testConfig->isInitializeSucceed(), 2000);
50+
s_dConfigAvailable = testConfig->isInitializeSucceed();
51+
52+
// Let initialization complete before deletion
53+
for (int i = 0; i < 100; ++i) {
54+
QCoreApplication::processEvents();
55+
}
56+
57+
delete testConfig;
58+
}
59+
s_initialized = true;
60+
}
61+
62+
if (!s_dConfigAvailable) {
63+
qDebug() << "DConfig schema not available - all tests will skip";
64+
}
65+
}
66+
67+
// Test 1: Simple creation and immediate deletion
68+
// If DConfig is valid, this should NOT crash
69+
// If it crashes, it means the signal connection lambda is unsafe
70+
void test_immediate_destroy_valid_dconfig()
71+
{
72+
if (!s_dConfigAvailable) {
73+
QSKIP("DConfig schema not available on this system");
74+
}
75+
76+
qDebug() << "\n=== Test 1: Immediate destroy with valid DConfig ===";
77+
78+
// Check if DConfig schema is available
79+
TreelandUserConfig *testConfig = TreelandUserConfig::createByName("org.deepin.dde.treeland.user",
80+
"org.deepin.dde.treeland",
81+
"/dde");
82+
QVERIFY2(testConfig != nullptr, "createByName returned nullptr");
83+
84+
// Wait for initialization to complete
85+
QTRY_VERIFY_WITH_TIMEOUT(testConfig->isInitializeSucceed(), 2000);
86+
87+
if (!testConfig->isInitializeSucceed()) {
88+
delete testConfig;
89+
QSKIP("DConfig schema not available on this system; skipping test");
90+
}
91+
92+
auto dconfig = testConfig->config();
93+
QVERIFY2(dconfig && dconfig->isValid(), "DConfig is not valid");
94+
95+
// Process remaining events before deletion
96+
for (int i = 0; i < 100; ++i) {
97+
QCoreApplication::processEvents();
98+
}
99+
100+
delete testConfig;
101+
102+
// Now run the actual test with assertions
103+
for (int cycle = 0; cycle < 50; ++cycle) {
104+
// Use createByName with correct parameters like production code
105+
TreelandUserConfig *config = TreelandUserConfig::createByName("org.deepin.dde.treeland.user",
106+
"org.deepin.dde.treeland",
107+
"/dde");
108+
109+
QVERIFY2(config != nullptr, qPrintable("createByName returned nullptr in cycle " + QString::number(cycle)));
110+
111+
// Delete immediately
112+
delete config;
113+
114+
// Process events
115+
QCoreApplication::processEvents();
116+
117+
if (cycle % 10 == 0) {
118+
qDebug() << " Cycle" << cycle << "- No crash yet";
119+
}
120+
}
121+
}
122+
123+
// Test 2: Destroy after signal connection is established
124+
// The crash is most likely here because:
125+
// 1. DConfig finishes initialization
126+
// 2. initializeInConfigThread sets up valueChanged connection
127+
// 3. Main thread then deletes the config
128+
// 4. If we trigger property change, the lambda captures destroyed 'this'
129+
void test_destroy_after_initialization()
130+
{
131+
if (!s_dConfigAvailable) {
132+
QSKIP("DConfig schema not available on this system");
133+
}
134+
135+
qDebug() << "\n=== Test 2: Destroy after initialization ===";
136+
137+
for (int cycle = 0; cycle < 30; ++cycle) {
138+
TreelandUserConfig *config = TreelandUserConfig::createByName("org.deepin.dde.treeland.user",
139+
"org.deepin.dde.treeland",
140+
"/dde");
141+
142+
QVERIFY2(config != nullptr, qPrintable("createByName returned nullptr in cycle " + QString::number(cycle)));
143+
144+
// Wait for initialization to complete with timeout
145+
// This gives time for DConfig to init and signal connection to be set up
146+
QTRY_VERIFY_WITH_TIMEOUT(config->isInitializeSucceed(), 2000);
147+
QVERIFY(config->isInitializeSucceed());
148+
149+
auto dconfig = config->config();
150+
QVERIFY2(dconfig != nullptr, "config() returned nullptr");
151+
QVERIFY2(dconfig->isValid(), qPrintable("DConfig is not valid in cycle " + QString::number(cycle)));
152+
153+
qDebug() << " Cycle" << cycle << "- Config initialized and valid";
154+
155+
// Process more events to allow initialization thread to complete
156+
// This is critical to avoid race conditions
157+
for (int i = 0; i < 200; ++i) {
158+
QCoreApplication::processEvents();
159+
QThread::usleep(10);
160+
}
161+
162+
// Now delete - the valueChanged connection should be fully set up
163+
// If config emits signal before deletion finishes, crash happens
164+
delete config;
165+
166+
// Process events - this is critical
167+
// If there are queued valueChanged signals, they execute now
168+
for (int i = 0; i < 50; ++i) {
169+
QCoreApplication::processEvents();
170+
QThread::msleep(2);
171+
}
172+
}
173+
}
174+
175+
// Test 3: Trigger property change then destroy
176+
// This more directly triggers the crash
177+
void test_property_change_then_destroy()
178+
{
179+
if (!s_dConfigAvailable) {
180+
QSKIP("DConfig schema not available on this system");
181+
}
182+
183+
qDebug() << "\n=== Test 3: Property change then destroy ===";
184+
185+
for (int cycle = 0; cycle < 20; ++cycle) {
186+
TreelandUserConfig *config = TreelandUserConfig::createByName("org.deepin.dde.treeland.user",
187+
"org.deepin.dde.treeland",
188+
"/dde");
189+
190+
QVERIFY2(config != nullptr, qPrintable("createByName returned nullptr in cycle " + QString::number(cycle)));
191+
192+
// Wait for initialization with timeout
193+
QTRY_VERIFY_WITH_TIMEOUT(config->isInitializeSucceed(), 2000);
194+
QVERIFY(config->isInitializeSucceed());
195+
196+
auto dconfig = config->config();
197+
QVERIFY2(dconfig != nullptr, "config() returned nullptr");
198+
QVERIFY2(dconfig->isValid(), qPrintable("DConfig is not valid in cycle " + QString::number(cycle)));
199+
200+
qDebug() << " Cycle" << cycle << "- DConfig is VALID";
201+
202+
// Change a property to trigger valueChanged signal
203+
// This will cause updateValue to be called in config thread
204+
dconfig->setValue(QStringLiteral("themeName"),
205+
QVariant::fromValue(QString("test_theme_" + QString::number(cycle))));
206+
207+
// Immediately delete while valueChanged signal might be in flight
208+
delete config;
209+
config = nullptr;
210+
211+
// Process events - the queued updateProperty lambdas execute
212+
// If Data was destroyed, this triggers SIGSEGV
213+
for (int i = 0; i < 50; ++i) {
214+
QCoreApplication::processEvents();
215+
QThread::msleep(2);
216+
}
217+
}
218+
}
219+
220+
// Test 4: Rapid property changes then destroy
221+
// More aggressive test to trigger race condition
222+
void test_rapid_changes_then_destroy()
223+
{
224+
if (!s_dConfigAvailable) {
225+
QSKIP("DConfig schema not available on this system");
226+
}
227+
228+
qDebug() << "\n=== Test 4: Rapid property changes then destroy ===";
229+
230+
for (int cycle = 0; cycle < 15; ++cycle) {
231+
TreelandUserConfig *config = TreelandUserConfig::createByName("org.deepin.dde.treeland.user",
232+
"org.deepin.dde.treeland",
233+
"/dde");
234+
235+
QVERIFY2(config != nullptr, qPrintable("createByName returned nullptr in cycle " + QString::number(cycle)));
236+
237+
// Wait for initialization with timeout
238+
QTRY_VERIFY_WITH_TIMEOUT(config->isInitializeSucceed(), 2000);
239+
QVERIFY(config->isInitializeSucceed());
240+
241+
auto dconfig = config->config();
242+
QVERIFY2(dconfig != nullptr, "config() returned nullptr");
243+
QVERIFY2(dconfig->isValid(), qPrintable("DConfig is not valid in cycle " + QString::number(cycle)));
244+
245+
// Rapidly change multiple properties
246+
for (int j = 0; j < 10; ++j) {
247+
dconfig->setValue(QStringLiteral("themeName"),
248+
QVariant::fromValue(QString("theme_" + QString::number(j))));
249+
dconfig->setValue(QStringLiteral("activeColor"),
250+
QVariant::fromValue(QString("color_" + QString::number(j))));
251+
QThread::msleep(1);
252+
}
253+
254+
// Delete while many updateValue calls are queued
255+
delete config;
256+
config = nullptr;
257+
258+
// Process events to execute queued lambdas
259+
for (int i = 0; i < 100; ++i) {
260+
QCoreApplication::processEvents();
261+
QThread::msleep(1);
262+
}
263+
}
264+
}
265+
266+
// Test 5: Multiple sequential operations - simulate multiple configs being created and destroyed
267+
void test_concurrent_configs()
268+
{
269+
if (!s_dConfigAvailable) {
270+
QSKIP("DConfig schema not available on this system");
271+
}
272+
273+
qDebug() << "\n=== Test 5: Multiple sequential config operations ===";
274+
275+
// Run 3 sequential operations (sequential instead of concurrent)
276+
for (int index = 1; index <= 3; ++index) {
277+
for (int cycle = 0; cycle < 5; ++cycle) {
278+
TreelandUserConfig *config = TreelandUserConfig::createByName("org.deepin.dde.treeland.user",
279+
"org.deepin.dde.treeland",
280+
"/dde");
281+
282+
QVERIFY2(config != nullptr, qPrintable("createByName returned nullptr at index " + QString::number(index) + " cycle " + QString::number(cycle)));
283+
284+
// Wait for initialization with timeout
285+
QTRY_VERIFY_WITH_TIMEOUT(config->isInitializeSucceed(), 2000);
286+
QVERIFY(config->isInitializeSucceed());
287+
288+
auto dconfig = config->config();
289+
QVERIFY2(dconfig != nullptr, "config() returned nullptr");
290+
QVERIFY2(dconfig->isValid(), qPrintable("DConfig is not valid at index " + QString::number(index)));
291+
292+
dconfig->setValue(QStringLiteral("themeName"),
293+
QVariant::fromValue(QString("seq_" + QString::number(index))));
294+
295+
delete config;
296+
297+
for (int i = 0; i < 20; ++i) {
298+
QCoreApplication::processEvents();
299+
QThread::msleep(2);
300+
}
301+
}
302+
}
303+
304+
// Process all remaining events
305+
for (int i = 0; i < 100; ++i) {
306+
QCoreApplication::processEvents();
307+
QThread::msleep(1);
308+
}
309+
}
310+
311+
// Test 6: Stress test - high frequency creation/destruction
312+
void test_high_frequency_stress()
313+
{
314+
if (!s_dConfigAvailable) {
315+
QSKIP("DConfig schema not available on this system");
316+
}
317+
318+
qDebug() << "\n=== Test 6: High frequency stress test ===";
319+
320+
// Read environment variable for configurable cycle count
321+
// Default: 20 for CI (fast), can be set to 100+ locally for comprehensive testing
322+
bool ok = false;
323+
int cycles = qEnvironmentVariableIntValue("TREELAND_STRESS_CYCLES", &ok);
324+
if (!ok || cycles <= 0) {
325+
cycles = 20; // Default for CI environments
326+
}
327+
328+
qDebug() << " Running stress test with" << cycles << "cycles (set TREELAND_STRESS_CYCLES to override)";
329+
330+
auto start = std::chrono::high_resolution_clock::now();
331+
332+
for (int cycle = 0; cycle < cycles; ++cycle) {
333+
TreelandUserConfig *config = TreelandUserConfig::createByName("org.deepin.dde.treeland.user",
334+
"org.deepin.dde.treeland",
335+
"/dde");
336+
337+
QVERIFY2(config != nullptr, qPrintable("createByName returned nullptr in cycle " + QString::number(cycle)));
338+
339+
// Minimal wait - let initialization start
340+
QThread::msleep(1);
341+
QCoreApplication::processEvents();
342+
343+
// Delete immediately
344+
delete config;
345+
346+
// Process some events
347+
for (int i = 0; i < 5; ++i) {
348+
QCoreApplication::processEvents();
349+
}
350+
}
351+
352+
auto end = std::chrono::high_resolution_clock::now();
353+
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
354+
355+
qDebug() << " " << cycles << "cycles completed in" << duration.count() << "ms";
356+
}
357+
358+
void cleanupTestCase()
359+
{
360+
// Final event processing to clean up any remaining objects
361+
for (int i = 0; i < 200; ++i) {
362+
QCoreApplication::processEvents();
363+
}
364+
365+
qDebug() << "=== Test Complete ===";
366+
}
367+
};
368+
369+
// Static member initialization
370+
bool ValidConfigCrashTest::s_dConfigAvailable = false;
371+
bool ValidConfigCrashTest::s_initialized = false;
372+
373+
#include "test_dconfig_valid_config.moc"
374+
QTEST_MAIN(ValidConfigCrashTest)

0 commit comments

Comments
 (0)