Skip to content

New in Qt 5.11: improvements to the model/view APIs (part 2)

In the last episode of this series we discussed QAbstractItemModel::checkIndex().

QAbstractItemModel::checkIndex() is a function added in Qt 5.11 that allows developers of model classes to perform some validity checks on the QModelIndex objects passed to the model; specifically, on the indices passed to the APIs that model classes need to implement (rowCount(), columnCount(), data, setData(), etc.).

These validity checks can be very useful when developing (and debugging) the model itself, or when using a complicated stack of models and proxy models.

However, by its nature, checkIndex() will check just the one index passed to it. It is unable to perform consistency checks on the model “as a whole”. For instance, for a given index, a model’s reimplementation of hasChildren() must be consistent with the values returned by rowCount() and columnCount().

Enter QAbstractItemModelTester

QAbstractItemModelTester is a new class in Qt 5.11 which helps to test item models. It complements checkIndex() by checking the consistency of the entire model from the point of view of a user of the model (like a view).

(Technically speaking, it’s not really new: Qt has had a private class called ModelTest amongst its own autotests for years; ModelTest‘s usecase is the same as QAbstractItemModelTester. For Qt 5.11 I’ve took ModelTest, cleaned it up, renamed it to QAbstractItemModelTester and added it as public API in the QtTest module.)

QAbstractItemModelTester is simple and immediate to use: just create an instance, and pass to its constructor the model that needs to be tested, like this:

QAbstractItemModelTester *tester = new QAbstractItemModelTester(modelToTest);
// keep the tester object around; it will run a series of tests on modelToTest

QAbstractItemModelTester will then automatically perform a series of non-destructive checks on the model. These checks are aimed at verifying the model’s overall consistency; they will verify that the (many) function of the QAbstractItemModel API are always coherent, and will not confuse anyone using the model (like a view, or a proxy model).

Furthermore, QAbstractItemModelTester will also listen to the signals emitted by the model to test; an emission triggers further validation checks, for instance emitting that a row has been added to the model will make tester verify that the row count has been increased by exactly 1.

Example

As an example, let’s consider this very simple model class (a list model of strings):

class StringListModel : public QAbstractListModel {
    Q_OBJECT
public:
    using QAbstractListModel::QAbstractListModel;

    int rowCount(const QModelIndex &parent) const override {
        Q_ASSERT(checkIndex(parent));
        if (parent.isValid())
            return 0;
        return m_strings.size();
    }

    QVariant data(const QModelIndex &index, int role) const override {
        Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));
        if (role != Qt::DisplayRole)
            return {};
        return m_strings[index.row()];
    }

    // appends the string at the end of the model
    void appendString(const QString &string) {
        beginInsertRows(QModelIndex(), m_strings.size(), m_strings.size() + 1);
        m_strings.append(string);
        endInsertRows();
    }

private:
    QStringList m_strings;
};

The implementation of the model is straightforward: since it’s a list model, only rowCount() and data() need to be implemented. Our model also has a third function (appendString()) that allows the application code to insert a new string into the model, specifically, at the very end.

Let’s focus on this very last function. When inserting new rows into a model, we are supposed to notify the views about the change. QAbstractItemModel’s documentation tells us to use a “transactional” approach: we need to call beginInsertRows() before inserting, perform the insertion, then call endInsertRows().

The parameters to beginInsertRows() describe how many new rows are being inserted, and at which position (the documentation discusses all the details); in our case, we are adding exactly one row at the very end of the model, so we pass m_strings.size() as the position at which the new row will appear.

The third parameter is however wrong: it’s basically stating that we are going to add two rows at the end of the model, while we’re actually adding only one. If we read the documentation carefully, we notice that the parameter shall indicate the index of the last row being added; since we’re adding only one, the index of the last row being added is the same as the first row being added. In order words: the second and third parameter of beginInsertRows() should be identical, like this:

        // correct call: adds 1 row, at position m_strings.size()
        beginInsertRows(QModelIndex(), m_strings.size(), m_strings.size());

These sort of mistakes (in the end, an off-by-one error) can cause lots of troubles: for instance, persistent model indexes start pointing to the wrong data, proxy models get corrupted, and so on.

If we use our model into a QAbstractItemModelTester, we get a notification that there is something wrong going on. For instance, given this setup:

    auto model = new StringListModel;
    auto tester = new QAbstractItemModelTester(model, 
                          QAbstractItemModelTester::FailureReportingMode::Warning);

Sometime later, when appendString() is called, QAbstractItemModelTester will detect the mismatch between the rows we are promising we are adding to the model, and the ones we are actually adding to the model. Since the tester object is set to Warning mode it will print a warning on the console, like this:

FAIL! Compared values are not the same:
   Actual (model->rowCount(parent)) 1
   Expected (c.oldSize + (end - start + 1)) 2
   (qabstractitemmodeltester.cpp:669)

We can then use ordinary debugging tools (e.g. a debugger with a breakpoint) to debug what condition caused this warning. We will then notice that the call stack leading to the warning starts in our appendString() function, so we need to double check it and figure out what’s going on in there.

Lastly, note the calls to checkIndex() in the model’s functions that implement the QAbstractListModel API (by overriding rowCount() and data()). QAbstractItemModelTester and checkIndex are meant to complement each other, as the former tests the entire model, while the latter is used to validate the model indexes passed to the various functions.

Reporting failures

In case some of the checks fail, QAbstractItemModelTester has three different ways of reporting the failure to the developer, selectable via an argument to the constructor:

  • By default, QAbstractItemModelTester uses the QtTest failure report mode: failures are reported through the QtTest’s logging mechanisms. This report mode is suitable if we are using QAbstractItemModelTester in a test driven by the QtTest framework, for instance when building unit tests for our model class; a problem in our model will result in the current test function being marked as failing.
  • QAbstractItemModelTester is also usable outside unit tests driven by QtTest. By specifying the Warning failure mode, QAbstractItemModelTester will print a warning statement in the qt.modeltest logging category.
  • Finally, the Fatal failure mode causes the application to crash immediately. In this mode, if a check fails, QAbstractItemModelTester is going to call qFatal() passing a string with a textual description of the error.

No matter which failure mode is set, QAbstractItemModelTester will print debug information about our model in the aforementioned qt.modeltest logging category, showing what kind of changes were detected by the tester class and what sorts of checks are being run. Sometimes this can be useful in order to track down bugs.

Note that this debug output needs to be explicitly enabled by the developer, for instance by setting the QT_LOGGING_RULES environment variable to contain the string qt.modeltest.debug=true. (For more information on Qt’s logging categories, see here).

Remarks

It is important to note that QAbstractItemModelTester will never perform destructive tests on a model (set new data, insert/remove rows and columns, and so on). This allows developers to safely use the tester in all conditions, including having it enabled in a full build of the application. This way models can be tested in real-world conditions, using actual data, as well as in long-running scenarios (which maybe are required to trigger some bug).

On the other hand, in order to thoroughly test a custom model, we may need to write checks for destructive changes. Since only the developer knows the exact behaviour of a model that undergoes a modification, the developer is supposed to write dedicated unit tests, thus complementing the tests done by QAbstractItemModelTester.

Finally: as I wrote in the last blog post, I believe that the model/view APIs in Qt have a narrow contract: one should never attempt to pass invalid data to a model (e.g. a model index out of range). QAbstractItemModelTester honours this, and will never attempt any illegal operation on a model. If for some reason you need to give your model a wide contract and want to test that contract, further tests (outside QAbstractItemModelTester) are needed.

That’s it for now for now, but stay tuned for more contributions to Qt!

Categories: KDAB Blogs / KDAB on Qt / QML / Qt / QtDevelopment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

By continuing to use the site, you agree to the use of cookies. More information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close