/******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2014 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "../plugins/platforms/x11/standalone/screens_xrandr.h" #include "../cursor.h" #include "../xcbutils.h" #include "mock_workspace.h" // Qt #include Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core") // mocking namespace KWin { QPoint Cursor::pos() { return QPoint(0, 0); } } // namespace KWin static xcb_window_t s_rootWindow = XCB_WINDOW_NONE; static xcb_connection_t *s_connection = nullptr; using namespace KWin; using namespace KWin::Xcb; class TestXRandRScreens : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testStartup(); void testChange(); void testMultipleChanges(); private: QScopedPointer m_xserver; }; void TestXRandRScreens::initTestCase() { // TODO: turn into init instead of initTestCase // needs to be initTestCase as KWin::connection caches the first created xcb_connection_t // thus changing X server for each test run would create problems qsrand(QDateTime::currentMSecsSinceEpoch()); // first reset just to be sure s_connection = nullptr; s_rootWindow = XCB_WINDOW_NONE; // start X Server m_xserver.reset(new QProcess); // randomize the display id in [1, 98] // 0 is not used because it conflicts with "normal" X server // 99 is not used because it's used by KDE's CI infrastructure const QString id = QStringLiteral(":") + QString::number((qrand() % 98) + 1); // using Xephyr as Xvfb doesn't support render extension m_xserver->start(QStringLiteral("Xephyr"), QStringList() << id); QVERIFY(m_xserver->waitForStarted()); QCOMPARE(m_xserver->state(), QProcess::Running); // give it some time before we open the X Display QTest::qWait(500); // create X connection int screen = 0; s_connection = xcb_connect(qPrintable(id), &screen); QVERIFY(s_connection); // set root window xcb_screen_iterator_t iter = xcb_setup_roots_iterator(xcb_get_setup(s_connection)); for (xcb_screen_iterator_t it = xcb_setup_roots_iterator(xcb_get_setup(s_connection)); it.rem; --screen, xcb_screen_next(&it)) { if (screen == 0) { s_rootWindow = iter.data->root; break; } } QVERIFY(s_rootWindow != XCB_WINDOW_NONE); qApp->setProperty("x11RootWindow", QVariant::fromValue(s_rootWindow)); qApp->setProperty("x11Connection", QVariant::fromValue(s_connection)); // get the extensions if (!Extensions::self()->isRandrAvailable()) { QSKIP("XRandR extension required"); } for (const auto &extension : Extensions::self()->extensions()) { if (extension.name == QByteArrayLiteral("RANDR")) { if (extension.version < 1 * 0x10 + 4) { QSKIP("At least XRandR 1.4 required"); } } } } void TestXRandRScreens::cleanupTestCase() { Extensions::destroy(); // close connection xcb_disconnect(s_connection); s_connection = nullptr; s_rootWindow = XCB_WINDOW_NONE; // kill X m_xserver->terminate(); m_xserver->waitForFinished(); } void TestXRandRScreens::testStartup() { KWin::MockWorkspace ws; QScopedPointer screens(new XRandRScreens(this)); QVERIFY(screens->eventType() != 0); QCOMPARE(screens->eventType(), Xcb::Extensions::self()->randrNotifyEvent()); QCOMPARE(screens->extension(), 0); QCOMPARE(screens->genericEventType(), 0); screens->init(); QRect xephyrDefault = QRect(0, 0, 640, 480); QCOMPARE(screens->count(), 1); QCOMPARE(screens->geometry(0), xephyrDefault); QCOMPARE(screens->geometry(1), QRect()); QCOMPARE(screens->geometry(-1), QRect()); QCOMPARE(static_cast(screens.data())->geometry(), xephyrDefault); QCOMPARE(screens->size(0), xephyrDefault.size()); QCOMPARE(screens->size(1), QSize()); QCOMPARE(screens->size(-1), QSize()); QCOMPARE(static_cast(screens.data())->size(), xephyrDefault.size()); // unfortunately we only have one output, so let's try at least to test somewhat QCOMPARE(screens->number(QPoint(0, 0)), 0); QCOMPARE(screens->number(QPoint(639, 479)), 0); QCOMPARE(screens->number(QPoint(1280, 1024)), 0); // let's change the mode RandR::CurrentResources resources(s_rootWindow); auto *crtcs = resources.crtcs(); auto *modes = xcb_randr_get_screen_resources_current_modes(resources.data()); auto *outputs = xcb_randr_get_screen_resources_current_outputs(resources.data()); RandR::SetCrtcConfig setter(crtcs[0], resources->timestamp, resources->config_timestamp, 0, 0, modes[0].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); QVERIFY(!setter.isNull()); // now let's recreate the XRandRScreens screens.reset(new XRandRScreens(this)); screens->init(); QRect geo = QRect(0, 0, modes[0].width, modes[0].height); QCOMPARE(screens->count(), 1); QCOMPARE(screens->geometry(0), geo); QCOMPARE(static_cast(screens.data())->geometry(), geo); QCOMPARE(screens->size(0), geo.size()); QCOMPARE(static_cast(screens.data())->size(), geo.size()); } void TestXRandRScreens::testChange() { KWin::MockWorkspace ws; QScopedPointer screens(new XRandRScreens(this)); screens->init(); // create some signal spys QSignalSpy changedSpy(screens.data(), SIGNAL(changed())); QVERIFY(changedSpy.isValid()); QVERIFY(changedSpy.isEmpty()); QVERIFY(changedSpy.wait()); changedSpy.clear(); QSignalSpy geometrySpy(screens.data(), SIGNAL(geometryChanged())); QVERIFY(geometrySpy.isValid()); QVERIFY(geometrySpy.isEmpty()); QSignalSpy sizeSpy(screens.data(), SIGNAL(sizeChanged())); QVERIFY(sizeSpy.isValid()); QVERIFY(sizeSpy.isEmpty()); // clear the event loop while (xcb_generic_event_t *e = xcb_poll_for_event(s_connection)) { free(e); } // let's change RandR::CurrentResources resources(s_rootWindow); auto *crtcs = resources.crtcs(); auto *modes = xcb_randr_get_screen_resources_current_modes(resources.data()); auto *outputs = xcb_randr_get_screen_resources_current_outputs(resources.data()); RandR::SetCrtcConfig setter(crtcs[0], resources->timestamp, resources->config_timestamp, 0, 0, modes[1].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); xcb_flush(s_connection); QVERIFY(!setter.isNull()); QVERIFY(setter->status == XCB_RANDR_SET_CONFIG_SUCCESS); xcb_generic_event_t *e = xcb_wait_for_event(s_connection); screens->event(e); free(e); QVERIFY(changedSpy.wait()); QCOMPARE(changedSpy.size(), 1); QCOMPARE(sizeSpy.size(), 1); QCOMPARE(geometrySpy.size(), 1); QRect geo = QRect(0, 0, modes[1].width, modes[1].height); QCOMPARE(screens->count(), 1); QCOMPARE(screens->geometry(0), geo); QCOMPARE(static_cast(screens.data())->geometry(), geo); QCOMPARE(screens->size(0), geo.size()); QCOMPARE(static_cast(screens.data())->size(), geo.size()); } void TestXRandRScreens::testMultipleChanges() { KWin::MockWorkspace ws; // multiple changes should only hit one changed signal QScopedPointer screens(new XRandRScreens(this)); screens->init(); // create some signal spys QSignalSpy changedSpy(screens.data(), SIGNAL(changed())); QVERIFY(changedSpy.isValid()); QVERIFY(changedSpy.isEmpty()); QVERIFY(changedSpy.wait()); changedSpy.clear(); QSignalSpy geometrySpy(screens.data(), SIGNAL(geometryChanged())); QVERIFY(geometrySpy.isValid()); QVERIFY(geometrySpy.isEmpty()); QSignalSpy sizeSpy(screens.data(), SIGNAL(sizeChanged())); QVERIFY(sizeSpy.isValid()); QVERIFY(sizeSpy.isEmpty()); // clear the event loop while (xcb_generic_event_t *e = xcb_poll_for_event(s_connection)) { free(e); } // first change RandR::CurrentResources resources(s_rootWindow); auto *crtcs = resources.crtcs(); auto *modes = xcb_randr_get_screen_resources_current_modes(resources.data()); auto *outputs = xcb_randr_get_screen_resources_current_outputs(resources.data()); RandR::SetCrtcConfig setter(crtcs[0], resources->timestamp, resources->config_timestamp, 0, 0, modes[0].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); QVERIFY(!setter.isNull()); QVERIFY(setter->status == XCB_RANDR_SET_CONFIG_SUCCESS); // second change RandR::SetCrtcConfig setter2(crtcs[0], setter->timestamp, resources->config_timestamp, 0, 0, modes[1].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); QVERIFY(!setter2.isNull()); QVERIFY(setter2->status == XCB_RANDR_SET_CONFIG_SUCCESS); auto passEvent = [&screens]() { xcb_generic_event_t *e = xcb_wait_for_event(s_connection); screens->event(e); free(e); }; passEvent(); passEvent(); QVERIFY(changedSpy.wait()); QCOMPARE(changedSpy.size(), 1); // previous state was modes[1] so the size didn't change QVERIFY(sizeSpy.isEmpty()); QVERIFY(geometrySpy.isEmpty()); QRect geo = QRect(0, 0, modes[1].width, modes[1].height); QCOMPARE(screens->count(), 1); QCOMPARE(screens->geometry(0), geo); QCOMPARE(static_cast(screens.data())->geometry(), geo); QCOMPARE(screens->size(0), geo.size()); QCOMPARE(static_cast(screens.data())->size(), geo.size()); } QTEST_GUILESS_MAIN(TestXRandRScreens) #include "test_xrandr_screens.moc"