Client: Add test for wl_data_offer leaks

Also refactors the mocking code for data device manager and implements its
isClean method to verify there are no leaks after each test.

MockCompositor::DataDevice now persists across get_data_device requests.

Task-number: QTBUG-73825
Change-Id: Ib5866e0c54d021e12557f9a96f27950010f2611b
Reviewed-by: Paul Olav Tvete <paul.tvete@qt.io>
This commit is contained in:
Johan Klokkhammer Helsing 2019-02-14 10:26:48 +01:00 committed by Johan Helsing
parent 04be9cf380
commit de2fd1db6b
4 changed files with 153 additions and 30 deletions

View File

@ -47,7 +47,7 @@ public:
add<DataDeviceManager>(dataDeviceVersion);
});
}
DataDevice *dataDevice() { return get<Seat>()->dataDevice(); }
DataDevice *dataDevice() { return get<DataDeviceManager>()->deviceFor(get<Seat>()); }
};
class tst_datadevicev1 : public QObject, private DataDeviceCompositor
@ -58,6 +58,8 @@ private slots:
void initTestCase();
void pasteAscii();
void pasteUtf8();
void destroysPreviousSelection();
void destroysSelectionWithSurface();
};
void tst_datadevicev1::initTestCase()
@ -69,7 +71,8 @@ void tst_datadevicev1::initTestCase()
QCOMPOSITOR_TRY_VERIFY(keyboard());
QCOMPOSITOR_TRY_VERIFY(dataDevice());
QCOMPOSITOR_TRY_COMPARE(dataDevice()->resource()->version(), dataDeviceVersion);
QCOMPOSITOR_TRY_VERIFY(dataDevice()->resourceMap().contains(client()));
QCOMPOSITOR_TRY_COMPARE(dataDevice()->resourceMap().value(client())->version(), dataDeviceVersion);
}
void tst_datadevicev1::pasteAscii()
@ -86,7 +89,8 @@ void tst_datadevicev1::pasteAscii()
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([&] {
auto *offer = new DataOffer(client(), dataDeviceVersion); // Cleaned up by destroy_resource
auto *client = xdgSurface()->resource()->client();
auto *offer = dataDevice()->sendDataOffer(client, {"text/plain"});
connect(offer, &DataOffer::receive, [](QString mimeType, int fd) {
QFile file;
file.open(fd, QIODevice::WriteOnly, QFile::FileHandleFlag::AutoCloseHandle);
@ -94,16 +98,14 @@ void tst_datadevicev1::pasteAscii()
file.write(QByteArray("normal ascii"));
file.close();
});
dataDevice()->sendDataOffer(offer);
offer->send_offer("text/plain");
dataDevice()->sendSelection(offer);
auto *surface = xdgSurface()->m_surface;
keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol
pointer()->sendEnter(surface, {32, 32});
pointer()->sendButton(client(), BTN_LEFT, 1);
pointer()->sendButton(client(), BTN_LEFT, 0);
pointer()->sendButton(client, BTN_LEFT, 1);
pointer()->sendButton(client, BTN_LEFT, 0);
});
QTRY_COMPARE(window.m_text, "normal ascii");
}
@ -122,7 +124,8 @@ void tst_datadevicev1::pasteUtf8()
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([&] {
auto *offer = new DataOffer(client(), dataDeviceVersion); // Cleaned up by destroy_resource
auto *client = xdgSurface()->resource()->client();
auto *offer = dataDevice()->sendDataOffer(client, {"text/plain", "text/plain;charset=utf-8"});
connect(offer, &DataOffer::receive, [](QString mimeType, int fd) {
QFile file;
file.open(fd, QIODevice::WriteOnly, QFile::FileHandleFlag::AutoCloseHandle);
@ -130,20 +133,81 @@ void tst_datadevicev1::pasteUtf8()
file.write(QByteArray("face with tears of joy: 😂"));
file.close();
});
dataDevice()->sendDataOffer(offer);
offer->send_offer("text/plain");
offer->send_offer("text/plain;charset=utf-8");
dataDevice()->sendSelection(offer);
auto *surface = xdgSurface()->m_surface;
keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol
pointer()->sendEnter(surface, {32, 32});
pointer()->sendButton(client(), BTN_LEFT, 1);
pointer()->sendButton(client(), BTN_LEFT, 0);
pointer()->sendButton(client, BTN_LEFT, 1);
pointer()->sendButton(client, BTN_LEFT, 0);
});
QTRY_COMPARE(window.m_text, "face with tears of joy: 😂");
}
void tst_datadevicev1::destroysPreviousSelection()
{
QRasterWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
// When the client receives a selection event, it is required to destroy the previous offer
exec([&] {
QCOMPARE(dataDevice()->m_sentSelectionOffers.size(), 0);
auto *offer = dataDevice()->sendDataOffer(client(), {"text/plain"});
dataDevice()->sendSelection(offer);
auto *surface = xdgSurface()->m_surface;
keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol
QCOMPARE(dataDevice()->m_sentSelectionOffers.size(), 1);
});
exec([&] {
auto *offer = dataDevice()->sendDataOffer(client(), {"text/plain"});
dataDevice()->sendSelection(offer);
QCOMPARE(dataDevice()->m_sentSelectionOffers.size(), 2);
});
// Verify the first offer gets destroyed
QCOMPOSITOR_TRY_COMPARE(dataDevice()->m_sentSelectionOffers.size(), 1);
exec([&] {
auto *offer = dataDevice()->sendDataOffer(client(), {"text/plain"});
dataDevice()->sendSelection(offer);
auto *surface = xdgSurface()->m_surface;
keyboard()->sendLeave(surface);
});
// Clients are required to destroy their offer when losing keyboard focus
QCOMPOSITOR_TRY_COMPARE(dataDevice()->m_sentSelectionOffers.size(), 0);
}
void tst_datadevicev1::destroysSelectionWithSurface()
{
auto *window = new QRasterWindow;
window->resize(64, 64);
window->show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
// When the client receives a selection event, it is required to destroy the previous offer
exec([&] {
QCOMPARE(dataDevice()->m_sentSelectionOffers.size(), 0);
auto *offer = dataDevice()->sendDataOffer(client(), {"text/plain"});
dataDevice()->sendSelection(offer);
auto *surface = xdgSurface()->m_surface;
keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol
QCOMPARE(dataDevice()->m_sentSelectionOffers.size(), 1);
});
// Ping to make sure we receive the wl_keyboard enter and leave events, before destroying the
// surface. Otherwise, the client will receive enter and leave events with a destroyed (null)
// surface, which is not what we are trying to test for here.
xdgPingAndWaitForPong();
window->destroy();
QCOMPOSITOR_TRY_COMPARE(dataDevice()->m_sentSelectionOffers.size(), 0);
}
QCOMPOSITOR_TEST_MAIN(tst_datadevicev1)
#include "tst_datadevicev1.moc"

View File

@ -250,9 +250,6 @@ public:
Keyboard* m_keyboard = nullptr;
QVector<Keyboard *> m_oldKeyboards;
DataDevice *dataDevice() { return m_dataDevice.data(); }
QScopedPointer<DataDevice> m_dataDevice;
uint m_capabilities = 0;
protected:

View File

@ -30,29 +30,60 @@
namespace MockCompositor {
bool DataDeviceManager::isClean()
{
for (auto *device : qAsConst(m_dataDevices)) {
// The client should not leak selection offers, i.e. if this fails, there is a missing
// data_offer.destroy request
if (!device->m_sentSelectionOffers.empty())
return false;
}
return true;
}
DataDevice *DataDeviceManager::deviceFor(Seat *seat)
{
Q_ASSERT(seat);
if (auto *device = m_dataDevices.value(seat, nullptr))
return device;
auto *device = new DataDevice(this, seat);
m_dataDevices[seat] = device;
return device;
}
void DataDeviceManager::data_device_manager_get_data_device(Resource *resource, uint32_t id, wl_resource *seatResource)
{
auto *seat = fromResource<Seat>(seatResource);
QVERIFY(seat);
QVERIFY(!seat->m_dataDevice);
seat->m_dataDevice.reset(new DataDevice(resource->client(), id, resource->version()));
auto *device = deviceFor(seat);
device->add(resource->client(), id, resource->version());
}
DataDevice::~DataDevice()
{
// If the client hasn't deleted the wayland object, just ignore subsequent events
if (auto *r = resource()->handle)
wl_resource_set_implementation(r, nullptr, nullptr, nullptr);
// If the client(s) hasn't deleted the wayland object, just ignore subsequent events
for (auto *r : resourceMap())
wl_resource_set_implementation(r->handle, nullptr, nullptr, nullptr);
}
void DataDevice::sendDataOffer(DataOffer *offer)
DataOffer *DataDevice::sendDataOffer(wl_client *client, const QStringList &mimeTypes)
{
wl_data_device::send_data_offer(offer->resource()->handle);
Q_ASSERT(client);
auto *offer = new DataOffer(this, client, m_manager->m_version);
for (auto *resource : resourceMap().values(client))
wl_data_device::send_data_offer(resource->handle, offer->resource()->handle);
for (const auto &mimeType : mimeTypes)
offer->send_offer(mimeType);
return offer;
}
void DataDevice::sendSelection(DataOffer *offer)
{
wl_data_device::send_selection(offer->resource()->handle);
auto *client = offer->resource()->client();
for (auto *resource : resourceMap().values(client))
wl_data_device::send_selection(resource->handle, offer->resource()->handle);
m_sentSelectionOffers << offer;
}
void DataOffer::data_offer_destroy_resource(Resource *resource)
@ -67,4 +98,11 @@ void DataOffer::data_offer_receive(Resource *resource, const QString &mime_type,
emit receive(mime_type, fd);
}
void DataOffer::data_offer_destroy(QtWaylandServer::wl_data_offer::Resource *resource)
{
bool removed = m_dataDevice->m_sentSelectionOffers.removeOne(this);
QVERIFY(removed);
wl_resource_destroy(resource->handle);
}
} // namespace MockCompositor

View File

@ -42,8 +42,14 @@ class DataDeviceManager : public Global, public QtWaylandServer::wl_data_device_
public:
explicit DataDeviceManager(CoreCompositor *compositor, int version = 1)
: QtWaylandServer::wl_data_device_manager(compositor->m_display, version)
, m_version(version)
{}
QVector<DataDevice *> m_dataDevices;
~DataDeviceManager() override { qDeleteAll(m_dataDevices); }
bool isClean() override;
DataDevice *deviceFor(Seat *seat);
int m_version = 1; // TODO: remove on libwayland upgrade
QMap<Seat *, DataDevice *> m_dataDevices;
protected:
void data_device_manager_get_data_device(Resource *resource, uint32_t id, ::wl_resource *seatResource) override;
@ -52,24 +58,42 @@ protected:
class DataDevice : public QtWaylandServer::wl_data_device
{
public:
explicit DataDevice(::wl_client *client, int id, int version)
: QtWaylandServer::wl_data_device(client, id, version)
explicit DataDevice(DataDeviceManager *manager, Seat *seat)
: m_manager(manager)
, m_seat(seat)
{}
~DataDevice() override;
void send_data_offer(::wl_resource *resource) = delete;
void sendDataOffer(DataOffer *offer);
DataOffer *sendDataOffer(::wl_client *client, const QStringList &mimeTypes = {});
DataOffer *sendDataOffer(const QStringList &mimeTypes = {});
void send_selection(::wl_resource *resource) = delete;
void sendSelection(DataOffer *offer);
DataDeviceManager *m_manager = nullptr;
Seat *m_seat = nullptr;
QVector<DataOffer *> m_sentSelectionOffers;
protected:
void data_device_release(Resource *resource) override
{
int removed = m_manager->m_dataDevices.remove(m_seat);
QVERIFY(removed);
wl_resource_destroy(resource->handle);
}
};
class DataOffer : public QObject, public QtWaylandServer::wl_data_offer
{
Q_OBJECT
public:
explicit DataOffer(::wl_client *client, int version)
explicit DataOffer(DataDevice *dataDevice, ::wl_client *client, int version)
: QtWaylandServer::wl_data_offer (client, 0, version)
, m_dataDevice(dataDevice)
{}
DataDevice *m_dataDevice = nullptr;
signals:
void receive(QString mimeType, int fd);
@ -77,7 +101,7 @@ protected:
void data_offer_destroy_resource(Resource *resource) override;
void data_offer_receive(Resource *resource, const QString &mime_type, int32_t fd) override;
// void data_offer_accept(Resource *resource, uint32_t serial, const QString &mime_type) override;
// void data_offer_destroy(Resource *resource) override;
void data_offer_destroy(Resource *resource) override;
};
} // namespace MockCompositor