From b3e4be2d8b9debf217657436139da0152f6f8797 Mon Sep 17 00:00:00 2001 From: Giuseppe D'Angelo Date: Thu, 24 Aug 2017 17:54:04 +0200 Subject: Long live QAbstractItemModelTester! AKA the model tester, living in QtTestLib now. Underwent some significant refactoring from the original modeltester: in particular, it will stop testing illegal indices. [ChangeLog][QtTestLib] Added QAbstractItemModelTester, a class to help testing item models. Change-Id: I0e5efed7217330be11465ce3abb3590f3f2601a4 Reviewed-by: David Faure --- src/testlib/doc/src/qttest-index.qdoc | 4 +- src/testlib/qabstractitemmodeltester.cpp | 807 +++++++++++++++++++++++++++++++ src/testlib/qabstractitemmodeltester.h | 131 +++++ src/testlib/testlib.pro | 8 +- 4 files changed, 947 insertions(+), 3 deletions(-) create mode 100644 src/testlib/qabstractitemmodeltester.cpp create mode 100644 src/testlib/qabstractitemmodeltester.h (limited to 'src/testlib') diff --git a/src/testlib/doc/src/qttest-index.qdoc b/src/testlib/doc/src/qttest-index.qdoc index 36ebfee463..7b3e96f72e 100644 --- a/src/testlib/doc/src/qttest-index.qdoc +++ b/src/testlib/doc/src/qttest-index.qdoc @@ -31,7 +31,9 @@ Qt Test provides classes for unit testing Qt applications and libraries. All public methods are in the \l QTest namespace. In addition, the - \l QSignalSpy class provides easy introspection for Qt's signals and slots. + \l QSignalSpy class provides easy introspection for Qt's signals and slots, + and the \l QAbstractItemModelTester allows for non-destructive testing + of item models. \section1 Getting Started diff --git a/src/testlib/qabstractitemmodeltester.cpp b/src/testlib/qabstractitemmodeltester.cpp new file mode 100644 index 0000000000..ef985007b1 --- /dev/null +++ b/src/testlib/qabstractitemmodeltester.cpp @@ -0,0 +1,807 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Giuseppe D'Angelo +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qabstractitemmodeltester.h" + +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcModelTest, "qt.modeltest") + +#define MODELTESTER_VERIFY(statement) \ +do { \ + if (!verify(static_cast(statement), #statement, "", __FILE__, __LINE__)) \ + return; \ +} while (false) + +#define MODELTESTER_COMPARE(actual, expected) \ +do { \ + if (!compare((actual), (expected), #actual, #expected, __FILE__, __LINE__)) \ + return; \ +} while (false) + +class QAbstractItemModelTesterPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QAbstractItemModelTester) +public: + QAbstractItemModelTesterPrivate(QAbstractItemModel *model, QAbstractItemModelTester::FailureReportingMode failureReportingMode); + + void nonDestructiveBasicTest(); + void rowAndColumnCount(); + void hasIndex(); + void index(); + void parent(); + void data(); + + void runAllTests(); + void layoutAboutToBeChanged(); + void layoutChanged(); + void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end); + void rowsInserted(const QModelIndex &parent, int start, int end); + void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + void rowsRemoved(const QModelIndex &parent, int start, int end); + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void headerDataChanged(Qt::Orientation orientation, int start, int end); + +private: + void checkChildren(const QModelIndex &parent, int currentDepth = 0); + + bool verify(bool statement, const char *statementStr, const char *description, const char *file, int line); + + template + bool compare(const T1 &t1, const T2 &t2, + const char *actual, const char *expected, + const char *file, int line); + + QPointer model; + QAbstractItemModelTester::FailureReportingMode failureReportingMode; + + struct Changing { + QModelIndex parent; + int oldSize; + QVariant last; + QVariant next; + }; + QStack insert; + QStack remove; + + bool fetchingMore; + + QList changing; +}; + +/*! + \class QAbstractItemModelTester + \since 5.11 + \inmodule QtTest + + \brief The QAbstractItemModelTester class helps testing QAbstractItemModel subclasses. + + The QAbstractItemModelTester class is a utility class to test item models. + + When implementing an item model (that is, a concrete QAbstractItemModel + subclass) one must abide to a very strict set of rules that ensure + consistency for users of the model (views, proxy models, and so on). + + For instance, for a given index, a model's reimplementation of + \l{QAbstractItemModel::hasChildren()}{hasChildren()} must be consistent + with the values returned by \l{QAbstractItemModel::rowCount()}{rowCount()} + and \l{QAbstractItemModel::columnCount()}{columnCount()}. + + QAbstractItemModelTester helps catching the most common errors in custom + item model classes. By performing a series of tests, it + will try to check that the model status is consistent at all times. The + tests will be repeated automatically every time the model is modified. + + QAbstractItemModelTester employs non-destructive tests, which typically + consist in reading data and metadata out of a given item model. + QAbstractItemModelTester will also attempt illegal modifications of + the model. In models which are properly implemented, such attempts + should be rejected, and no data should be changed as a consequence. + + \section1 Usage + + Using QAbstractItemModelTester is straightforward. In a \l{Qt Test Overview}{test case} + it is sufficient to create an instance, passing the model that + needs to be tested to the constructor: + + \code + MyModel *modelToBeTested = ...; + auto tester = new QAbstractItemModelTester(modelToBeTested); + \endcode + + QAbstractItemModelTester will report testing failures through the + Qt Test logging mechanisms. + + It is also possible to use QAbstractItemModelTester outside of a test case. + For instance, it may be useful to test an item model used by an application + without the need of building an explicit unit test for such a model (which + might be challenging). In order to use QAbstractItemModelTester outside of + a test case, pass one of the \c QAbstractItemModelTester::FailureReportingMode + enumerators to its constructor, therefore specifying how failures should + be logged. + + QAbstractItemModelTester may also report additional debugging information + as logging messages under the \c qt.modeltest logging category. Such + debug logging is disabled by default; refer to the + QLoggingCategory documentation to learn how to enable it. + + \note While QAbstractItemModelTester is a valid help for development and + testing of custom item models, it does not (and cannot) catch all possible + problems in QAbstractItemModel subclasses. Notably, it will never perform + meaningful destructive testing of a model, which must be therefore tested + separately. + + \sa {Model/View Programming}, QAbstractItemModel +*/ + +/*! + \enum QAbstractItemModelTester::FailureReportingMode + + This enumeration specifies how QAbstractItemModelTester should report + a failure when it tests a QAbstractItemModel subclass. + + \value FailureReportingMode::QtTest The failures will be reported through + QtTest's logging mechanism. + + \value FailureReportingMode::Warning The failures will be reported as + warning messages in the \c{qt.modeltest} logging category. + + \value FailureReportingMode::Fatal A failure will cause immediate and + abnormal program termination. The reason for the failure will be reported + using \c{qFatal()}. +*/ + +/*! + Creates a model tester instance, with the given \a parent, that will test + the model \a model. +*/ +QAbstractItemModelTester::QAbstractItemModelTester(QAbstractItemModel *model, QObject *parent) + : QAbstractItemModelTester(model, FailureReportingMode::QtTest, parent) +{ +} + +/*! + Creates a model tester instance, with the given \a parent, that will test + the model \a model, using the specified \a mode to report test failures. + + \sa QAbstractItemModelTester::FailureReportingMode +*/ +QAbstractItemModelTester::QAbstractItemModelTester(QAbstractItemModel *model, FailureReportingMode mode, QObject *parent) + : QObject(*new QAbstractItemModelTesterPrivate(model, mode), parent) +{ + if (!model) + qFatal("%s: model must not be null", Q_FUNC_INFO); + + Q_D(QAbstractItemModelTester); + + const auto &runAllTests = [d] { d->runAllTests(); }; + + connect(model, &QAbstractItemModel::columnsAboutToBeInserted, + this, runAllTests); + connect(model, &QAbstractItemModel::columnsAboutToBeRemoved, + this, runAllTests); + connect(model, &QAbstractItemModel::columnsInserted, + this, runAllTests); + connect(model, &QAbstractItemModel::columnsRemoved, + this, runAllTests); + connect(model, &QAbstractItemModel::dataChanged, + this, runAllTests); + connect(model, &QAbstractItemModel::headerDataChanged, + this, runAllTests); + connect(model, &QAbstractItemModel::layoutAboutToBeChanged, + this, runAllTests); + connect(model, &QAbstractItemModel::layoutChanged, + this, runAllTests); + connect(model, &QAbstractItemModel::modelReset, + this, runAllTests); + connect(model, &QAbstractItemModel::rowsAboutToBeInserted, + this, runAllTests); + connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, + this, runAllTests); + connect(model, &QAbstractItemModel::rowsInserted, + this, runAllTests); + connect(model, &QAbstractItemModel::rowsRemoved, + this, runAllTests); + + // Special checks for changes + connect(model, &QAbstractItemModel::layoutAboutToBeChanged, + this, [d]{ d->layoutAboutToBeChanged(); }); + connect(model, &QAbstractItemModel::layoutChanged, + this, [d]{ d->layoutChanged(); }); + + connect(model, &QAbstractItemModel::rowsAboutToBeInserted, + this, [d](const QModelIndex &parent, int start, int end) { d->rowsAboutToBeInserted(parent, start, end); }); + connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, + this, [d](const QModelIndex &parent, int start, int end) { d->rowsAboutToBeRemoved(parent, start, end); }); + connect(model, &QAbstractItemModel::rowsInserted, + this, [d](const QModelIndex &parent, int start, int end) { d->rowsInserted(parent, start, end); }); + connect(model, &QAbstractItemModel::rowsRemoved, + this, [d](const QModelIndex &parent, int start, int end) { d->rowsRemoved(parent, start, end); }); + connect(model, &QAbstractItemModel::dataChanged, + this, [d](const QModelIndex &topLeft, const QModelIndex &bottomRight) { d->dataChanged(topLeft, bottomRight); }); + connect(model, &QAbstractItemModel::headerDataChanged, + this, [d](Qt::Orientation orientation, int start, int end) { d->headerDataChanged(orientation, start, end); }); + + runAllTests(); +} + +/*! + Returns the model that this instance is testing. +*/ +QAbstractItemModel *QAbstractItemModelTester::model() const +{ + Q_D(const QAbstractItemModelTester); + return d->model.data(); +} + +/*! + Returns the mode that this instancing is using to report test failures. + + \sa QAbstractItemModelTester::FailureReportingMode +*/ +QAbstractItemModelTester::FailureReportingMode QAbstractItemModelTester::failureReportingMode() const +{ + Q_D(const QAbstractItemModelTester); + return d->failureReportingMode; +} + +QAbstractItemModelTesterPrivate::QAbstractItemModelTesterPrivate(QAbstractItemModel *model, QAbstractItemModelTester::FailureReportingMode failureReportingMode) + : model(model), + failureReportingMode(failureReportingMode), + fetchingMore(false) +{ +} + +void QAbstractItemModelTesterPrivate::runAllTests() +{ + if (fetchingMore) + return; + nonDestructiveBasicTest(); + rowAndColumnCount(); + hasIndex(); + index(); + parent(); + data(); +} + +/*! + nonDestructiveBasicTest tries to call a number of the basic functions (not all) + to make sure the model doesn't outright segfault, testing the functions that makes sense. +*/ +void QAbstractItemModelTesterPrivate::nonDestructiveBasicTest() +{ + MODELTESTER_VERIFY(!model->buddy(QModelIndex()).isValid()); + model->canFetchMore(QModelIndex()); + MODELTESTER_VERIFY(model->columnCount(QModelIndex()) >= 0); + fetchingMore = true; + model->fetchMore(QModelIndex()); + fetchingMore = false; + Qt::ItemFlags flags = model->flags(QModelIndex()); + MODELTESTER_VERIFY(flags == Qt::ItemIsDropEnabled || flags == 0); + model->hasChildren(QModelIndex()); + model->hasIndex(0, 0); + QVariant cache; + model->match(QModelIndex(), -1, cache); + model->mimeTypes(); + MODELTESTER_VERIFY(!model->parent(QModelIndex()).isValid()); + MODELTESTER_VERIFY(model->rowCount() >= 0); + model->span(QModelIndex()); + model->supportedDropActions(); + model->roleNames(); +} + +/*! + Tests model's implementation of QAbstractItemModel::rowCount(), + columnCount() and hasChildren(). + + Models that are dynamically populated are not as fully tested here. + */ +void QAbstractItemModelTesterPrivate::rowAndColumnCount() +{ + if (!model->hasChildren()) + return; + + QModelIndex topIndex = model->index(0, 0, QModelIndex()); + + // check top row + int rows = model->rowCount(topIndex); + MODELTESTER_VERIFY(rows >= 0); + + int columns = model->columnCount(topIndex); + MODELTESTER_VERIFY(columns >= 0); + + if (rows == 0 || columns == 0) + return; + + MODELTESTER_VERIFY(model->hasChildren(topIndex)); + + QModelIndex secondLevelIndex = model->index(0, 0, topIndex); + MODELTESTER_VERIFY(secondLevelIndex.isValid()); + + rows = model->rowCount(secondLevelIndex); + MODELTESTER_VERIFY(rows >= 0); + + columns = model->columnCount(secondLevelIndex); + MODELTESTER_VERIFY(columns >= 0); + + if (rows == 0 || columns == 0) + return; + + MODELTESTER_VERIFY(model->hasChildren(secondLevelIndex)); + + // rowCount() / columnCount() are tested more extensively in checkChildren() +} + +/*! + Tests model's implementation of QAbstractItemModel::hasIndex() + */ +void QAbstractItemModelTesterPrivate::hasIndex() +{ + // Make sure that invalid values returns an invalid index + MODELTESTER_VERIFY(!model->hasIndex(-2, -2)); + MODELTESTER_VERIFY(!model->hasIndex(-2, 0)); + MODELTESTER_VERIFY(!model->hasIndex(0, -2)); + + const int rows = model->rowCount(); + const int columns = model->columnCount(); + + // check out of bounds + MODELTESTER_VERIFY(!model->hasIndex(rows, columns)); + MODELTESTER_VERIFY(!model->hasIndex(rows + 1, columns + 1)); + + if (rows > 0 && columns > 0) + MODELTESTER_VERIFY(model->hasIndex(0, 0)); + + // hasIndex() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::index() + */ +void QAbstractItemModelTesterPrivate::index() +{ + const int rows = model->rowCount(); + const int columns = model->columnCount(); + + for (int row = 0; row < rows; ++row) { + for (int column = 0; column < columns; ++column) { + // Make sure that the same index is *always* returned + QModelIndex a = model->index(row, column); + QModelIndex b = model->index(row, column); + MODELTESTER_VERIFY(a.isValid()); + MODELTESTER_VERIFY(b.isValid()); + MODELTESTER_COMPARE(a, b); + } + } + + // index() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::parent() + */ +void QAbstractItemModelTesterPrivate::parent() +{ + // Make sure the model won't crash and will return an invalid QModelIndex + // when asked for the parent of an invalid index. + MODELTESTER_VERIFY(!model->parent(QModelIndex()).isValid()); + + if (!model->hasChildren()) + return; + + // Column 0 | Column 1 | + // QModelIndex() | | + // \- topIndex | topIndex1 | + // \- childIndex | childIndex1 | + + // Common error test #1, make sure that a top level index has a parent + // that is a invalid QModelIndex. + QModelIndex topIndex = model->index(0, 0, QModelIndex()); + MODELTESTER_VERIFY(!model->parent(topIndex).isValid()); + + // Common error test #2, make sure that a second level index has a parent + // that is the first level index. + if (model->hasChildren(topIndex)) { + QModelIndex childIndex = model->index(0, 0, topIndex); + MODELTESTER_VERIFY(childIndex.isValid()); + MODELTESTER_COMPARE(model->parent(childIndex), topIndex); + } + + // Common error test #3, the second column should NOT have the same children + // as the first column in a row. + // Usually the second column shouldn't have children. + if (model->hasIndex(0, 1)) { + QModelIndex topIndex1 = model->index(0, 1, QModelIndex()); + MODELTESTER_VERIFY(topIndex1.isValid()); + if (model->hasChildren(topIndex) && model->hasChildren(topIndex1)) { + QModelIndex childIndex = model->index(0, 0, topIndex); + MODELTESTER_VERIFY(childIndex.isValid()); + QModelIndex childIndex1 = model->index(0, 0, topIndex1); + MODELTESTER_VERIFY(childIndex1.isValid()); + MODELTESTER_VERIFY(childIndex != childIndex1); + } + } + + // Full test, walk n levels deep through the model making sure that all + // parent's children correctly specify their parent. + checkChildren(QModelIndex()); +} + +/*! + Called from the parent() test. + + A model that returns an index of parent X should also return X when asking + for the parent of the index. + + This recursive function does pretty extensive testing on the whole model in an + effort to catch edge cases. + + This function assumes that rowCount(), columnCount() and index() already work. + If they have a bug it will point it out, but the above tests should have already + found the basic bugs because it is easier to figure out the problem in + those tests then this one. + */ +void QAbstractItemModelTesterPrivate::checkChildren(const QModelIndex &parent, int currentDepth) +{ + // First just try walking back up the tree. + QModelIndex p = parent; + while (p.isValid()) + p = p.parent(); + + // For models that are dynamically populated + if (model->canFetchMore(parent)) { + fetchingMore = true; + model->fetchMore(parent); + fetchingMore = false; + } + + const int rows = model->rowCount(parent); + const int columns = model->columnCount(parent); + + if (rows > 0) + MODELTESTER_VERIFY(model->hasChildren(parent)); + + // Some further testing against rows(), columns(), and hasChildren() + MODELTESTER_VERIFY(rows >= 0); + MODELTESTER_VERIFY(columns >= 0); + if (rows > 0 && columns > 0) + MODELTESTER_VERIFY(model->hasChildren(parent)); + + const QModelIndex topLeftChild = model->index(0, 0, parent); + + MODELTESTER_VERIFY(!model->hasIndex(rows, 0, parent)); + MODELTESTER_VERIFY(!model->hasIndex(rows + 1, 0, parent)); + + for (int r = 0; r < rows; ++r) { + MODELTESTER_VERIFY(!model->hasIndex(r, columns, parent)); + MODELTESTER_VERIFY(!model->hasIndex(r, columns + 1, parent)); + for (int c = 0; c < columns; ++c) { + MODELTESTER_VERIFY(model->hasIndex(r, c, parent)); + QModelIndex index = model->index(r, c, parent); + // rowCount() and columnCount() said that it existed... + if (!index.isValid()) + qCWarning(lcModelTest) << "Got invalid index at row=" << r << "col=" << c << "parent=" << parent; + MODELTESTER_VERIFY(index.isValid()); + + // index() should always return the same index when called twice in a row + QModelIndex modifiedIndex = model->index(r, c, parent); + MODELTESTER_COMPARE(index, modifiedIndex); + + { + const QModelIndex sibling = model->sibling(r, c, topLeftChild); + MODELTESTER_COMPARE(index, sibling); + } + { + const QModelIndex sibling = topLeftChild.sibling(r, c); + MODELTESTER_COMPARE(index, sibling); + } + + // Some basic checking on the index that is returned + MODELTESTER_COMPARE(index.model(), model); + MODELTESTER_COMPARE(index.row(), r); + MODELTESTER_COMPARE(index.column(), c); + + // If the next test fails here is some somewhat useful debug you play with. + if (model->parent(index) != parent) { + qCWarning(lcModelTest) << "Inconsistent parent() implementation detected:"; + qCWarning(lcModelTest) << " index=" << index << "exp. parent=" << parent << "act. parent=" << model->parent(index); + qCWarning(lcModelTest) << " row=" << r << "col=" << c << "depth=" << currentDepth; + qCWarning(lcModelTest) << " data for child" << model->data(index).toString(); + qCWarning(lcModelTest) << " data for parent" << model->data(parent).toString(); + } + + // Check that we can get back our real parent. + MODELTESTER_COMPARE(model->parent(index), parent); + + QPersistentModelIndex persistentIndex = index; + + // recursively go down the children + if (model->hasChildren(index) && currentDepth < 10) + checkChildren(index, ++currentDepth); + + // make sure that after testing the children that the index doesn't change. + QModelIndex newerIndex = model->index(r, c, parent); + MODELTESTER_COMPARE(persistentIndex, newerIndex); + } + } +} + +/*! + Tests model's implementation of QAbstractItemModel::data() + */ +void QAbstractItemModelTesterPrivate::data() +{ + if (!model->hasChildren()) + return; + + MODELTESTER_VERIFY(model->index(0, 0).isValid()); + + // General Purpose roles that should return a QString + QVariant variant; + variant = model->data(model->index(0, 0), Qt::DisplayRole); + if (variant.isValid()) + MODELTESTER_VERIFY(variant.canConvert()); + variant = model->data(model->index(0, 0), Qt::ToolTipRole); + if (variant.isValid()) + MODELTESTER_VERIFY(variant.canConvert()); + variant = model->data(model->index(0, 0), Qt::StatusTipRole); + if (variant.isValid()) + MODELTESTER_VERIFY(variant.canConvert()); + variant = model->data(model->index(0, 0), Qt::WhatsThisRole); + if (variant.isValid()) + MODELTESTER_VERIFY(variant.canConvert()); + + // General Purpose roles that should return a QSize + variant = model->data(model->index(0, 0), Qt::SizeHintRole); + if (variant.isValid()) + MODELTESTER_VERIFY(variant.canConvert()); + + // Check that the alignment is one we know about + QVariant textAlignmentVariant = model->data(model->index(0, 0), Qt::TextAlignmentRole); + if (textAlignmentVariant.isValid()) { + Qt::Alignment alignment = textAlignmentVariant.value(); + MODELTESTER_COMPARE(alignment, (alignment & (Qt::AlignHorizontal_Mask | Qt::AlignVertical_Mask))); + } + + // Check that the "check state" is one we know about. + QVariant checkStateVariant = model->data(model->index(0, 0), Qt::CheckStateRole); + if (checkStateVariant.isValid()) { + int state = checkStateVariant.toInt(); + MODELTESTER_VERIFY(state == Qt::Unchecked + || state == Qt::PartiallyChecked + || state == Qt::Checked); + } + + Q_Q(QAbstractItemModelTester); + + if (!QTestPrivate::testDataGuiRoles(q)) + return; +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsInserted() + */ +void QAbstractItemModelTesterPrivate::rowsAboutToBeInserted(const QModelIndex &parent, int start, int end) +{ + qCDebug(lcModelTest) << "rowsAboutToBeInserted" + << "start=" << start << "end=" << end << "parent=" << parent + << "parent data=" << model->data(parent).toString() + << "current count of parent=" << model->rowCount(parent) + << "last before insertion=" << model->index(start - 1, 0, parent) << model->data(model->index(start - 1, 0, parent)); + + Changing c; + c.parent = parent; + c.oldSize = model->rowCount(parent); + c.last = (start - 1 >= 0) ? model->index(start - 1, 0, parent).data() : QVariant(); + c.next = (start < c.oldSize) ? model->index(start, 0, parent).data() : QVariant(); + insert.push(c); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeInserted() + */ +void QAbstractItemModelTesterPrivate::rowsInserted(const QModelIndex &parent, int start, int end) +{ + qCDebug(lcModelTest) << "rowsInserted" + << "start=" << start << "end=" << end << "parent=" << parent + << "parent data=" << model->data(parent).toString() + << "current count of parent=" << model->rowCount(parent); + + for (int i = start; i <= end; ++i) { + qCDebug(lcModelTest) << " itemWasInserted:" << i + << model->index(i, 0, parent).data(); + } + + + Changing c = insert.pop(); + MODELTESTER_COMPARE(parent, c.parent); + + MODELTESTER_COMPARE(model->rowCount(parent), c.oldSize + (end - start + 1)); + if (start - 1 >= 0) + MODELTESTER_COMPARE(model->data(model->index(start - 1, 0, c.parent)), c.last); + + if (end + 1 < model->rowCount(c.parent)) { + if (c.next != model->data(model->index(end + 1, 0, c.parent))) { + qDebug() << start << end; + for (int i = 0; i < model->rowCount(); ++i) + qDebug() << model->index(i, 0).data().toString(); + qDebug() << c.next << model->data(model->index(end + 1, 0, c.parent)); + } + + MODELTESTER_COMPARE(model->data(model->index(end + 1, 0, c.parent)), c.next); + } +} + +void QAbstractItemModelTesterPrivate::layoutAboutToBeChanged() +{ + for (int i = 0; i < qBound(0, model->rowCount(), 100); ++i) + changing.append(QPersistentModelIndex(model->index(i, 0))); +} + +void QAbstractItemModelTesterPrivate::layoutChanged() +{ + for (int i = 0; i < changing.count(); ++i) { + QPersistentModelIndex p = changing[i]; + MODELTESTER_COMPARE(model->index(p.row(), p.column(), p.parent()), QModelIndex(p)); + } + changing.clear(); +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsRemoved() + */ +void QAbstractItemModelTesterPrivate::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + qCDebug(lcModelTest) << "rowsAboutToBeRemoved" + << "start=" << start << "end=" << end << "parent=" << parent + << "parent data=" << model->data(parent).toString() + << "current count of parent=" << model->rowCount(parent) + << "last before removal=" << model->index(start - 1, 0, parent) << model->data(model->index(start - 1, 0, parent)); + + Changing c; + c.parent = parent; + c.oldSize = model->rowCount(parent); + c.last = model->data(model->index(start - 1, 0, parent)); + c.next = model->data(model->index(end + 1, 0, parent)); + remove.push(c); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeRemoved() + */ +void QAbstractItemModelTesterPrivate::rowsRemoved(const QModelIndex &parent, int start, int end) +{ + qCDebug(lcModelTest) << "rowsRemoved" + << "start=" << start << "end=" << end << "parent=" << parent + << "parent data=" << model->data(parent).toString() + << "current count of parent=" << model->rowCount(parent); + + Changing c = remove.pop(); + MODELTESTER_COMPARE(parent, c.parent); + MODELTESTER_COMPARE(model->rowCount(parent), c.oldSize - (end - start + 1)); + MODELTESTER_COMPARE(model->data(model->index(start - 1, 0, c.parent)), c.last); + MODELTESTER_COMPARE(model->data(model->index(start, 0, c.parent)), c.next); +} + +void QAbstractItemModelTesterPrivate::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + MODELTESTER_VERIFY(topLeft.isValid()); + MODELTESTER_VERIFY(bottomRight.isValid()); + QModelIndex commonParent = bottomRight.parent(); + MODELTESTER_COMPARE(topLeft.parent(), commonParent); + MODELTESTER_VERIFY(topLeft.row() <= bottomRight.row()); + MODELTESTER_VERIFY(topLeft.column() <= bottomRight.column()); + int rowCount = model->rowCount(commonParent); + int columnCount = model->columnCount(commonParent); + MODELTESTER_VERIFY(bottomRight.row() < rowCount); + MODELTESTER_VERIFY(bottomRight.column() < columnCount); +} + +void QAbstractItemModelTesterPrivate::headerDataChanged(Qt::Orientation orientation, int start, int end) +{ + MODELTESTER_VERIFY(start >= 0); + MODELTESTER_VERIFY(end >= 0); + MODELTESTER_VERIFY(start <= end); + int itemCount = orientation == Qt::Vertical ? model->rowCount() : model->columnCount(); + MODELTESTER_VERIFY(start < itemCount); + MODELTESTER_VERIFY(end < itemCount); +} + +bool QAbstractItemModelTesterPrivate::verify(bool statement, + const char *statementStr, const char *description, + const char *file, int line) +{ + static const char formatString[] = "FAIL! %s (%s) returned FALSE (%s:%d)"; + + switch (failureReportingMode) { + case QAbstractItemModelTester::FailureReportingMode::QtTest: + return QTest::qVerify(statement, statementStr, description, file, line); + break; + + case QAbstractItemModelTester::FailureReportingMode::Warning: + if (!statement) + qCWarning(lcModelTest, formatString, statementStr, description, file, line); + break; + + case QAbstractItemModelTester::FailureReportingMode::Fatal: + if (!statement) + qFatal(formatString, statementStr, description, file, line); + break; + } + + return statement; +} + + +template +bool QAbstractItemModelTesterPrivate::compare(const T1 &t1, const T2 &t2, + const char *actual, const char *expected, + const char *file, int line) +{ + const bool result = static_cast(t1 == t2); + + static const char formatString[] = "FAIL! Compared values are not the same:\n Actual (%s) %s\n Expected (%s) %s\n (%s:%d)"; + + switch (failureReportingMode) { + case QAbstractItemModelTester::FailureReportingMode::QtTest: + return QTest::qCompare(t1, t2, actual, expected, file, line); + break; + + case QAbstractItemModelTester::FailureReportingMode::Warning: + if (!result) + qCWarning(lcModelTest, formatString, actual, QTest::toString(t1), expected, QTest::toString(t2), file, line); + break; + + case QAbstractItemModelTester::FailureReportingMode::Fatal: + if (!result) + qFatal(formatString, actual, QTest::toString(t1), expected, QTest::toString(t2), file, line); + break; + } + + return result; +} + + +QT_END_NAMESPACE diff --git a/src/testlib/qabstractitemmodeltester.h b/src/testlib/qabstractitemmodeltester.h new file mode 100644 index 0000000000..617e189817 --- /dev/null +++ b/src/testlib/qabstractitemmodeltester.h @@ -0,0 +1,131 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QABSTRACTITEMMODELTESTER_H +#define QABSTRACTITEMMODELTESTER_H + +#include +#include + +#ifdef QT_GUI_LIB +#include +#include +#include +#include +#include +#include +#endif + +QT_BEGIN_NAMESPACE + +class QAbstractItemModel; +class QAbstractItemModelTester; +class QAbstractItemModelTesterPrivate; + +namespace QTestPrivate { +inline bool testDataGuiRoles(QAbstractItemModelTester *tester); +} + +class Q_TESTLIB_EXPORT QAbstractItemModelTester : public QObject +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QAbstractItemModelTester) + +public: + enum class FailureReportingMode { + QtTest, + Warning, + Fatal + }; + + QAbstractItemModelTester(QAbstractItemModel *model, QObject *parent = nullptr); + QAbstractItemModelTester(QAbstractItemModel *model, FailureReportingMode mode, QObject *parent = nullptr); + + QAbstractItemModel *model() const; + FailureReportingMode failureReportingMode() const; + +private: + friend inline bool QTestPrivate::testDataGuiRoles(QAbstractItemModelTester *tester); + bool verify(bool statement, const char *statementStr, const char *description, const char *file, int line); +}; + +namespace QTestPrivate { +inline bool testDataGuiRoles(QAbstractItemModelTester *tester) +{ +#ifdef QT_GUI_LIB + +#define MODELTESTER_VERIFY(statement) \ +do { \ + if (!tester->verify(static_cast(statement), #statement, "", __FILE__, __LINE__)) \ + return false; \ +} while (false) + + const auto model = tester->model(); + Q_ASSERT(model); + + if (!model->hasChildren()) + return true; + + QVariant variant; + + variant = model->data(model->index(0, 0), Qt::DecorationRole); + if (variant.isValid()) { + MODELTESTER_VERIFY(variant.canConvert() + || variant.canConvert() + || variant.canConvert() + || variant.canConvert() + || variant.canConvert()); + } + + // General Purpose roles that should return a QFont + variant = model->data(model->index(0, 0), Qt::FontRole); + if (variant.isValid()) + MODELTESTER_VERIFY(variant.canConvert()); + + // General Purpose roles that should return a QColor or a QBrush + variant = model->data(model->index(0, 0), Qt::BackgroundColorRole); + if (variant.isValid()) + MODELTESTER_VERIFY(variant.canConvert() || variant.canConvert()); + + variant = model->data(model->index(0, 0), Qt::TextColorRole); + if (variant.isValid()) + MODELTESTER_VERIFY(variant.canConvert() || variant.canConvert()); + +#undef MODELTESTER_VERIFY + +#else + Q_UNUSED(tester); +#endif // QT_GUI_LIB + + return true; +} +} // namespaceQTestPrivate + +QT_END_NAMESPACE + +#endif // QABSTRACTITEMMODELTESTER_H diff --git a/src/testlib/testlib.pro b/src/testlib/testlib.pro index f99f28ca84..109feee630 100644 --- a/src/testlib/testlib.pro +++ b/src/testlib/testlib.pro @@ -11,7 +11,9 @@ unix:!embedded:QMAKE_PKGCONFIG_DESCRIPTION = Qt \ QMAKE_DOCS = $$PWD/doc/qttestlib.qdocconf -HEADERS = qbenchmark.h \ +HEADERS = \ + qabstractitemmodeltester.h \ + qbenchmark.h \ qbenchmark_p.h \ qbenchmarkmeasurement_p.h \ qbenchmarktimemeasurers_p.h \ @@ -40,7 +42,9 @@ HEADERS = qbenchmark.h \ qtestblacklist_p.h \ qtesthelpers_p.h -SOURCES = qtestcase.cpp \ +SOURCES = \ + qabstractitemmodeltester.cpp \ + qtestcase.cpp \ qtestlog.cpp \ qtesttable.cpp \ qtestdata.cpp \ -- cgit v1.2.3