/* SPDX-FileCopyrightText: 2013 Martin Gräßlin SPDX-License-Identifier: LGPL-2.1-or-later */ #include "nettesthelper.h" #include #include #include #include // system #include Q_DECLARE_METATYPE(NET::State) Q_DECLARE_METATYPE(NET::States) Q_DECLARE_METATYPE(NET::Actions) using Property = UniqueCPointer; // clang-format off #define INFO NETWinInfo info(m_connection, m_testWindow, m_rootWindow, NET::WMAllProperties, NET::WM2AllProperties, NET::WindowManager); #define ATOM(name) \ KXUtils::Atom atom(connection(), QByteArrayLiteral(#name)); #define UTF8 KXUtils::Atom utf8String(connection(), QByteArrayLiteral("UTF8_STRING")); #define GETPROP(type, length, formatSize) \ xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(connection(), false, m_testWindow, \ atom, type, 0, length); \ Property reply(xcb_get_property_reply(connection(), cookie, nullptr)); \ QVERIFY(reply); \ QCOMPARE(reply->format, uint8_t(formatSize)); \ QCOMPARE(reply->value_len, uint32_t(length)); #define VERIFYDELETED(t) \ xcb_get_property_cookie_t cookieDeleted = xcb_get_property_unchecked(connection(), false, m_testWindow, \ atom, t, 0, 1); \ Property replyDeleted(xcb_get_property_reply(connection(), cookieDeleted, nullptr)); \ QVERIFY(replyDeleted); \ QVERIFY(replyDeleted->type == XCB_ATOM_NONE); class NetWinInfoTestWM : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); void testState_data(); void testState(); void testVisibleName(); void testVisibleIconName(); void testDesktop_data(); void testDesktop(); void testOpacity_data(); void testOpacity(); void testAllowedActions_data(); void testAllowedActions(); void testFrameExtents(); void testFrameExtentsKDE(); void testFrameOverlap(); void testFullscreenMonitors(); private: bool hasAtomFlag(const xcb_atom_t *atoms, int atomsLenght, const QByteArray &actionName); void testStrut(xcb_atom_t atom, NETStrut(NETWinInfo:: *getter)(void)const, void (NETWinInfo:: *setter)(NETStrut), NET::Property property, NET::Property2 property2 = NET::Property2(0)); void waitForPropertyChange(NETWinInfo *info, xcb_atom_t atom, NET::Property prop, NET::Property2 prop2 = NET::Property2(0)); xcb_connection_t *connection() { return m_connection; } xcb_connection_t *m_connection; QList m_connections; std::unique_ptr m_xvfb; xcb_window_t m_rootWindow; xcb_window_t m_testWindow; QByteArray m_displayNumber; }; void NetWinInfoTestWM::initTestCase() { } void NetWinInfoTestWM::cleanupTestCase() { // close connection while (!m_connections.isEmpty()) { xcb_disconnect(m_connections.takeFirst()); } } void NetWinInfoTestWM::init() { // first reset just to be sure m_connection = nullptr; m_rootWindow = XCB_WINDOW_NONE; m_testWindow = XCB_WINDOW_NONE; const QString xfvbExec = QStandardPaths::findExecutable(QStringLiteral("Xvfb")); QVERIFY(!xfvbExec.isEmpty()); // start Xvfb m_xvfb.reset(new QProcess); // use pipe to pass fd to Xvfb to get back the display id int pipeFds[2]; QVERIFY(pipe(pipeFds) == 0); m_xvfb->start(QStringLiteral("Xvfb"), QStringList{ QStringLiteral("-displayfd"), QString::number(pipeFds[1]) }); QVERIFY(m_xvfb->waitForStarted()); QCOMPARE(m_xvfb->state(), QProcess::Running); // reads from pipe, closes write side close(pipeFds[1]); QFile readPipe; QVERIFY(readPipe.open(pipeFds[0], QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)); QByteArray displayNumber = readPipe.readLine(); readPipe.close(); displayNumber.prepend(QByteArray(":")); displayNumber.remove(displayNumber.size() -1, 1); m_displayNumber = displayNumber; // create X connection int screen = 0; m_connection = xcb_connect(displayNumber.constData(), &screen); QVERIFY(m_connection); QVERIFY(!xcb_connection_has_error(m_connection)); m_rootWindow = KXUtils::rootWindow(m_connection, screen); // create test window m_testWindow = xcb_generate_id(m_connection); uint32_t values[] = {XCB_EVENT_MASK_PROPERTY_CHANGE}; xcb_create_window(m_connection, XCB_COPY_FROM_PARENT, m_testWindow, m_rootWindow, 0, 0, 100, 100, 0, XCB_COPY_FROM_PARENT, XCB_COPY_FROM_PARENT, XCB_CW_EVENT_MASK, values); // and map it xcb_map_window(m_connection, m_testWindow); } void NetWinInfoTestWM::cleanup() { // destroy test window xcb_unmap_window(m_connection, m_testWindow); xcb_destroy_window(m_connection, m_testWindow); m_testWindow = XCB_WINDOW_NONE; // delay till clenupTestCase as otherwise xcb reuses the same memory address m_connections << connection(); // kill Xvfb m_xvfb->terminate(); m_xvfb->waitForFinished(); } void NetWinInfoTestWM::waitForPropertyChange(NETWinInfo *info, xcb_atom_t atom, NET::Property prop, NET::Property2 prop2) { while (true) { UniqueCPointer event(xcb_wait_for_event(connection())); if (!event) { break; } if ((event->response_type & ~0x80) != XCB_PROPERTY_NOTIFY) { continue; } xcb_property_notify_event_t *pe = reinterpret_cast(event.get()); if (pe->window != m_testWindow) { continue; } if (pe->atom != atom) { continue; } NET::Properties dirty; NET::Properties2 dirty2; info->event(event.get(), &dirty, &dirty2); if (prop != 0) { QVERIFY(dirty & prop); } if (prop2 != 0) { QVERIFY(dirty2 & prop2); } if (!prop) { QCOMPARE(dirty, NET::Properties()); } if (!prop2) { QCOMPARE(dirty2, NET::Properties2()); } break; } } bool NetWinInfoTestWM::hasAtomFlag(const xcb_atom_t *atoms, int atomsLength, const QByteArray &actionName) { KXUtils::Atom atom(connection(), actionName); if (atom == XCB_ATOM_NONE) { qDebug() << "get atom failed"; return false; } for (int i = 0; i < atomsLength; ++i) { if (atoms[i] == atom) { return true; } } return false; } void NetWinInfoTestWM::testAllowedActions_data() { QTest::addColumn("actions"); QTest::addColumn >("names"); const QByteArray move = QByteArrayLiteral("_NET_WM_ACTION_MOVE"); const QByteArray resize = QByteArrayLiteral("_NET_WM_ACTION_RESIZE"); const QByteArray minimize = QByteArrayLiteral("_NET_WM_ACTION_MINIMIZE"); const QByteArray shade = QByteArrayLiteral("_NET_WM_ACTION_SHADE"); const QByteArray stick = QByteArrayLiteral("_NET_WM_ACTION_STICK"); const QByteArray maxVert = QByteArrayLiteral("_NET_WM_ACTION_MAXIMIZE_VERT"); const QByteArray maxHoriz = QByteArrayLiteral("_NET_WM_ACTION_MAXIMIZE_HORZ"); const QByteArray fullscreen = QByteArrayLiteral("_NET_WM_ACTION_FULLSCREEN"); const QByteArray desktop = QByteArrayLiteral("_NET_WM_ACTION_CHANGE_DESKTOP"); const QByteArray close = QByteArrayLiteral("_NET_WM_ACTION_CLOSE"); QTest::newRow("move") << NET::Actions(NET::ActionMove) << (QList() << move); QTest::newRow("resize") << NET::Actions(NET::ActionResize) << (QList() << resize); QTest::newRow("minimize") << NET::Actions(NET::ActionMinimize) << (QList() << minimize); QTest::newRow("shade") << NET::Actions(NET::ActionShade) << (QList() << shade); QTest::newRow("stick") << NET::Actions(NET::ActionStick) << (QList() << stick); QTest::newRow("maxVert") << NET::Actions(NET::ActionMaxVert) << (QList() << maxVert); QTest::newRow("maxHoriz") << NET::Actions(NET::ActionMaxHoriz) << (QList() << maxHoriz); QTest::newRow("fullscreen") << NET::Actions(NET::ActionFullScreen) << (QList() << fullscreen); QTest::newRow("desktop") << NET::Actions(NET::ActionChangeDesktop) << (QList() << desktop); QTest::newRow("close") << NET::Actions(NET::ActionClose) << (QList() << close); QTest::newRow("none") << NET::Actions() << QList(); QTest::newRow("all") << NET::Actions(NET::ActionMove | NET::ActionResize | NET::ActionMinimize | NET::ActionShade | NET::ActionStick | NET::ActionMaxVert | NET::ActionMaxHoriz | NET::ActionFullScreen | NET::ActionChangeDesktop | NET::ActionClose) << (QList() << move << resize << minimize << shade << stick << maxVert << maxHoriz << fullscreen << desktop << close); } void NetWinInfoTestWM::testAllowedActions() { QVERIFY(connection()); ATOM(_NET_WM_ALLOWED_ACTIONS) INFO QCOMPARE(info.allowedActions(), NET::Actions()); QFETCH(NET::Actions, actions); info.setAllowedActions(actions); QCOMPARE(info.allowedActions(), actions); // compare with the X property QFETCH(QList, names); QVERIFY(atom != XCB_ATOM_NONE); GETPROP(XCB_ATOM_ATOM, names.size(), 32) xcb_atom_t *atoms = reinterpret_cast(xcb_get_property_value(reply.get())); for (int i = 0; i < names.size(); ++i) { QVERIFY(hasAtomFlag(atoms, names.size(), names.at(i))); } // and wait for our event waitForPropertyChange(&info, atom, NET::Property(0), NET::WM2AllowedActions); QCOMPARE(info.allowedActions(), actions); } void NetWinInfoTestWM::testDesktop_data() { QTest::addColumn("desktop"); QTest::addColumn("propertyDesktop"); QTest::newRow("1") << 1 << uint32_t(0); QTest::newRow("4") << 4 << uint32_t(3); QTest::newRow("on all") << int(NET::OnAllDesktops) << uint32_t(~0); } void NetWinInfoTestWM::testDesktop() { QVERIFY(connection()); ATOM(_NET_WM_DESKTOP) INFO QCOMPARE(info.desktop(), 0); QFETCH(int, desktop); info.setDesktop(desktop); QCOMPARE(info.desktop(), desktop); // compare with the X property QVERIFY(atom != XCB_ATOM_NONE); GETPROP(XCB_ATOM_CARDINAL, 1, 32) QTEST(reinterpret_cast(xcb_get_property_value(reply.get()))[0], "propertyDesktop"); // and wait for our event waitForPropertyChange(&info, atom, NET::WMDesktop); QCOMPARE(info.desktop(), desktop); // delete it info.setDesktop(0); QCOMPARE(info.desktop(), 0); VERIFYDELETED(XCB_ATOM_CARDINAL) // and wait for our event waitForPropertyChange(&info, atom, NET::WMDesktop); QCOMPARE(info.desktop(), 0); } void NetWinInfoTestWM::testStrut(xcb_atom_t atom, NETStrut(NETWinInfo:: *getter)(void)const, void (NETWinInfo:: *setter)(NETStrut), NET::Property property, NET::Property2 property2) { INFO NETStrut extents = (info.*getter)(); QCOMPARE(extents.bottom, 0); QCOMPARE(extents.left, 0); QCOMPARE(extents.right, 0); QCOMPARE(extents.top, 0); NETStrut newExtents; newExtents.bottom = 10; newExtents.left = 20; newExtents.right = 30; newExtents.top = 40; (info.*setter)(newExtents); extents = (info.*getter)(); QCOMPARE(extents.bottom, newExtents.bottom); QCOMPARE(extents.left, newExtents.left); QCOMPARE(extents.right, newExtents.right); QCOMPARE(extents.top, newExtents.top); // compare with the X property QVERIFY(atom != XCB_ATOM_NONE); GETPROP(XCB_ATOM_CARDINAL, 4, 32) uint32_t *data = reinterpret_cast(xcb_get_property_value(reply.get())); QCOMPARE(data[0], uint32_t(newExtents.left)); QCOMPARE(data[1], uint32_t(newExtents.right)); QCOMPARE(data[2], uint32_t(newExtents.top)); QCOMPARE(data[3], uint32_t(newExtents.bottom)); // and wait for our event waitForPropertyChange(&info, atom, property, property2); extents = (info.*getter)(); QCOMPARE(extents.bottom, newExtents.bottom); QCOMPARE(extents.left, newExtents.left); QCOMPARE(extents.right, newExtents.right); QCOMPARE(extents.top, newExtents.top); } void NetWinInfoTestWM::testFrameExtents() { QVERIFY(connection()); ATOM(_NET_FRAME_EXTENTS) testStrut(atom, &NETWinInfo::frameExtents, &NETWinInfo::setFrameExtents, NET::WMFrameExtents); } void NetWinInfoTestWM::testFrameExtentsKDE() { // same as testFrameExtents just with a different atom name QVERIFY(connection()); ATOM(_KDE_NET_WM_FRAME_STRUT) testStrut(atom, &NETWinInfo::frameExtents, &NETWinInfo::setFrameExtents, NET::WMFrameExtents); } void NetWinInfoTestWM::testFrameOverlap() { QVERIFY(connection()); ATOM(_NET_WM_FRAME_OVERLAP) testStrut(atom, &NETWinInfo::frameOverlap, &NETWinInfo::setFrameOverlap, NET::Property(0), NET::WM2FrameOverlap); } void NetWinInfoTestWM::testOpacity_data() { QTest::addColumn("opacity"); QTest::newRow("0 %") << uint32_t(0); QTest::newRow("50 %") << uint32_t(0x0000ffff); QTest::newRow("100 %") << uint32_t(0xffffffff); } void NetWinInfoTestWM::testOpacity() { QVERIFY(connection()); ATOM(_NET_WM_WINDOW_OPACITY) INFO QCOMPARE(info.opacity(), static_cast(0xffffffffU)); QFETCH(uint32_t, opacity); info.setOpacity(opacity); QCOMPARE(info.opacity(), static_cast(opacity)); // compare with the X property QVERIFY(atom != XCB_ATOM_NONE); GETPROP(XCB_ATOM_CARDINAL, 1, 32) QCOMPARE(reinterpret_cast(xcb_get_property_value(reply.get()))[0], opacity); // and wait for our event waitForPropertyChange(&info, atom, NET::Property(0), NET::WM2Opacity); QCOMPARE(info.opacity(), static_cast(opacity)); } void NetWinInfoTestWM::testState_data() { QTest::addColumn("states"); QTest::addColumn >("names"); const QByteArray modal = QByteArrayLiteral("_NET_WM_STATE_MODAL"); const QByteArray sticky = QByteArrayLiteral("_NET_WM_STATE_STICKY"); const QByteArray maxVert = QByteArrayLiteral("_NET_WM_STATE_MAXIMIZED_VERT"); const QByteArray maxHoriz = QByteArrayLiteral("_NET_WM_STATE_MAXIMIZED_HORZ"); const QByteArray shaded = QByteArrayLiteral("_NET_WM_STATE_SHADED"); const QByteArray skipTaskbar = QByteArrayLiteral("_NET_WM_STATE_SKIP_TASKBAR"); const QByteArray skipSwitcher = QByteArrayLiteral("_KDE_NET_WM_STATE_SKIP_SWITCHER"); const QByteArray keepAbove = QByteArrayLiteral("_NET_WM_STATE_ABOVE"); const QByteArray staysOnTop = QByteArrayLiteral("_NET_WM_STATE_STAYS_ON_TOP"); const QByteArray skipPager = QByteArrayLiteral("_NET_WM_STATE_SKIP_PAGER"); const QByteArray hidden = QByteArrayLiteral("_NET_WM_STATE_HIDDEN"); const QByteArray fullScreen = QByteArrayLiteral("_NET_WM_STATE_FULLSCREEN"); const QByteArray keepBelow = QByteArrayLiteral("_NET_WM_STATE_BELOW"); const QByteArray demandsAttention = QByteArrayLiteral("_NET_WM_STATE_DEMANDS_ATTENTION"); const QByteArray focused = QByteArrayLiteral("_NET_WM_STATE_FOCUSED"); QTest::newRow("modal") << NET::States(NET::Modal) << (QList() << modal); QTest::newRow("sticky") << NET::States(NET::Sticky) << (QList() << sticky); QTest::newRow("maxVert") << NET::States(NET::MaxVert) << (QList() << maxVert); QTest::newRow("maxHoriz") << NET::States(NET::MaxHoriz) << (QList() << maxHoriz); QTest::newRow("shaded") << NET::States(NET::Shaded) << (QList() << shaded); QTest::newRow("skipTaskbar") << NET::States(NET::SkipTaskbar) << (QList() << skipTaskbar); QTest::newRow("keepAbove") << NET::States(NET::KeepAbove) << (QList() << keepAbove << staysOnTop); QTest::newRow("skipPager") << NET::States(NET::SkipPager) << (QList() << skipPager); QTest::newRow("hidden") << NET::States(NET::Hidden) << (QList() << hidden); QTest::newRow("fullScreen") << NET::States(NET::FullScreen) << (QList() << fullScreen); QTest::newRow("keepBelow") << NET::States(NET::KeepBelow) << (QList() << keepBelow); QTest::newRow("demandsAttention") << NET::States(NET::DemandsAttention) << (QList() << demandsAttention); QTest::newRow("skipSwitcher") << NET::States(NET::SkipSwitcher) << (QList() << skipSwitcher); QTest::newRow("focused") << NET::States(NET::Focused) << (QList() << focused); // TODO: it's possible to be keep above and below at the same time?!? QTest::newRow("all") << NET::States(NET::Modal | NET::Sticky | NET::Max | NET::Shaded | NET::SkipTaskbar | NET::SkipPager | NET::KeepAbove | NET::KeepBelow | NET::Hidden | NET::FullScreen | NET::DemandsAttention | NET::SkipSwitcher | NET::Focused) << (QList() << modal << sticky << maxVert << maxHoriz << shaded << skipTaskbar << keepAbove << skipPager << hidden << fullScreen << keepBelow << demandsAttention << staysOnTop << skipSwitcher << focused); } void NetWinInfoTestWM::testState() { QVERIFY(connection()); ATOM(_NET_WM_STATE) INFO QCOMPARE(info.state(), NET::States()); QFETCH(NET::States, states); info.setState(states, NET::States()); QCOMPARE(info.state(), states); // compare with the X property QFETCH(QList, names); QVERIFY(atom != XCB_ATOM_NONE); GETPROP(XCB_ATOM_ATOM, names.size(), 32) xcb_atom_t *atoms = reinterpret_cast(xcb_get_property_value(reply.get())); for (int i = 0; i < names.size(); ++i) { QVERIFY(hasAtomFlag(atoms, names.size(), names.at(i))); } // and wait for our event waitForPropertyChange(&info, atom, NET::WMState); QCOMPARE(info.state(), states); } void NetWinInfoTestWM::testVisibleIconName() { QVERIFY(connection()); ATOM(_NET_WM_VISIBLE_ICON_NAME) UTF8 INFO QVERIFY(!info.visibleIconName()); info.setVisibleIconName("foo"); QCOMPARE(info.visibleIconName(), "foo"); // compare with the X property QVERIFY(atom != XCB_ATOM_NONE); QVERIFY(utf8String != XCB_ATOM_NONE); GETPROP(utf8String, 3, 8) QCOMPARE(reinterpret_cast(xcb_get_property_value(reply.get())), "foo"); // and wait for our event waitForPropertyChange(&info, atom, NET::WMVisibleIconName); QCOMPARE(info.visibleIconName(), "foo"); // delete the string info.setVisibleIconName(""); QCOMPARE(info.visibleIconName(), ""); VERIFYDELETED(utf8String) // and wait for our event waitForPropertyChange(&info, atom, NET::WMVisibleIconName); QVERIFY(!info.visibleIconName()); // set again, to ensure we don't leak on tear down info.setVisibleIconName("bar"); xcb_flush(connection()); waitForPropertyChange(&info, atom, NET::WMVisibleIconName); QCOMPARE(info.visibleIconName(), "bar"); } void NetWinInfoTestWM::testVisibleName() { QVERIFY(connection()); ATOM(_NET_WM_VISIBLE_NAME) UTF8 INFO QVERIFY(!info.visibleName()); info.setVisibleName("foo"); QCOMPARE(info.visibleName(), "foo"); // compare with the X property QVERIFY(atom != XCB_ATOM_NONE); QVERIFY(utf8String != XCB_ATOM_NONE); GETPROP(utf8String, 3, 8) QCOMPARE(reinterpret_cast(xcb_get_property_value(reply.get())), "foo"); // and wait for our event waitForPropertyChange(&info, atom, NET::WMVisibleName); QCOMPARE(info.visibleName(), "foo"); // delete the string info.setVisibleName(""); QCOMPARE(info.visibleName(), ""); VERIFYDELETED(utf8String) // and wait for our event waitForPropertyChange(&info, atom, NET::WMVisibleName); QVERIFY(!info.visibleName()); // set again, to ensure we don't leak on tear down info.setVisibleName("bar"); xcb_flush(connection()); waitForPropertyChange(&info, atom, NET::WMVisibleName); QCOMPARE(info.visibleName(), "bar"); } class MockWinInfo : public NETWinInfo { public: MockWinInfo(xcb_connection_t *connection, xcb_window_t window, xcb_window_t rootWindow) : NETWinInfo(connection, window, rootWindow, NET::WMAllProperties, NET::WM2AllProperties, NET::WindowManager) { } protected: void changeFullscreenMonitors(NETFullscreenMonitors topology) override { setFullscreenMonitors(topology); } }; void NetWinInfoTestWM::testFullscreenMonitors() { // test case for BUG 391960 QVERIFY(connection()); const uint32_t maskValues[] = { XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_COLOR_MAP_CHANGE | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_FOCUS_CHANGE | // For NotifyDetailNone XCB_EVENT_MASK_EXPOSURE }; UniqueCPointer redirectCheck(xcb_request_check(connection(), xcb_change_window_attributes_checked(connection(), m_rootWindow, XCB_CW_EVENT_MASK, maskValues))); QVERIFY(!redirectCheck); KXUtils::Atom atom(connection(), QByteArrayLiteral("_NET_WM_FULLSCREEN_MONITORS")); // create client connection auto clientConnection = xcb_connect(m_displayNumber.constData(), nullptr); QVERIFY(clientConnection); QVERIFY(!xcb_connection_has_error(clientConnection)); NETWinInfo clientInfo(clientConnection, m_testWindow, m_rootWindow, NET::WMAllProperties, NET::WM2AllProperties); NETFullscreenMonitors topology; topology.top = 1; topology.bottom = 2; topology.left = 3; topology.right = 4; clientInfo.setFullscreenMonitors(topology); xcb_flush(clientConnection); MockWinInfo info(connection(), m_testWindow, m_rootWindow); while (true) { UniqueCPointer event(xcb_wait_for_event(connection())); if (!event) { break; } if ((event->response_type & ~0x80) != XCB_CLIENT_MESSAGE) { continue; } NET::Properties dirtyProtocols; NET::Properties2 dirtyProtocols2; QCOMPARE(info.fullscreenMonitors().isSet(), false); info.event(event.get(), &dirtyProtocols, &dirtyProtocols2); QCOMPARE(info.fullscreenMonitors().isSet(), true); break; } xcb_flush(connection()); // now the property should be updated waitForPropertyChange(&info, atom, NET::Property(0), NET::WM2FullscreenMonitors); QCOMPARE(info.fullscreenMonitors().top, 1); QCOMPARE(info.fullscreenMonitors().bottom, 2); QCOMPARE(info.fullscreenMonitors().left, 3); QCOMPARE(info.fullscreenMonitors().right, 4); xcb_disconnect(clientConnection); } QTEST_GUILESS_MAIN(NetWinInfoTestWM) #include "netwininfotestwm.moc"