Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-90454

Tool for inspecting connections between QObjects

    XMLWordPrintable

Details

    • Suggestion
    • Resolution: Unresolved
    • P3: Somewhat important
    • None
    • None
    • Testing: qtestlib
    • None

    Description

      To have exported class into QObject as singleton cannot be running in separate thread (moveToThread). To overcome this issue, you need to create an intermediate object, expose it to QML and connect it with worker QObject that is running in separate thread.

      With this pattern, you get a set of code that cannot be easily tested (connectTo method, for instance, more on example below) and may pose problems when refactoring and changing your API between the intermediate and worker class.

      An example code:

      // Intermediate
      class MyIntermediate : public QObject {
        Q_OBJECT
        QML_ELEMENT
        QML_SINGLETON
      
      public:
        explicit MyIntermediate() = default;
      
        void connectTo(const MyWorker *worker) const {
          connect(this, &MyIntermediate::query, worker, &MyWorker::query);
          connect(this, &MyIntermediate::command, worker, &MyWorker::command);
      
          connect(worker, &MyWorker::commandError, this, &MyIntermediate::commandErrorHandler);
          connect(worker, &MyWorker::commandSuccess, this, &MyIntermediate::commandSuccess);
          connect(worker, &MyWorker::queryResponse, this, &MyIntermediate::queryResponse);
        }
      
      public slots:
        void commandErrorHandler(int errorCode) {
          // Here be some mapping if needed
        }
      
      signals:
        // Queries
        void query();
      
        // Commands
        void command(const QString &input);
      
        // Responses
        void queryResponse(const QStringList &results);
        void commandSuccess();
        void commandError(int erorCode);
      };
      
      // Worker
      class MyWorker : public QObject {
        Q_OBJECT
      
      public:
        explicit MyWorker() = default;
      
      public slots:
        // Queries
        virtual void query();
      
        // Commands
        virtual void command(const QString &input);
      
      signals:
        void queryResponse(const QStringList &results);
        void commandSuccess();
        void commandError(int errorCode);
      };
      
      

      As you can see, it would be hard to test the connections (unless writing integration tests for each function).

      So I've created a few helper methods to verify the connectivity is correct (while you go and change the interaction between both classes).

      The tool:

      #define INSPECT_CONNECTIONS                                                    \
      private:                                                                       \
        int countReceivers(const char *signal) const override {                      \
          return receivers(signal);                                                  \
        }                                                                            \
        const QObject *getQObject() const override { return this; }                  \
        void assertSuperclass(const QMetaObject &superclassMetaObject) override {    \
          auto errorMsg                                                              \
              = QString(                                                             \
                    "Inspecting wrong class for connections, inspecting %1 on %2")   \
                    .arg(superclassMetaObject.className())                           \
                    .arg(metaObject()->className());                                 \
          QVERIFY2(metaObject()->inherits(&superclassMetaObject),                    \
                   qPrintable(errorMsg));                                            \
        }
      
      #define INSPECT_SIGNAL(y, x, c) QCOMPARE(y.numConnections(x), c)
      
      class ConnectionInspector;
      
      class InspectorAddon {
        friend class ConnectionInspector;
      
      protected:
        virtual int countReceivers(const char *signal) const = 0;
        virtual void assertSuperclass(const QMetaObject &superclassMetaObject) = 0;
        virtual const QObject *getQObject() const = 0;
      };
      
      class ConnectionInspector : public QMap<QString, int> {
      public:
        int sum() {
          int output = 0;
          for (auto num : qAsConst(*this))
            output += num;
          return output;
        }
      
        template <typename PointerToMemberFunction>
        inline int numConnections(PointerToMemberFunction signal) {
          auto signalMember = QMetaMethod::fromSignal(signal);
          auto name = methodToString(signalMember);
          return value(name);
        }
      
        template <class T> void inspect(const InspectorAddon *objectInspector) {
          auto *object = objectInspector->getQObject();
          assertSuperclass(&T::staticMetaObject, object->metaObject());
      
          for (auto objectSignal : listSignals<T>()) {
            auto signalSignature = objectSignal.methodSignature();
            // A magic number from SIGNAL macro
            auto signalDescriptor = signalSignature.prepend('2');
            insert(ConnectionInspector::methodToString(objectSignal),
                   objectInspector->countReceivers(signalDescriptor));
          }
        }
      
      private:
        static QString methodToString(const QMetaMethod &method) {
          return QString("%1:%2").arg(method.enclosingMetaObject()->className(),
                                      QString(method.methodSignature()));
        }
      
        static void assertSuperclass(const QMetaObject *superclassMetaObject,
                                     const QMetaObject *metaObject) {
          auto errorMsg
              = QString("Inspecting wrong class for connections, inspecting %1 on %2")
                    .arg(superclassMetaObject->className(), metaObject->className());
          QVERIFY2(metaObject->inherits(superclassMetaObject), qPrintable(errorMsg));
        }
      
        template <class T> static QList<QMetaMethod> listSignals() {
          QList<QMetaMethod> objectSignals;
      
          auto numberOfMethods = T::staticMetaObject.methodCount();
          for (auto methodIndex = T::staticMetaObject.methodOffset();
               methodIndex < numberOfMethods; ++methodIndex) {
            auto method = T::staticMetaObject.method(methodIndex);
            if (method.methodType() == QMetaMethod::Signal) {
              objectSignals << method;
            }
          }
      
          return objectSignals;
        }
      };
      

      And this is how the test looks like:

      class MonitoredMyWorker : public MyWorker, public InspectorAddon {
        Q_OBJECT
        INSPECT_CONNECTIONS
      };
      
      class MonitoredMyIntermediate : public MyIntermediate, public InspectorAddon {
        Q_OBJECT
        INSPECT_CONNECTIONS
      };
      
      void MyIntermediateTest::test_connectTo() {
        InspectedMyIntermediate intermediate;
        InspectedMyWorker worker;
      
        intermediate.connectTo(&worker);
      
        ConnectionInspector inspector;
        inspector.inspect<MyWorker>(&worker);
        inspector.inspect<MyIntermediate>(&intermediate);
      
        INSPECT_SIGNAL(inspector, &MyIntermediate::query, 1); // Cannot assert the other end, but can assert receivers count
        // All other signals ...
        QCOMPARE(inspector.sum(), 5); // "Guard" assert if you add new connections
      }
      

      It is crude and feels a bit of "bad magic", but wanted to share if anyone sees this useful in any way. It could require some feature (like asserting all signals are connected, or number of all available signals, etc.).

      Attachments

        No reviews matched the request. Check your Options in the drop-down menu of this sections header.

        Activity

          People

            macadder Jason McDonald
            gogo Gregor Kališnik
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

            Dates

              Created:
              Updated:

              Gerrit Reviews

                There are no open Gerrit changes