1 /*  This file is part of the KDE libraries
2 
3     SPDX-FileCopyrightText: 2012, 2019 David Faure <faure@kde.org>
4     SPDX-FileCopyrightText: 2012 Kai Dombrowe <just89@gmx.de>
5 
6     SPDX-License-Identifier: LGPL-2.1-or-later
7 */
8 
9 #include "netwm.h"
10 #include <QSignalSpy>
11 #include <QWidget>
12 #include <QX11Info>
13 #include <kstartupinfo.h>
14 #include <qtest_widgets.h>
15 
16 #include <xcb/xcb.h>
17 
18 Q_DECLARE_METATYPE(KStartupInfoId)
19 Q_DECLARE_METATYPE(KStartupInfoData)
20 
21 class KStartupInfo_UnitTest : public QObject
22 {
23     Q_OBJECT
24 public:
KStartupInfo_UnitTest()25     KStartupInfo_UnitTest()
26         : m_listener(KStartupInfo::CleanOnCantDetect, this)
27         , m_receivedCount(0)
28     {
29         qRegisterMetaType<KStartupInfoId>();
30         qRegisterMetaType<KStartupInfoData>();
31         connect(&m_listener, SIGNAL(gotNewStartup(KStartupInfoId, KStartupInfoData)), this, SLOT(slotNewStartup(KStartupInfoId, KStartupInfoData)));
32     }
33 
34 protected Q_SLOTS:
slotNewStartup(const KStartupInfoId & id,const KStartupInfoData & data)35     void slotNewStartup(const KStartupInfoId &id, const KStartupInfoData &data)
36     {
37         ++m_receivedCount;
38         m_receivedId = id;
39         m_receivedData = data;
40         Q_EMIT ready();
41     }
42 Q_SIGNALS:
43     void ready();
44 
45 private Q_SLOTS:
46     void testStart();
47     void dontCrashCleanup_data();
48     void dontCrashCleanup();
49     void checkCleanOnCantDetectTest();
50     void checkStartupTest_data();
51     void checkStartupTest();
52     void createNewStartupIdTest();
53     void createNewStartupIdForTimestampTest();
54     void setNewStartupIdTest();
55 
56 private:
57     KStartupInfo m_listener;
58 
59     int m_receivedCount;
60     KStartupInfoId m_receivedId;
61     KStartupInfoData m_receivedData;
62 };
63 
testStart()64 void KStartupInfo_UnitTest::testStart()
65 {
66     KStartupInfoId id;
67     id.initId(KStartupInfo::createNewStartupId());
68 
69     KStartupInfoData data;
70     const QString appId = "/dir with space/kstartupinfo_unittest.desktop";
71     data.setApplicationId(appId);
72     const QString iconPath = "/dir with space/kstartupinfo_unittest.png";
73     data.setIcon(iconPath);
74     const QString description = "A description";
75     data.setDescription(description);
76     const QString name = "A name";
77     data.setName(name);
78     const int pid = 12345;
79     data.addPid(pid);
80     const QString bin = "dir with space/kstartupinfo_unittest";
81     data.setBin(bin);
82 
83     QSignalSpy removedSpy(&m_listener, SIGNAL(gotRemoveStartup(KStartupInfoId, KStartupInfoData)));
84     QVERIFY(removedSpy.isValid());
85 
86     KStartupInfo::sendStartup(id, data);
87     KStartupInfo::sendFinish(id, data);
88 
89     QSignalSpy spy(this, SIGNAL(ready()));
90     spy.wait(5000);
91 
92     QCOMPARE(m_receivedCount, 1);
93     // qDebug() << m_receivedId.id(); // something like "$HOSTNAME;1342544979;490718;8602_TIME0"
94     QCOMPARE(m_receivedData.name(), name);
95     QCOMPARE(m_receivedData.description(), description);
96     QCOMPARE(m_receivedData.applicationId(), appId);
97     QCOMPARE(m_receivedData.icon(), iconPath);
98     QCOMPARE(m_receivedData.bin(), bin);
99     // qDebug() << m_receivedData.bin() << m_receivedData.name() << m_receivedData.description() << m_receivedData.icon() << m_receivedData.pids() <<
100     // m_receivedData.hostname() << m_receivedData.applicationId();
101 
102     int waitTime = 0;
103     while (waitTime < 5000 && removedSpy.count() < 1) {
104         QTest::qWait(200);
105         waitTime += 200;
106     }
107     QCOMPARE(removedSpy.count(), 1);
108 }
109 
doSync()110 static void doSync()
111 {
112     auto *c = QX11Info::connection();
113     const auto cookie = xcb_get_input_focus(c);
114     xcb_generic_error_t *error = nullptr;
115     QScopedPointer<xcb_get_input_focus_reply_t, QScopedPointerPodDeleter> sync(xcb_get_input_focus_reply(c, cookie, &error));
116     if (error) {
117         free(error);
118     }
119 }
120 
dontCrashCleanup_data()121 void KStartupInfo_UnitTest::dontCrashCleanup_data()
122 {
123     QTest::addColumn<bool>("silent");
124     QTest::addColumn<bool>("change");
125     QTest::addColumn<int>("countRemoveStartup");
126 
127     QTest::newRow("normal") << false << false << 2;
128     QTest::newRow("silent") << true << false << 0;
129     QTest::newRow("uninited") << false << true << 0;
130 }
131 
dontCrashCleanup()132 void KStartupInfo_UnitTest::dontCrashCleanup()
133 {
134     qputenv("KSTARTUPINFO_TIMEOUT", QByteArrayLiteral("1"));
135 
136     KStartupInfoId id;
137     KStartupInfoId id2;
138     id.initId(QByteArrayLiteral("somefancyidwhichisrandom_kstartupinfo_unittest_0"));
139     id2.initId(QByteArrayLiteral("somefancyidwhichisrandom_kstartupinfo_unittest_1"));
140 
141     KStartupInfoData data;
142     data.setApplicationId(QStringLiteral("/dir with space/kstartupinfo_unittest.desktop"));
143     data.setIcon(QStringLiteral("/dir with space/kstartupinfo_unittest.png"));
144     data.setDescription(QStringLiteral("A description"));
145     data.setName(QStringLiteral("A name"));
146     data.addPid(12345);
147     data.setBin(QStringLiteral("dir with space/kstartupinfo_unittest"));
148     QFETCH(bool, silent);
149     if (silent) {
150         data.setSilent(KStartupInfoData::Yes);
151     }
152 
153     QSignalSpy spy(&m_listener, SIGNAL(gotRemoveStartup(KStartupInfoId, KStartupInfoData)));
154     QFETCH(bool, change);
155     if (change) {
156         KStartupInfo::sendChange(id, data);
157         KStartupInfo::sendChange(id2, data);
158     } else {
159         KStartupInfo::sendStartup(id, data);
160         KStartupInfo::sendStartup(id2, data);
161     }
162 
163     // let's do a roundtrip to the X server
164     doSync();
165 
166     QFETCH(int, countRemoveStartup);
167     int waitTime = 1900;
168     QTest::qWait(1900);
169     while (waitTime <= 5000) {
170         QTest::qWait(200);
171         waitTime += 200;
172         if (spy.count() == countRemoveStartup) {
173             break;
174         }
175     }
176     QCOMPARE(spy.count(), countRemoveStartup);
177 }
178 
checkCleanOnCantDetectTest()179 void KStartupInfo_UnitTest::checkCleanOnCantDetectTest()
180 {
181     KStartupInfoId id;
182     KStartupInfoId id2;
183     id.initId(QByteArrayLiteral("somefancyidwhichisrandom_kstartupinfo_unittest_0"));
184     id2.initId(QByteArrayLiteral("somefancyidwhichisrandom_kstartupinfo_unittest_1"));
185 
186     KStartupInfoData data;
187     data.setApplicationId(QStringLiteral("/dir with space/kstartupinfo_unittest.desktop"));
188     data.setIcon(QStringLiteral("/dir with space/kstartupinfo_unittest.png"));
189     data.setDescription(QStringLiteral("A description"));
190     data.setName(QStringLiteral("A name"));
191     data.setBin(QStringLiteral("dir with space/kstartupinfo_unittest"));
192     data.setWMClass(QByteArrayLiteral("0"));
193 
194     xcb_connection_t *c = QX11Info::connection();
195     xcb_window_t window = xcb_generate_id(c);
196     uint32_t values[] = {XCB_EVENT_MASK_PROPERTY_CHANGE};
197     xcb_create_window(c,
198                       XCB_COPY_FROM_PARENT,
199                       window,
200                       QX11Info::appRootWindow(),
201                       0,
202                       0,
203                       100,
204                       100,
205                       0,
206                       XCB_COPY_FROM_PARENT,
207                       XCB_COPY_FROM_PARENT,
208                       XCB_CW_EVENT_MASK,
209                       values);
210 
211     KStartupInfo::sendStartup(id, data);
212     KStartupInfo::sendStartup(id2, data);
213 
214     int previousCount = m_receivedCount;
215 
216     doSync();
217     QTest::qWait(10);
218 
219     xcb_map_window(c, window);
220     xcb_flush(c);
221     QTest::qWait(10);
222 
223     xcb_unmap_window(c, window);
224     xcb_flush(c);
225     QTest::qWait(100);
226     xcb_map_window(c, window);
227     xcb_flush(c);
228 
229     QCOMPARE(m_receivedCount, previousCount + 2);
230     QCOMPARE(m_receivedId, id2);
231 }
232 
checkStartupTest_data()233 void KStartupInfo_UnitTest::checkStartupTest_data()
234 {
235     QTest::addColumn<QByteArray>("wmClass");
236     QTest::addColumn<int>("pid");
237 
238     QTest::newRow("wmClass") << QByteArrayLiteral("kstartupinfotest") << 0;
239     QTest::newRow("pid") << QByteArray() << 12345;
240 }
241 
checkStartupTest()242 void KStartupInfo_UnitTest::checkStartupTest()
243 {
244     KStartupInfoId id;
245     KStartupInfoId id2;
246     id.initId(QByteArrayLiteral("somefancyidwhichisrandom_kstartupinfo_unittest_0"));
247     id2.initId(QByteArrayLiteral("somefancyidwhichisrandom_kstartupinfo_unittest_1"));
248 
249     KStartupInfoData data;
250     data.setApplicationId(QStringLiteral("/dir with space/kstartupinfo_unittest.desktop"));
251     data.setIcon(QStringLiteral("/dir with space/kstartupinfo_unittest.png"));
252     data.setDescription(QStringLiteral("A description"));
253     data.setName(QStringLiteral("A name"));
254     data.setBin(QStringLiteral("dir with space/kstartupinfo_unittest"));
255     QFETCH(int, pid);
256     data.addPid(pid);
257     data.setHostname(QByteArrayLiteral("localhost"));
258 
259     // important for this test: WMClass
260     QFETCH(QByteArray, wmClass);
261     data.setWMClass(wmClass);
262 
263     xcb_connection_t *c = QX11Info::connection();
264     xcb_window_t window = xcb_generate_id(c);
265     uint32_t values[] = {XCB_EVENT_MASK_PROPERTY_CHANGE};
266     xcb_create_window(c,
267                       XCB_COPY_FROM_PARENT,
268                       window,
269                       QX11Info::appRootWindow(),
270                       0,
271                       0,
272                       100,
273                       100,
274                       0,
275                       XCB_COPY_FROM_PARENT,
276                       XCB_COPY_FROM_PARENT,
277                       XCB_CW_EVENT_MASK,
278                       values);
279 
280     xcb_change_property(c,
281                         XCB_PROP_MODE_REPLACE,
282                         window,
283                         XCB_ATOM_WM_CLASS,
284                         XCB_ATOM_STRING,
285                         8,
286                         wmClass.length() * 2 + 1,
287                         "kstartupinfotest\0kstartupinfotest");
288     xcb_change_property(c, XCB_PROP_MODE_REPLACE, window, XCB_ATOM_WM_CLIENT_MACHINE, XCB_ATOM_STRING, 8, 9, "localhost");
289     NETWinInfo winInfo(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::Properties(), NET::Properties2());
290     winInfo.setPid(pid);
291 
292     KStartupInfo info(KStartupInfo::DisableKWinModule | KStartupInfo::AnnounceSilenceChanges, this);
293     KStartupInfo::sendStartup(id, data);
294     KStartupInfo::sendStartup(id2, data);
295 
296     doSync();
297     QTest::qWait(100);
298 
299     QCOMPARE(info.checkStartup(window), KStartupInfo::Match);
300     QCOMPARE(info.checkStartup(window), KStartupInfo::Match);
301 }
302 
createNewStartupIdTest()303 void KStartupInfo_UnitTest::createNewStartupIdTest()
304 {
305     const QByteArray &id = KStartupInfo::createNewStartupId();
306     QVERIFY(!id.isEmpty());
307     const int index = id.indexOf(QByteArrayLiteral("TIME"));
308     QVERIFY(index != -1);
309     const QByteArray time = id.mid(index + 4);
310     QVERIFY(time.toULongLong() != 0u);
311 }
312 
createNewStartupIdForTimestampTest()313 void KStartupInfo_UnitTest::createNewStartupIdForTimestampTest()
314 {
315     const QByteArray &id = KStartupInfo::createNewStartupIdForTimestamp(5);
316     QVERIFY(!id.isEmpty());
317     const int index = id.indexOf(QByteArrayLiteral("TIME"));
318     QVERIFY(index != -1);
319     QCOMPARE(id.mid(index + 4).toULongLong(), 5u);
320 }
321 
setNewStartupIdTest()322 void KStartupInfo_UnitTest::setNewStartupIdTest()
323 {
324     {
325         QWindow window;
326         const QByteArray str = "somefancyidwhichisrandom_kstartupinfo_unittest_2";
327         KStartupInfo::setNewStartupId(&window, str);
328         QCOMPARE(KStartupInfo::startupId(), str);
329     }
330 
331 #if KWINDOWSYSTEM_ENABLE_DEPRECATED_SINCE(5, 62)
332     {
333         QWidget widget;
334         const QByteArray str = "somefancyidwhichisrandom_kstartupinfo_unittest_3";
335         KStartupInfo::setNewStartupId(&widget, str); // deprecated
336         QCOMPARE(KStartupInfo::startupId(), str);
337     }
338 #endif
339 }
340 
341 QTEST_MAIN(KStartupInfo_UnitTest)
342 
343 #include "kstartupinfo_unittest.moc"
344