Introduce a test server

Summary:
The idea of shipping a test server is to have something like Xvfb for
Wayland. To be able to run a test application with a fake Wayland server.

To make this super easy the test server binary is installed into libexec
directory and provides a cmake function to run a test application with
the test server.

The test server takes full control over the process. It's a guiless
application and starts the passed test application once it is fully
set up. The environment is setup to have the test application connect
to the fake server (WAYLAND_SOCKET env variable and QT_QPA_PLATFORM).

When the started application finishes the test server goes down and
exits with the exit value of the test application. This allows a good
integration with ctest.

The test server is a virtual server which supports the following
interfaces:
* Shm
* Compositor
* Shell
* Seat
* DataDeviceManager
* Idle
* SubCompositor
* Output (1280x1024 at 60 Hz with 96 dpi)
* FakeInput

This is sufficient to bring up a QtWayland based application and
allows some basic interactions from a test application (e.g. fake
input).

So far the server fakes a repaint every 16 msec, but does not yet
pass events to the test applications.

To integrate this into an application for testing use:
    find_package(KF5Wayland CONFIG)
    add_executable(myTest myTest.cpp)
    target_link_libraries(myTest Qt5::Gui Qt5::Test)
    kwaylandtest(myTest)

When now running ctest in the build directory the test server gets
started and will start the myTest binary and report the passed/failed
in the expected and normal way.

This way a test case can easily be run against both X11 and Wayland.

Reviewers: #plasma

Subscribers: plasma-devel

Tags: #plasma

Differential Revision: https://phabricator.kde.org/D1726

Changelog: Virtual framebuffer server for auto tests
This commit is contained in:
Martin Gräßlin 2016-05-31 15:41:29 +02:00
parent 151a910f4c
commit 682652714d
6 changed files with 316 additions and 0 deletions

View file

@ -3,3 +3,10 @@
find_dependency(Qt5Gui @REQUIRED_QT_VERSION@)
include("${CMAKE_CURRENT_LIST_DIR}/KF5WaylandTargets.cmake")
function(kwaylandtest testBinaryName)
add_test(NAME ${testBinaryName}-kwayland-test COMMAND
@CMAKE_INSTALL_FULL_LIBEXECDIR@/org-kde-kf5-kwayland-testserver ${CMAKE_CURRENT_BINARY_DIR}/${testBinaryName}
)
endfunction()

View file

@ -1,3 +1,5 @@
add_subdirectory(testserver)
include(ECMMarkAsTest)
find_package(Qt5Concurrent ${QT_MIN_VERSION} CONFIG QUIET)

View file

@ -0,0 +1,3 @@
add_executable(org-kde-kf5-kwayland-testserver main.cpp testserver.cpp)
target_link_libraries(org-kde-kf5-kwayland-testserver Qt5::Core KF5::WaylandServer)
install(TARGETS org-kde-kf5-kwayland-testserver DESTINATION ${LIBEXEC_INSTALL_DIR} )

View file

@ -0,0 +1,39 @@
/********************************************************************
Copyright 2016 Martin Gräßlin <mgraesslin@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) version 3, or any
later version accepted by the membership of KDE e.V. (or its
successor approved by the membership of KDE e.V.), which shall
act as a proxy defined in Section 6 of version 3 of the license.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#include "testserver.h"
#include <QCoreApplication>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
auto arguments = QCoreApplication::arguments();
// get rid of our own application path
arguments.removeFirst();
if (arguments.size() < 1) {
return 1;
}
TestServer *server = new TestServer(&a);
server->init();
server->startTestApp(arguments.takeFirst(), arguments);
return a.exec();
}

View file

@ -0,0 +1,200 @@
/********************************************************************
Copyright 2016 Martin Gräßlin <mgraesslin@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) version 3, or any
later version accepted by the membership of KDE e.V. (or its
successor approved by the membership of KDE e.V.), which shall
act as a proxy defined in Section 6 of version 3 of the license.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#include "testserver.h"
#include "../../server/display.h"
#include "../../server/compositor_interface.h"
#include "../../server/datadevicemanager_interface.h"
#include "../../server/idle_interface.h"
#include "../../server/fakeinput_interface.h"
#include "../../server/seat_interface.h"
#include "../../server/shell_interface.h"
#include "../../server/surface_interface.h"
#include "../../server/subcompositor_interface.h"
#include <QCoreApplication>
#include <QElapsedTimer>
#include <QProcess>
#include <QTimer>
// system
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
using namespace KWayland::Server;
TestServer::TestServer(QObject *parent)
: QObject(parent)
, m_repaintTimer(new QTimer(this))
, m_timeSinceStart(new QElapsedTimer)
, m_cursorPos(QPointF(0, 0))
{
}
TestServer::~TestServer() = default;
void TestServer::init()
{
Q_ASSERT(!m_display);
m_display = new Display(this);
m_display->start(Display::StartMode::ConnectClientsOnly);
m_display->createShm();
m_display->createCompositor()->create();
m_shell = m_display->createShell(m_display);
connect(m_shell, &ShellInterface::surfaceCreated, this,
[this] (ShellSurfaceInterface *surface) {
m_shellSurfaces << surface;
// TODO: pass keyboard/pointer/touch focus on mapped
connect(surface, &QObject::destroyed, this,
[this, surface] {
m_shellSurfaces.removeOne(surface);
}
);
}
);
m_shell->create();
m_seat = m_display->createSeat(m_display);
m_seat->setHasKeyboard(true);
m_seat->setHasPointer(true);
m_seat->setHasTouch(true);
m_seat->create();
m_display->createDataDeviceManager(m_display)->create();
m_display->createIdle(m_display)->create();
m_display->createSubCompositor(m_display)->create();
// output
auto output = m_display->createOutput(m_display);
const QSize size(1280, 1024);
output->setGlobalPosition(QPoint(0, 0));
output->setPhysicalSize(size / 3.8);
output->addMode(size);
output->create();
auto fakeInput = m_display->createFakeInput(m_display);
fakeInput->create();
connect(fakeInput, &FakeInputInterface::deviceCreated, this,
[this] (FakeInputDevice *device) {
device->setAuthentication(true);
connect(device, &FakeInputDevice::pointerMotionRequested, this,
[this] (const QSizeF &delta) {
m_seat->setTimestamp(m_timeSinceStart->elapsed());
m_cursorPos = m_cursorPos + QPointF(delta.width(), delta.height());
m_seat->setPointerPos(m_cursorPos);
}
);
connect(device, &FakeInputDevice::pointerButtonPressRequested, this,
[this] (quint32 button) {
m_seat->setTimestamp(m_timeSinceStart->elapsed());
m_seat->pointerButtonPressed(button);
}
);
connect(device, &FakeInputDevice::pointerButtonReleaseRequested, this,
[this] (quint32 button) {
m_seat->setTimestamp(m_timeSinceStart->elapsed());
m_seat->pointerButtonReleased(button);
}
);
connect(device, &FakeInputDevice::pointerAxisRequested, this,
[this] (Qt::Orientation orientation, qreal delta) {
m_seat->setTimestamp(m_timeSinceStart->elapsed());
m_seat->pointerAxis(orientation, delta);
}
);
connect(device, &FakeInputDevice::touchDownRequested, this,
[this] (quint32 id, const QPointF &pos) {
m_seat->setTimestamp(m_timeSinceStart->elapsed());
m_touchIdMapper.insert(id, m_seat->touchDown(pos));
}
);
connect(device, &FakeInputDevice::touchMotionRequested, this,
[this] (quint32 id, const QPointF &pos) {
m_seat->setTimestamp(m_timeSinceStart->elapsed());
const auto it = m_touchIdMapper.constFind(id);
if (it != m_touchIdMapper.constEnd()) {
m_seat->touchMove(it.value(), pos);
}
}
);
connect(device, &FakeInputDevice::touchUpRequested, this,
[this] (quint32 id) {
m_seat->setTimestamp(m_timeSinceStart->elapsed());
const auto it = m_touchIdMapper.find(id);
if (it != m_touchIdMapper.end()) {
m_seat->touchUp(it.value());
m_touchIdMapper.erase(it);
}
}
);
connect(device, &FakeInputDevice::touchCancelRequested, this,
[this] {
m_seat->setTimestamp(m_timeSinceStart->elapsed());
m_seat->cancelTouchSequence();
}
);
connect(device, &FakeInputDevice::touchFrameRequested, this,
[this] {
m_seat->setTimestamp(m_timeSinceStart->elapsed());
m_seat->touchFrame();
}
);
}
);
m_repaintTimer->setInterval(1000 / 60);
connect(m_repaintTimer, &QTimer::timeout, this, &TestServer::repaint);
m_repaintTimer->start();
m_timeSinceStart->start();
}
void TestServer::startTestApp(const QString &app, const QStringList &arguments)
{
int sx[2];
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) < 0) {
QCoreApplication::instance()->exit(1);
return;
}
m_display->createClient(sx[0]);
int socket = dup(sx[1]);
if (socket == -1) {
QCoreApplication::instance()->exit(1);
return;
}
QProcess *p = new QProcess(this);
p->setProcessChannelMode(QProcess::ForwardedChannels);
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
environment.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("wayland"));
environment.insert(QStringLiteral("WAYLAND_SOCKET"), QString::fromUtf8(QByteArray::number(socket)));
p->setProcessEnvironment(environment);
auto finishedSignal = static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished);
connect(p, finishedSignal, QCoreApplication::instance(), &QCoreApplication::exit);
auto errorSignal = static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::error);
connect(p, errorSignal, this,
[] {
QCoreApplication::instance()->exit(1);
}
);
p->start(app, arguments);
}
void TestServer::repaint()
{
for (auto it = m_shellSurfaces.constBegin(), end = m_shellSurfaces.constEnd(); it != end; ++it) {
(*it)->surface()->frameRendered(m_timeSinceStart->elapsed());
}
}

View file

@ -0,0 +1,65 @@
/********************************************************************
Copyright 2016 Martin Gräßlin <mgraesslin@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) version 3, or any
later version accepted by the membership of KDE e.V. (or its
successor approved by the membership of KDE e.V.), which shall
act as a proxy defined in Section 6 of version 3 of the license.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#ifndef TESTSERVER_H
#define TESTSERVER_H
#include <QHash>
#include <QObject>
#include <QPointF>
#include <QVector>
class QElapsedTimer;
class QTimer;
namespace KWayland
{
namespace Server
{
class Display;
class SeatInterface;
class ShellInterface;
class ShellSurfaceInterface;
}
}
class TestServer : public QObject
{
Q_OBJECT
public:
explicit TestServer(QObject *parent);
virtual ~TestServer();
void init();
void startTestApp(const QString &app, const QStringList &arguments);
private:
void repaint();
KWayland::Server::Display *m_display = nullptr;
KWayland::Server::ShellInterface *m_shell = nullptr;
KWayland::Server::SeatInterface *m_seat = nullptr;
QVector<KWayland::Server::ShellSurfaceInterface*> m_shellSurfaces;
QTimer *m_repaintTimer;
QScopedPointer<QElapsedTimer> m_timeSinceStart;
QPointF m_cursorPos;
QHash<qint32, qint32> m_touchIdMapper;
};
#endif