Revision control

Copy as Markdown

// Copyright (C) 2017 Ford Motor Company
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "../shared/model_utilities.h"
#include <QtTest/QtTest>
#include <QAbstractItemModelTester>
#include <QMetaType>
#include <QRemoteObjectReplica>
#include <QRemoteObjectNode>
#include <QAbstractItemModelReplica>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include <QEventLoop>
#include <QRandomGenerator>
#include <QTcpServer>
#include <QHostAddress>
namespace {
QList<QStandardItem*> createInsertionChildren(int num, const QString& name, const QColor &background)
{
QList<QStandardItem*> children;
for (int i = 0; i < num; ++i) {
QStandardItem *item = new QStandardItem(QStringLiteral("%1 %2").arg(name).arg(i+1));
item->setBackground(background);
children.append(item);
}
return children;
}
class RowsWatcher : public WaitHelper
{
public:
RowsWatcher(const QAbstractItemModel *model, int expectedRowsCount)
: WaitHelper(), m_model(model), m_expectedRowsCount(expectedRowsCount)
{
connect(m_model, &QAbstractItemModel::rowsInserted, this,
[this](const QModelIndex &parent, int first, int last) {
const auto columnCount = m_model->columnCount(parent);
for (int row = first; row <= last; ++row) {
for (int column = 0; column < columnCount; ++column)
m_changedData.append(m_model->index(row, column, parent));
}
onNumRowsChanged(parent, first, last);
});
connect(m_model, &QAbstractItemModel::rowsRemoved, this, &RowsWatcher::onNumRowsChanged);
}
void onNumRowsChanged(const QModelIndex &parent, int first, int last)
{
const auto compare = [=](const RowData &row) {
return (row.m_start == first && row.m_end == last
&& compareIndices(row.m_index, parent));
};
QVERIFY(std::find_if(m_pendingRows.begin(), m_pendingRows.end(), compare)
!= m_pendingRows.end());
m_currentRowsCount += last - first + 1;
if (m_currentRowsCount == m_expectedRowsCount)
finish();
}
void scheduleRowsToWatch(const QModelIndex &index, int start, int end)
{
m_pendingRows.push_back(RowData { index, start, end });
}
QList<QModelIndex> changedData() const { return m_changedData; }
private:
struct RowData
{
QModelIndex m_index;
int m_start = -1;
int m_end = -1;
};
const QAbstractItemModel *m_model;
QList<RowData> m_pendingRows;
QList<QModelIndex> m_changedData;
int m_currentRowsCount = 0;
const int m_expectedRowsCount;
};
QTextStream cout(stdout, QIODevice::WriteOnly);
//Keep in case we need detailed debugging in the future...
#if 0
inline QDebug operator <<(QDebug dbg, const InsertedRow &row)
{
return dbg.nospace() << "Index(" << row.m_index << ", " << row.m_start << ", " << row.m_end << ")";
}
inline void dumpModel(const QAbstractItemModel *model, const QModelIndex &parent = QModelIndex())
{
int level = 0;
for (QModelIndex idx = parent; idx.isValid(); idx = model->parent(idx), ++level);
const QHash<int,QByteArray> roles = model->roleNames();
for (int i = 0; i < model->rowCount(parent); ++i) {
for (int j = 0; j < model->columnCount(parent); ++j) {
const QModelIndex index = model->index(i, j);
Q_ASSERT(index.isValid());
QString s;
s.fill(QChar(' '), level*2);
cout << qPrintable(s);
cout << QString(QLatin1String("%1:%2")).arg(i).arg(j);
for (auto it = roles.cbegin(), end = roles.cend(); it != end; ++it) {
const QVariant v = model->data(index, it.key());
if (!v.isValid()) continue;
QString t;
QDebug(&t) << v;
cout << " " << QString::fromUtf8(it.value()) << "=" << t.trimmed();
}
{
QString t;
QDebug(&t) << model->flags(index);
cout << " flags=" << t;
}
cout << "\n";
cout.flush();
dumpModel(model, index); // recursive
}
}
}
#endif
void compareData(const QAbstractItemModel *sourceModel, const QAbstractItemModelReplica *replica)
{
QVERIFY(sourceModel);
QVERIFY(replica);
QCOMPARE(replica->rowCount(), sourceModel->rowCount());
QCOMPARE(replica->columnCount(), sourceModel->columnCount());
QCOMPARE(replica->roleNames(), sourceModel->roleNames());
for (int i = 0; i < sourceModel->rowCount(); ++i) {
for (int j = 0; j < sourceModel->columnCount(); ++j) {
const auto roles = replica->availableRoles();
for (int role : roles) {
QCOMPARE(replica->index(i, j).data(role), sourceModel->index(i, j).data(role));
}
}
}
}
void compareIndex(const QModelIndex &sourceIndex, const QModelIndex &replicaIndex,
const QList<int> &roles)
{
QVERIFY(sourceIndex.isValid());
QVERIFY(replicaIndex.isValid());
for (int role : roles) {
QCOMPARE(replicaIndex.data(role), sourceIndex.data(role));
}
const QAbstractItemModel *sourceModel = sourceIndex.model();
const QAbstractItemModel *replicaModel = replicaIndex.model();
const int sourceRowCount = sourceModel->rowCount(sourceIndex);
const int replicaRowCount = replicaModel->rowCount(replicaIndex);
QCOMPARE(replicaRowCount, sourceRowCount);
const int sourceColumnCount = sourceModel->columnCount(sourceIndex);
const int replicaColumnCount = replicaModel->columnCount(replicaIndex);
// only test the column count if the row count is larger than zero, because we
// assume the column count is constant over a tree model and it doesn't make a
// difference in the view.
if (sourceRowCount)
QCOMPARE(replicaColumnCount, sourceColumnCount);
for (int i = 0; i < sourceRowCount; ++i) {
for (int j = 0; j < sourceColumnCount; ++j) {
auto sourceChild = sourceModel->index(i, j, sourceIndex);
auto replicaChild = replicaModel->index(i, j, replicaIndex);
compareIndex(sourceChild, replicaChild, roles);
}
}
}
void compareTreeData(const QAbstractItemModel *sourceModel, const QAbstractItemModelReplica *replica)
{
QVERIFY(sourceModel);
QVERIFY(replica);
QCOMPARE(replica->rowCount(), sourceModel->rowCount());
QCOMPARE(replica->columnCount(), sourceModel->columnCount());
for (int i = 0; i < sourceModel->rowCount(); ++i) {
for (int j = 0; j < sourceModel->columnCount(); ++j) {
const QModelIndex replicaIndex = replica->index(i, j);
const QModelIndex sourceIndex = sourceModel->index(i, j);
compareIndex(sourceIndex, replicaIndex, replica->availableRoles());
}
}
}
void compareTreeData(const QAbstractItemModel *sourceModel, const QAbstractItemModel *replica, const QList<int> &roles)
{
QVERIFY(sourceModel);
QVERIFY(replica);
QCOMPARE(replica->rowCount(), sourceModel->rowCount());
QCOMPARE(replica->columnCount(), sourceModel->columnCount());
for (int i = 0; i < sourceModel->rowCount(); ++i) {
for (int j = 0; j < sourceModel->columnCount(); ++j) {
const QModelIndex replicaIndex = replica->index(i, j);
const QModelIndex sourceIndex = sourceModel->index(i, j);
compareIndex(sourceIndex, replicaIndex, roles);
}
}
}
void compareFlags(const QAbstractItemModel *sourceModel, const QAbstractItemModelReplica *replica)
{
QVERIFY(sourceModel);
QVERIFY(replica);
QCOMPARE(replica->rowCount(), sourceModel->rowCount());
QCOMPARE(replica->columnCount(), sourceModel->columnCount());
for (int i = 0; i < sourceModel->rowCount(); ++i) {
for (int j = 0; j < sourceModel->columnCount(); ++j) {
if (replica->index(i, j).flags() != sourceModel->index(i, j).flags())
qWarning() << sourceModel->index(i, j).flags() << replica->index(i, j).flags() << i << j;
QCOMPARE(replica->index(i, j).flags(), sourceModel->index(i, j).flags());
}
}
}
// class to test cutom role names
class RolenamesListModel : public QAbstractListModel
{
public:
explicit RolenamesListModel(QObject *parent = nullptr) : QAbstractListModel(parent) { }
int rowCount(const QModelIndex &) const override { return int(m_list.size()); }
QVariant data(const QModelIndex &index, int role) const override
{
if (role == Qt::UserRole)
return m_list.at(index.row()).second;
else if (role == Qt::UserRole+1)
return m_list.at(index.row()).first;
else
return QVariant();
}
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> roles;
roles[Qt::UserRole] = "name";
roles[Qt::UserRole+1] = "pid";
return roles;
}
void addPair(const QVariant pid,const QVariant name) {
m_list.append(qMakePair(pid, name));
}
void clearList() {
m_list.clear();
}
private:
QList<QPair<QVariant, QVariant>> m_list;
};
QList<QStandardItem*> addChild(int numChilds, int nestingLevel)
{
QList<QStandardItem*> result;
if (nestingLevel == 0)
return result;
for (int i = 0; i < numChilds; ++i) {
QStandardItem *child = new QStandardItem(QStringLiteral("Child num %1, nesting Level %2").arg(i+1).arg(nestingLevel));
if (i == 0) {
QList<QStandardItem*> res = addChild(numChilds, nestingLevel -1);
if (res.size() > 0)
child->appendRow(res);
}
result.push_back(child);
}
return result;
}
int getRandomNumber(int min, int max)
{
int res = QRandomGenerator::global()->generate() & INT_MAX;
const int diff = (max - min);
res = res % diff;
res += min;
return res;
}
class FetchData : public WaitHelper
{
public:
FetchData(const QAbstractItemModelReplica *replica) : WaitHelper(), m_replica(replica)
{
if (!m_replica->isInitialized()) {
QEventLoop l;
connect(m_replica, &QAbstractItemModelReplica::initialized, &l, &QEventLoop::quit);
l.exec();
}
connect(m_replica, &QAbstractItemModelReplica::dataChanged, this, &FetchData::dataChanged);
connect(m_replica, &QAbstractItemModelReplica::rowsInserted, this, &FetchData::rowsInserted);
}
bool fetchAndWait(int timeout = 15000)
{
addAll();
fetch();
return wait(timeout);
}
private:
const QAbstractItemModelReplica *m_replica;
QHash<QPersistentModelIndex, QList<int>> m_pending;
QSet<QPersistentModelIndex> m_waitForInsertion;
void addData(const QModelIndex &index, const QList<int> &roles)
{
for (int role : roles) {
const bool cached = m_replica->hasData(index, role);
if (cached)
continue;
if (!m_pending.contains(index))
m_pending[index] = QList<int>() << role;
else {
if (!m_pending[index].contains(role))
m_pending[index].append(role);
}
}
}
void addIndex(const QModelIndex &parent, const QList<int> &roles)
{
if (parent.isValid())
addData(parent, roles);
for (int i = 0; i < m_replica->rowCount(parent); ++i) {
for (int j = 0; j < m_replica->columnCount(parent); ++j) {
const QModelIndex index = m_replica->index(i, j, parent);
Q_ASSERT(index.isValid());
addIndex(index, roles);
}
}
}
void addAll()
{
addIndex(QModelIndex(), m_replica->availableRoles());
}
void fetch()
{
if (m_pending.isEmpty() && m_waitForInsertion.isEmpty()) {
finish();
return;
}
QHash<QPersistentModelIndex, QList<int>> pending(m_pending);
pending.detach();
auto it(pending.constBegin()), end(pending.constEnd());
for (; it != end; ++it) {
for (int role : it.value()) {
QVariant v = m_replica->data(it.key(), role);
Q_UNUSED(v)
}
}
}
void rowsInserted(const QModelIndex &parent, int first, int last)
{
static QList<int> rolesV;
if (rolesV.isEmpty())
rolesV << Qt::DisplayRole << Qt::BackgroundRole;
m_waitForInsertion.remove(parent);
const int columnCount = m_replica->columnCount(parent);
if (!(m_replica->hasChildren(parent) && columnCount > 0 && m_replica->rowCount(parent) > 0))
qWarning() << m_replica->hasChildren(parent) << columnCount << m_replica->rowCount(parent) << parent.data();
QVERIFY(m_replica->hasChildren(parent) && columnCount > 0 && m_replica->rowCount(parent) > 0);
for (int i = first; i <= last; ++i) {
for (int j = 0; j < columnCount; ++j) {
const QModelIndex index = m_replica->index(i, j, parent);
const int childRowCount = m_replica->rowCount(index);
QVERIFY(index.isValid());
if (m_replica->hasChildren(index) && childRowCount == 0) {
if (index.column() == 0)
m_waitForInsertion.insert(index);
}
addIndex(index, rolesV);
}
}
if (m_replica->hasChildren(parent))
fetch();
}
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles = {})
{
Q_ASSERT(topLeft.isValid());
Q_ASSERT(bottomRight.isValid());
Q_ASSERT(topLeft.parent() == bottomRight.parent());
const QModelIndex parent = topLeft.parent();
for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
for (int j = topLeft.column(); j <= bottomRight.column(); ++j) {
const QModelIndex index = m_replica->index(i, j, parent);
Q_ASSERT(index.isValid());
auto it = m_pending.find(index);
if (it == m_pending.end())
continue;
#if 0
QList<int> itroles = it.value();
itroles.detach();
if (roles.isEmpty()) {
itroles.clear();
} else {
for (int r : roles) {
itroles.removeAll(r);
}
}
if (itroles.isEmpty()) {
m_pending.erase(it);
} else {
m_pending[index] = itroles;
}
#else
Q_UNUSED(roles)
m_pending.erase(it);
if (m_replica->hasChildren(index)) {
// ask for the row count to get an update
const int rowCount = m_replica->rowCount(index);
for (int i = 0; i < rowCount; ++i) {
const QModelIndex cIndex = m_replica->index(i, 0, index);
Q_ASSERT(cIndex.isValid());
addIndex(cIndex, m_replica->availableRoles());
}
if (rowCount)
fetch();
else if (index.column() == 0)
m_waitForInsertion.insert(index);
}
#endif
}
}
if (m_pending.isEmpty() && m_waitForInsertion.isEmpty())
finish();
}
};
} // namespace
#define _SETUP_TEST_ \
QRemoteObjectHost basicServer; \
QRemoteObjectNode client; \
QRemoteObjectRegistryHost registryServer; \
QVERIFY(setup_models(basicServer, client, registryServer));
class TestModelView: public QObject
{
Q_OBJECT
QStandardItemModel m_sourceModel;
RolenamesListModel m_listModel;
public:
bool setup_models(QRemoteObjectHost &basicServer,
QRemoteObjectNode &client,
QRemoteObjectRegistryHost &registryServer);
private slots:
// NB: The tests have side effects on the models used, and need to be run
// in order (they may depend on previous side effects).
void initTestCase();
void testEmptyModel();
void testInitialData();
void testInitialDataTree();
void testHeaderData();
void testHeaderDataChange();
void testFlags();
void testDataChanged();
void testDataChangedTree();
void testDataInsertion();
void testDataInsertionTree();
void testSetData();
void testSetDataTree();
void testDataRemoval();
void testDataRemovalTree();
void testServerInsertDataTree();
void testRoleNames();
void testModelTest_data();
void testModelTest();
void testSortFilterModel();
void testSelectionFromReplica();
void testSelectionFromSource();
void testChildSelection();
void testCacheData_data();
void testCacheData();
void cleanup();
};
void TestModelView::initTestCase()
{
QLoggingCategory::setFilterRules("qt.remoteobjects.warning=false");
static const int modelSize = 20;
QHash<int,QByteArray> roleNames;
roleNames[Qt::DisplayRole] = "text";
roleNames[Qt::BackgroundRole] = "background";
m_sourceModel.setItemRoleNames(roleNames);
QStringList list;
list.reserve(modelSize);
QStringList hHeaderList;
hHeaderList << QStringLiteral("First Column with spacing") << QStringLiteral("Second Column with spacing");
m_sourceModel.setHorizontalHeaderLabels(hHeaderList);
for (int i = 0; i < modelSize; ++i) {
QStandardItem *firstItem = new QStandardItem(QStringLiteral("FancyTextNumber %1").arg(i));
QStandardItem *secondItem = new QStandardItem(QStringLiteral("FancyRow2TextNumber %1").arg(i));
if (i % 2 == 0)
firstItem->setBackground(Qt::red);
firstItem->appendRow(addChild(2,getRandomNumber(1, 4)));
QList<QStandardItem*> row;
row << firstItem << secondItem;
m_sourceModel.appendRow(row);
list << QStringLiteral("FancyTextNumber %1").arg(i);
}
const int numElements = 1000;
for (int i = 0; i < numElements; ++i) {
QString name = QString("Data %1").arg(i);
QString pid = QString("%1").arg(i);
m_listModel.addPair(name, pid);
}
}
static bool portPairIsFree(int port)
{
QTcpServer probe1;
QTcpServer probe2;
return probe1.listen(QHostAddress::LocalHost, port)
&& probe2.listen(QHostAddress::LocalHost, port + 1);
}
bool TestModelView::setup_models(QRemoteObjectHost &basicServer, QRemoteObjectNode &client, QRemoteObjectRegistryHost &registryServer)
{
static int port = 65211;
static const QString url = QStringLiteral("tcp://127.0.0.1:%1");
// QStandardItem::flags are stored as data with Qt::UserRole - 1
static const QList<int> sourceModelRoles( {Qt::DisplayRole, Qt::BackgroundRole, (Qt::UserRole - 1)} );
static const QList<int> listModelRoles( {Qt::UserRole, Qt::UserRole+1} );
const int maxAttempts = 10;
int attempt = 0;
while (!portPairIsFree(port) && ++attempt < maxAttempts)
port += 2;
// setup_models() returns bool, so QVERIFY/QVERIFY2 ("return;" with no
// value) can't be used here; call QTest::qVerify() directly instead.
if (!QTest::qVerify(attempt < maxAttempts, "attempt < maxAttempts",
"setup_models: unable to find a free port pair after several attempts",
__FILE__, __LINE__))
return false;
//Setup registry
//Registry needs to be created first until we get the retry mechanism implemented
if (!QTest::qVerify(basicServer.setHostUrl(QUrl(url.arg(port))),
"basicServer.setHostUrl(QUrl(url.arg(port)))", "", __FILE__, __LINE__))
return false;
if (!QTest::qVerify(registryServer.setRegistryUrl(QUrl(url.arg(port+1))),
"registryServer.setRegistryUrl(QUrl(url.arg(port+1)))", "", __FILE__, __LINE__))
return false;
if (!QTest::qVerify(basicServer.setRegistryUrl(QUrl(url.arg(port+1))),
"basicServer.setRegistryUrl(QUrl(url.arg(port+1)))", "", __FILE__, __LINE__))
return false;
basicServer.enableRemoting(&m_sourceModel, "test", sourceModelRoles);
basicServer.enableRemoting(&m_listModel, "testRoleNames", listModelRoles);
client.setRegistryUrl(QUrl(url.arg(port+1)));
port += 2;
return true;
}
#ifdef SLOW_MODELTEST
#define MODELTEST_WAIT_TIME 25000
#else
#define MODELTEST_WAIT_TIME
#endif
void TestModelView::testEmptyModel()
{
_SETUP_TEST_
QList<int> roles = QList<int>() << Qt::DisplayRole << Qt::BackgroundRole;
QStandardItemModel emptyModel;
basicServer.enableRemoting(&emptyModel, "emptyModel", roles);
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("emptyModel"));
model->setRootCacheSize(1000);
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
compareData(&emptyModel, model.data());
}
void TestModelView::testInitialData()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
compareData(&m_sourceModel, model.data());
}
void TestModelView::testInitialDataTree()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
compareTreeData(&m_sourceModel, model.data());
}
void TestModelView::testHeaderData()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
// ask for all Data members first, so we don't have to wait for update signals
QSignalSpy spyHeader(model.data(), &QAbstractItemModelReplica::headerDataChanged);
for (int i = 0; i < m_sourceModel.rowCount(); ++i)
model->headerData(i, Qt::Vertical, Qt::DisplayRole);
for (int i = 0; i < m_sourceModel.columnCount(); ++i)
model->headerData(i, Qt::Horizontal, Qt::DisplayRole);
spyHeader.wait();
for (int i = 0; i < m_sourceModel.rowCount(); ++i)
QCOMPARE(model->headerData(i, Qt::Vertical, Qt::DisplayRole), m_sourceModel.headerData(i, Qt::Vertical, Qt::DisplayRole));
for (int i = 0; i < m_sourceModel.columnCount(); ++i)
QCOMPARE(model->headerData(i, Qt::Horizontal, Qt::DisplayRole), m_sourceModel.headerData(i, Qt::Horizontal, Qt::DisplayRole));
}
void TestModelView::testHeaderDataChange()
{
_SETUP_TEST_
QString newHeader = QStringLiteral("New header name");
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
QVERIFY(model->headerData(0, Qt::Horizontal, Qt::DisplayRole).toString() != newHeader);
QSignalSpy spyHeader(model.data(), &QAbstractItemModelReplica::headerDataChanged);
m_sourceModel.setHeaderData(0, Qt::Horizontal, newHeader, Qt::DisplayRole);
spyHeader.wait();
QTRY_COMPARE(model->headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(), newHeader);
spyHeader.clear();
m_sourceModel.setHeaderData(1, Qt::Horizontal, newHeader, Qt::DisplayRole);
spyHeader.wait();
QTRY_COMPARE(model->headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), newHeader);
QString anotherHeader = QStringLiteral("Modified header name");
m_sourceModel.setHeaderData(0, Qt::Horizontal, anotherHeader, Qt::DisplayRole);
spyHeader.wait();
QTRY_COMPARE(model->headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(), anotherHeader);
QCOMPARE(model->headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), newHeader);
}
void TestModelView::testDataChangedTree()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
compareTreeData(&m_sourceModel, model.data());
QSignalSpy dataChangedSpy(model.data(), &QAbstractItemModelReplica::dataChanged);
QSet<int> expected;
for (int i = 10; i < 20; ++i) {
const QModelIndex parent = m_sourceModel.index(i,0);
const int rowCount = m_sourceModel.rowCount(parent);
const int colCount = m_sourceModel.columnCount(parent);
for (int row = 0; row < rowCount; ++row) {
for (int col = 0; col < colCount; ++col) {
if (col % 2 == 0)
m_sourceModel.setData(m_sourceModel.index(row, col, parent), QColor(Qt::gray), Qt::BackgroundRole);
else
m_sourceModel.setData(m_sourceModel.index(row, col, parent), QColor(Qt::cyan), Qt::BackgroundRole);
}
}
m_sourceModel.setData(m_sourceModel.index(i, 1), QColor(Qt::magenta), Qt::BackgroundRole);
expected << i;
}
bool signalsReceived = false;
int runs = 0;
const int maxRuns = 10;
while (runs < maxRuns) {
if (dataChangedSpy.wait() &&!dataChangedSpy.isEmpty()) {
signalsReceived = true;
for (auto args : dataChangedSpy) {
int row = args.at(1).value<QModelIndex>().row();
if (row && expected.contains(row))
expected.remove(row);
}
if (expected.size() == 0)
break;
}
++runs;
}
QVERIFY(signalsReceived);
compareTreeData(&m_sourceModel, model.data());
}
void TestModelView::testFlags()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
QSignalSpy dataChangedSpy(model.data(), &QAbstractItemModelReplica::dataChanged);
for (int i = 10; i < 20; ++i) {
QStandardItem* firstItem = m_sourceModel.item(i, 0);
QStandardItem* secondItem = m_sourceModel.item(i, 1);
firstItem->setFlags(firstItem->flags() | Qt::ItemIsEnabled | Qt::ItemIsAutoTristate);
secondItem->setFlags(firstItem->flags() | Qt::ItemIsEnabled);
}
bool signalsReceived = false;
while (dataChangedSpy.wait()) {
signalsReceived = true;
if (dataChangedSpy.takeLast().at(1).value<QModelIndex>().row() == 19)
break;
}
QVERIFY(signalsReceived);
compareFlags(&m_sourceModel, model.data());
}
void TestModelView::testDataChanged()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
QSignalSpy dataChangedSpy(model.data(), &QAbstractItemModelReplica::dataChanged);
for (int i = 10; i < 20; ++i)
m_sourceModel.setData(m_sourceModel.index(i, 1), QColor(Qt::blue), Qt::BackgroundRole);
bool signalsReceived = false;
while (dataChangedSpy.wait()) {
signalsReceived = true;
if (dataChangedSpy.takeLast().at(1).value<QModelIndex>().row() == 19)
break;
}
QVERIFY(signalsReceived);
compareData(&m_sourceModel, model.data());
}
void TestModelView::testDataInsertion()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
const int insertedRowsCount = 9;
RowsWatcher watcher(model.data(), insertedRowsCount);
QList<QModelIndex> pending;
m_sourceModel.insertRows(2, insertedRowsCount);
watcher.scheduleRowsToWatch(QModelIndex(), 2, 2 + insertedRowsCount - 1);
QVERIFY(watcher.wait());
QCOMPARE(m_sourceModel.rowCount(), model->rowCount());
pending.append(watcher.changedData());
// change one row to check for inconsistencies
m_sourceModel.setData(m_sourceModel.index(0, 1), QColor(Qt::green), Qt::BackgroundRole);
m_sourceModel.setData(m_sourceModel.index(0, 1), QLatin1String("foo"), Qt::DisplayRole);
pending.append(model->index(0, 1));
WaitForDataChanged w(model.data(), pending);
QVERIFY(w.wait());
compareData(&m_sourceModel, model.data());
}
void TestModelView::testDataInsertionTree()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
const int insertedRowsCount = 9;
const int insertedChildRowsCount = 4;
RowsWatcher watcher(model.data(), insertedRowsCount + insertedChildRowsCount);
QList<QModelIndex> pending;
for (int i = 0; i < insertedRowsCount; ++i) {
watcher.scheduleRowsToWatch(QModelIndex(), 2 + i, 2 + i);
m_sourceModel.insertRow(2 + i, createInsertionChildren(2, QStringLiteral("insertedintree"), Qt::darkRed));
const QModelIndex childIndex = m_sourceModel.index(2 + i, 0);
const QModelIndex childIndex2 = m_sourceModel.index(2 + i, 1);
pending.append(childIndex);
pending.append(childIndex2);
}
const QModelIndex parent = m_sourceModel.index(10, 0);
QStandardItem* parentItem = m_sourceModel.item(10, 0);
for (int i = 0; i < insertedChildRowsCount; ++i) {
watcher.scheduleRowsToWatch(parent, i, i);
parentItem->insertRow(i, createInsertionChildren(2, QStringLiteral("insertedintreedeep"), Qt::darkCyan));
const QModelIndex childIndex = m_sourceModel.index(0, 0, parent);
const QModelIndex childIndex2 = m_sourceModel.index(0, 1, parent);
Q_ASSERT(childIndex.isValid());
Q_ASSERT(childIndex2.isValid());
pending.append(childIndex);
pending.append(childIndex2);
}
QVERIFY(watcher.wait());
QCOMPARE(m_sourceModel.rowCount(), model->rowCount());
pending.append(watcher.changedData());
// change one row to check for inconsistencies
pending << m_sourceModel.index(0, 0, parent);
WaitForDataChanged w(model.data(), pending);
m_sourceModel.setData(m_sourceModel.index(0, 0, parent), QColor(Qt::green), Qt::BackgroundRole);
m_sourceModel.setData(m_sourceModel.index(0, 0, parent), QLatin1String("foo"), Qt::DisplayRole);
QVERIFY(w.wait());
compareTreeData(&m_sourceModel, model.data());
}
void TestModelView::testDataRemoval()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
qputenv("QTRO_NODES_CACHE_SIZE", "1000");
model->setRootCacheSize(1000);
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
const QPersistentModelIndex parent = m_sourceModel.index(10, 0);
{
const int removedRowsCount = 3;
RowsWatcher watcher(model.data(), removedRowsCount);
m_sourceModel.removeRows(0, removedRowsCount, parent);
watcher.scheduleRowsToWatch(parent, 0, removedRowsCount - 1);
QVERIFY(watcher.wait());
QCOMPARE(m_sourceModel.rowCount(parent), model->rowCount(model->index(10, 0)));
}
{
const int removedRowsCount = 8;
RowsWatcher watcher(model.data(), removedRowsCount);
m_sourceModel.removeRows(2, removedRowsCount);
watcher.scheduleRowsToWatch(QModelIndex(), 2, 2 + removedRowsCount - 1);
QVERIFY(watcher.wait());
QCOMPARE(m_sourceModel.rowCount(), model->rowCount());
}
// change one row to check for inconsistencies
QList<QModelIndex> pending;
pending << m_sourceModel.index(0, 0, parent);
WaitForDataChanged w(model.data(), pending);
m_sourceModel.setData(m_sourceModel.index(0, 0, parent), QColor(Qt::green), Qt::BackgroundRole);
m_sourceModel.setData(m_sourceModel.index(0, 0, parent), QLatin1String("foo"), Qt::DisplayRole);
QVERIFY(w.wait());
compareTreeData(&m_sourceModel, model.data());
}
void TestModelView::testRoleNames()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> repModel( client.acquireModel(QStringLiteral("testRoleNames")));
// Set a bigger cache enough to keep all the data otherwise the last test will fail
repModel->setRootCacheSize(1500);
FetchData f(repModel.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
// test custom role names
QCOMPARE(repModel.data()->roleNames(), m_listModel.roleNames());
// test data associated with custom roles
compareData(&m_listModel,repModel.data());
}
void TestModelView::testDataRemovalTree()
{
_SETUP_TEST_
m_sourceModel.removeRows(2, 4);
//Maybe some checks here?
}
void TestModelView::testServerInsertDataTree()
{
_SETUP_TEST_
QList<int> roles = QList<int> { Qt::DisplayRole, Qt::BackgroundRole };
QStandardItemModel testTreeModel;
basicServer.enableRemoting(&testTreeModel, "testTreeModel", roles);
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("testTreeModel"));
QTRY_COMPARE(testTreeModel.rowCount(), model->rowCount());
QVERIFY(testTreeModel.insertRow(0));
QVERIFY(testTreeModel.insertColumn(0));
auto root = testTreeModel.index(0, 0);
QVERIFY(testTreeModel.setData(root, QLatin1String("Root"), Qt::DisplayRole));
QVERIFY(testTreeModel.setData(root, QColor(Qt::green), Qt::BackgroundRole));
QVERIFY(testTreeModel.insertRow(0, root));
QVERIFY(testTreeModel.insertColumn(0, root));
auto child1 = testTreeModel.index(0, 0, root);
QVERIFY(testTreeModel.setData(child1, QLatin1String("Child1"), Qt::DisplayRole));
QVERIFY(testTreeModel.setData(child1, QColor(Qt::red), Qt::BackgroundRole));
QTRY_COMPARE(testTreeModel.rowCount(), model->rowCount());
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
compareData(&testTreeModel, model.data());
}
void TestModelView::testModelTest_data()
{
// QtRemoteObjects::InitialAction is a namespace enum. QTest/QFETCH fail to compile with it as
// the column type, so use bool instead.
QTest::addColumn<bool>("prefetch");
QTest::newRow("size only") << false;
QTest::newRow("prefetch") << true;
}
void TestModelView::testModelTest()
{
QFETCH(bool, prefetch);
_SETUP_TEST_
QtRemoteObjects::InitialAction action = QtRemoteObjects::InitialAction::FetchRootSize;
if (prefetch)
action = QtRemoteObjects::InitialAction::PrefetchData;
QScopedPointer<QAbstractItemModelReplica> repModel( client.acquireModel(QStringLiteral("test"), action));
QAbstractItemModelTester test(repModel.data(), QAbstractItemModelTester::FailureReportingMode::Fatal);
FetchData f(repModel.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
Q_UNUSED(test)
}
void TestModelView::testSortFilterModel()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> repModel( client.acquireModel(QStringLiteral("test")));
FetchData f(repModel.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
QSortFilterProxyModel clientSort;
clientSort.setSourceModel(repModel.data());
clientSort.setSortRole(Qt::DisplayRole);
QSortFilterProxyModel sourceSort;
sourceSort.setSourceModel(&m_sourceModel);
sourceSort.setSortRole(Qt::DisplayRole);
compareTreeData(&sourceSort, &clientSort, repModel->availableRoles());
}
void TestModelView::testSetData()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
compareTreeData(&m_sourceModel, model.data(), model->availableRoles());
//fetched and verified initial state, now setData on the client
QList<QModelIndex> pending;
QList<QModelIndex> pendingReplica;
for (int row = 0, numRows = model->rowCount(); row < numRows; ++row) {
for (int column = 0, numColumns = model->columnCount(); column != numColumns; ++column) {
const QModelIndex index = model->index(row, column);
const QString newData = QStringLiteral("This entry was changed with setData");
QVERIFY(model->setData(index, newData, Qt::DisplayRole));
pending.append(m_sourceModel.index(row, column));
pendingReplica.append(model->index(row, column));
}
}
WaitForDataChanged waiter(&m_sourceModel, pending);
QVERIFY(waiter.wait());
WaitForDataChanged waiterReplica(model.data(), pendingReplica);
QVERIFY(waiterReplica.wait());
compareData(&m_sourceModel, model.data());
}
void TestModelView::testSetDataTree()
{
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("test"));
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
compareTreeData(&m_sourceModel, model.data(), model->availableRoles());
//fetched and verified initial state, now setData on the client
QList<QModelIndex> pending;
QList<QModelIndex> pendingReplica;
QList<QModelIndex> stack;
stack.push_back(QModelIndex());
QList<QModelIndex> sourceStack;
sourceStack.push_back(QModelIndex());
const QString newData = QStringLiteral("This entry was changed with setData in a tree %1 %2 %3");
while (!stack.isEmpty()) {
const QModelIndex parent = stack.takeLast();
const QModelIndex parentSource = sourceStack.takeLast();
for (int row = 0; row < model->rowCount(parent); ++row) {
for (int column = 0; column < model->columnCount(parent); ++column) {
const QModelIndex index = model->index(row, column, parent);
const QModelIndex indexSource = m_sourceModel.index(row, column, parentSource);
QVERIFY(model->setData(index, newData.arg(parent.isValid()).arg(row).arg(column), Qt::DisplayRole));
pending.append(indexSource);
pendingReplica.append(index);
if (column == 0) {
stack.push_back(index);
sourceStack.push_back(indexSource);
}
}
}
}
WaitForDataChanged waiter(&m_sourceModel, pending);
QVERIFY(waiter.wait());
WaitForDataChanged waiterReplica(model.data(), pendingReplica);
QVERIFY(waiterReplica.wait());
compareData(&m_sourceModel, model.data());
}
void TestModelView::testSelectionFromReplica()
{
_SETUP_TEST_
QList<int> roles = QList<int> { Qt::DisplayRole, Qt::BackgroundRole };
QStandardItemModel simpleModel;
for (int i = 0; i < 4; ++i)
simpleModel.appendRow(new QStandardItem(QString("item %0").arg(i)));
QItemSelectionModel selectionModel(&simpleModel);
basicServer.enableRemoting(&simpleModel, "simpleModelFromReplica", roles, &selectionModel);
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("simpleModelFromReplica"));
QItemSelectionModel *replicaSelectionModel = model->selectionModel();
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
replicaSelectionModel->setCurrentIndex(model->index(1,0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
QTRY_COMPARE(selectionModel.currentIndex().row(), 1);
}
void TestModelView::testSelectionFromSource()
{
_SETUP_TEST_
QList<int> roles = QList<int> { Qt::DisplayRole, Qt::BackgroundRole };
QStandardItemModel simpleModel;
for (int i = 0; i < 4; ++i)
simpleModel.appendRow(new QStandardItem(QString("item %0").arg(i)));
QItemSelectionModel selectionModel(&simpleModel);
basicServer.enableRemoting(&simpleModel, "simpleModelFromSource", roles, &selectionModel);
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("simpleModelFromSource"));
QItemSelectionModel *replicaSelectionModel = model->selectionModel();
FetchData f(model.data());
QVERIFY(f.fetchAndWait(MODELTEST_WAIT_TIME));
selectionModel.setCurrentIndex(simpleModel.index(1,0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
QTRY_COMPARE(replicaSelectionModel->currentIndex().row(), 1);
}
void TestModelView::testCacheData_data()
{
QTest::addColumn<QList<int>>("roles");
QTest::newRow("empty") << QList<int> {};
QTest::newRow("all") << QList<int> { Qt::UserRole, Qt::UserRole + 1 };
}
void TestModelView::testCacheData()
{
QFETCH(QList<int>, roles);
_SETUP_TEST_
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("testRoleNames", QtRemoteObjects::PrefetchData, roles));
model->setRootCacheSize(1000);
QEventLoop l;
connect(model.data(), &QAbstractItemModelReplica::initialized, &l, &QEventLoop::quit);
l.exec();
compareData(&m_listModel, model.data());
}
void TestModelView::testChildSelection()
{
_SETUP_TEST_
QList<int> roles = { Qt::DisplayRole, Qt::BackgroundRole };
QStandardItemModel simpleModel;
QStandardItem *parentItem = simpleModel.invisibleRootItem();
for (int i = 0; i < 4; ++i) {
QStandardItem *item = new QStandardItem(QString("item %0").arg(i));
parentItem->appendRow(item);
parentItem = item;
}
QItemSelectionModel selectionModel(&simpleModel);
basicServer.enableRemoting(&simpleModel, "treeModelFromSource", roles, &selectionModel);
QScopedPointer<QAbstractItemModelReplica> model(client.acquireModel("treeModelFromSource", QtRemoteObjects::PrefetchData, roles));
QItemSelectionModel *replicaSelectionModel = model->selectionModel();
QTRY_COMPARE(simpleModel.rowCount(), model->rowCount());
QTRY_COMPARE(model->data(model->index(0, 0)), QVariant(QString("item 0")));
// select an item not yet "seen" by the replica
selectionModel.setCurrentIndex(simpleModel.index(0, 0, simpleModel.index(0,0)), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
QTRY_COMPARE(replicaSelectionModel->currentIndex().row(), 0);
QVERIFY(replicaSelectionModel->currentIndex().parent().isValid());
}
void TestModelView::cleanup()
{
// wait for delivery of RemoveObject events to the source
QTest::qWait(20);
}
QTEST_MAIN(TestModelView)
#include "tst_modelview.moc"