Add support for interface based script events and actions

pull/339/head
Michael Zanetti 2020-10-02 13:18:27 +02:00
parent a0add78af0
commit 79dd00cb57
10 changed files with 532 additions and 32 deletions

View File

@ -33,6 +33,8 @@ HEADERS += nymeacore.h \
scriptengine/scriptalarm.h \ scriptengine/scriptalarm.h \
scriptengine/scriptengine.h \ scriptengine/scriptengine.h \
scriptengine/scriptevent.h \ scriptengine/scriptevent.h \
scriptengine/scriptinterfaceaction.h \
scriptengine/scriptinterfaceevent.h \
scriptengine/scriptstate.h \ scriptengine/scriptstate.h \
transportinterface.h \ transportinterface.h \
nymeaconfiguration.h \ nymeaconfiguration.h \
@ -112,6 +114,8 @@ SOURCES += nymeacore.cpp \
scriptengine/scriptalarm.cpp \ scriptengine/scriptalarm.cpp \
scriptengine/scriptengine.cpp \ scriptengine/scriptengine.cpp \
scriptengine/scriptevent.cpp \ scriptengine/scriptevent.cpp \
scriptengine/scriptinterfaceaction.cpp \
scriptengine/scriptinterfaceevent.cpp \
scriptengine/scriptstate.cpp \ scriptengine/scriptstate.cpp \
transportinterface.cpp \ transportinterface.cpp \
nymeaconfiguration.cpp \ nymeaconfiguration.cpp \

View File

@ -68,6 +68,19 @@ void ScriptAction::setThingId(const QString &thingId)
} }
} }
QString ScriptAction::interfaceName() const
{
return m_interfaceName;
}
void ScriptAction::setInterfaceName(const QString &interfaceName)
{
if (m_interfaceName != interfaceName) {
m_interfaceName = interfaceName;
emit interfaceNameChanged();
}
}
QString ScriptAction::actionTypeId() const QString ScriptAction::actionTypeId() const
{ {
return m_actionTypeId; return m_actionTypeId;
@ -96,11 +109,24 @@ void ScriptAction::setActionName(const QString &actionName)
void ScriptAction::execute(const QVariantMap &params) void ScriptAction::execute(const QVariantMap &params)
{ {
Things things;
if (m_thingId.isEmpty() && !m_interfaceName.isEmpty()) {
foreach (Thing *thing, m_thingManager->configuredThings()) {
if (thing->thingClass().interfaces().contains(m_interfaceName)) {
things.append(thing);
}
}
}
Thing *thing = m_thingManager->configuredThings().findById(ThingId(m_thingId)); Thing *thing = m_thingManager->configuredThings().findById(ThingId(m_thingId));
if (!thing) { if (thing && !things.contains(thing)) {
qCWarning(dcScriptEngine) << "No thing with id" << m_thingId; things.append(thing);
}
if (things.isEmpty()) {
qCWarning(dcScriptEngine) << "No things matching by id" << m_thingId << "and interface" << m_interfaceName;
return; return;
} }
foreach (Thing *thing, things) {
ActionType actionType; ActionType actionType;
if (!ActionTypeId(m_actionTypeId).isNull()) { if (!ActionTypeId(m_actionTypeId).isNull()) {
actionType = thing->thingClass().actionTypes().findById(ActionTypeId(m_actionTypeId)); actionType = thing->thingClass().actionTypes().findById(ActionTypeId(m_actionTypeId));
@ -108,10 +134,10 @@ void ScriptAction::execute(const QVariantMap &params)
actionType = thing->thingClass().actionTypes().findByName(m_actionName); actionType = thing->thingClass().actionTypes().findByName(m_actionName);
} }
if (actionType.id().isNull()) { if (actionType.id().isNull()) {
qCWarning(dcScriptEngine()) << "Either a valid actionTypeId or actionName is required"; qCWarning(dcScriptEngine()) << "Thing" << thing->name() << "does not have actionTypeId" << m_actionTypeId << "or actionName" << m_actionName;
return; continue;
} }
Action action(actionType.id(), ThingId(m_thingId), Action::TriggeredByScript); Action action(actionType.id(), thing->id(), Action::TriggeredByScript);
ParamList paramList; ParamList paramList;
foreach (const QString &paramNameOrId, params.keys()) { foreach (const QString &paramNameOrId, params.keys()) {
ParamType paramType; ParamType paramType;
@ -127,7 +153,9 @@ void ScriptAction::execute(const QVariantMap &params)
paramList << Param(paramType.id(), params.value(paramNameOrId)); paramList << Param(paramType.id(), params.value(paramNameOrId));
} }
action.setParams(paramList); action.setParams(paramList);
qCDebug(dcScriptEngine()) << "Executing action:" << action.thingId() << action.actionTypeId() << action.params();
m_thingManager->executeAction(action); m_thingManager->executeAction(action);
} }
}
} }

View File

@ -43,6 +43,7 @@ class ScriptAction : public QObject, public QQmlParserStatus
Q_OBJECT Q_OBJECT
Q_INTERFACES(QQmlParserStatus) Q_INTERFACES(QQmlParserStatus)
Q_PROPERTY(QString thingId READ thingId WRITE setThingId NOTIFY thingIdChanged) Q_PROPERTY(QString thingId READ thingId WRITE setThingId NOTIFY thingIdChanged)
Q_PROPERTY(QString interfaceName READ interfaceName WRITE setInterfaceName NOTIFY interfaceNameChanged)
Q_PROPERTY(QString deviceId READ thingId WRITE setThingId NOTIFY thingIdChanged) // DEPRECATED Q_PROPERTY(QString deviceId READ thingId WRITE setThingId NOTIFY thingIdChanged) // DEPRECATED
Q_PROPERTY(QString actionTypeId READ actionTypeId WRITE setActionTypeId NOTIFY actionTypeIdChanged) Q_PROPERTY(QString actionTypeId READ actionTypeId WRITE setActionTypeId NOTIFY actionTypeIdChanged)
Q_PROPERTY(QString actionName READ actionName WRITE setActionName NOTIFY actionNameChanged) Q_PROPERTY(QString actionName READ actionName WRITE setActionName NOTIFY actionNameChanged)
@ -54,6 +55,9 @@ public:
QString thingId() const; QString thingId() const;
void setThingId(const QString &thingId); void setThingId(const QString &thingId);
QString interfaceName() const;
void setInterfaceName(const QString &interfaceName);
QString actionTypeId() const; QString actionTypeId() const;
void setActionTypeId(const QString &actionTypeId); void setActionTypeId(const QString &actionTypeId);
@ -65,12 +69,14 @@ public slots:
signals: signals:
void thingIdChanged(); void thingIdChanged();
void interfaceNameChanged();
void actionTypeIdChanged(); void actionTypeIdChanged();
void actionNameChanged(); void actionNameChanged();
public: public:
ThingManager *m_thingManager = nullptr; ThingManager *m_thingManager = nullptr;
QString m_thingId; QString m_thingId;
QString m_interfaceName;
QString m_actionTypeId; QString m_actionTypeId;
QString m_actionName; QString m_actionName;
}; };

View File

@ -35,6 +35,8 @@
#include "scriptevent.h" #include "scriptevent.h"
#include "scriptstate.h" #include "scriptstate.h"
#include "scriptalarm.h" #include "scriptalarm.h"
#include "scriptinterfaceaction.h"
#include "scriptinterfaceevent.h"
#include "nymeasettings.h" #include "nymeasettings.h"
@ -63,6 +65,8 @@ ScriptEngine::ScriptEngine(ThingManager *deviceManager, QObject *parent) : QObje
qmlRegisterType<ScriptEvent>("nymea", 1, 0, "ThingEvent"); qmlRegisterType<ScriptEvent>("nymea", 1, 0, "ThingEvent");
qmlRegisterType<ScriptAction>("nymea", 1, 0, "ThingAction"); qmlRegisterType<ScriptAction>("nymea", 1, 0, "ThingAction");
qmlRegisterType<ScriptState>("nymea", 1, 0, "ThingState"); qmlRegisterType<ScriptState>("nymea", 1, 0, "ThingState");
qmlRegisterType<ScriptInterfaceAction>("nymea", 1, 0, "InterfaceAction");
qmlRegisterType<ScriptInterfaceEvent>("nymea", 1, 0, "InterfaceEvent");
qmlRegisterType<ScriptAlarm>("nymea", 1, 0, "Alarm"); qmlRegisterType<ScriptAlarm>("nymea", 1, 0, "Alarm");
m_engine = new QQmlEngine(this); m_engine = new QQmlEngine(this);

View File

@ -28,8 +28,8 @@
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef EVENTLISTENER_H #ifndef SCRIPTEVENT_H
#define EVENTLISTENER_H #define SCRIPTEVENT_H
#include <QObject> #include <QObject>
#include <QUuid> #include <QUuid>
@ -72,7 +72,6 @@ signals:
void eventTypeIdChanged(); void eventTypeIdChanged();
void eventNameChanged(); void eventNameChanged();
// void triggered(ScriptParams *params);
void triggered(const QVariantMap &params); void triggered(const QVariantMap &params);
private: private:
@ -85,4 +84,4 @@ private:
} }
#endif // EVENTLISTENER_H #endif // SCRIPTEVENT_H

View File

@ -0,0 +1,126 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "scriptinterfaceaction.h"
#include "integrations/thingmanager.h"
#include "types/action.h"
#include <QQmlEngine>
#include <qqml.h>
#include "loggingcategories.h"
namespace nymeaserver {
ScriptInterfaceAction::ScriptInterfaceAction(QObject *parent) : QObject(parent)
{
}
void ScriptInterfaceAction::classBegin()
{
m_thingManager = reinterpret_cast<ThingManager*>(qmlEngine(this)->property("thingManager").toULongLong());
}
void ScriptInterfaceAction::componentComplete()
{
}
QString ScriptInterfaceAction::interfaceName() const
{
return m_interfaceName;
}
void ScriptInterfaceAction::setInterfaceName(const QString &interfaceName)
{
if (m_interfaceName != interfaceName) {
m_interfaceName = interfaceName;
emit interfaceNameChanged();
}
}
QString ScriptInterfaceAction::actionName() const
{
return m_actionName;
}
void ScriptInterfaceAction::setActionName(const QString &actionName)
{
if (m_actionName != actionName) {
m_actionName = actionName;
emit actionNameChanged();
}
}
void ScriptInterfaceAction::execute(const QVariantMap &params)
{
Things things;
if (!m_interfaceName.isEmpty()) {
foreach (Thing *thing, m_thingManager->configuredThings()) {
if (thing->thingClass().interfaces().contains(m_interfaceName)) {
things.append(thing);
}
}
}
if (things.isEmpty()) {
qCWarning(dcScriptEngine) << "No things matching by interface" << m_interfaceName;
return;
}
foreach (Thing *thing, things) {
ActionType actionType = thing->thingClass().actionTypes().findByName(m_actionName);
if (actionType.id().isNull()) {
qCWarning(dcScriptEngine()) << "Thing" << thing->name() << "does not have action" << m_actionName;
continue;
}
Action action(actionType.id(), thing->id(), Action::TriggeredByScript);
ParamList paramList;
foreach (const QString &paramNameOrId, params.keys()) {
ParamType paramType;
if (!ParamTypeId(paramNameOrId).isNull()) {
paramType = actionType.paramTypes().findById(ParamTypeId(paramNameOrId));
} else {
paramType = actionType.paramTypes().findByName(paramNameOrId);
}
if (paramType.id().isNull()) {
qCWarning(dcScriptEngine()) << "Invalid param id or name";
continue;
}
paramList << Param(paramType.id(), params.value(paramNameOrId));
}
action.setParams(paramList);
qCDebug(dcScriptEngine()) << "Executing action:" << action.thingId() << action.actionTypeId() << action.params();
m_thingManager->executeAction(action);
}
}
}

View File

@ -0,0 +1,73 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef SCRIPTINTERFACEACTION_H
#define SCRIPTINTERFACEACTION_H
#include <QObject>
#include <QQmlParserStatus>
class ThingManager;
namespace nymeaserver {
class ScriptInterfaceAction : public QObject, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
Q_PROPERTY(QString interfaceName READ interfaceName WRITE setInterfaceName NOTIFY interfaceNameChanged)
Q_PROPERTY(QString actionName READ actionName WRITE setActionName NOTIFY actionNameChanged)
public:
explicit ScriptInterfaceAction(QObject *parent = nullptr);
void classBegin() override;
void componentComplete() override;
QString interfaceName() const;
void setInterfaceName(const QString &interfaceName);
QString actionName() const;
void setActionName(const QString &actionName);
public slots:
void execute(const QVariantMap &params);
signals:
void interfaceNameChanged();
void actionNameChanged();
public:
ThingManager *m_thingManager = nullptr;
QString m_interfaceName;
QString m_actionName;
};
}
#endif // SCRIPTINTERFACEACTION_H

View File

@ -0,0 +1,103 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "scriptinterfaceevent.h"
#include <qqml.h>
#include <QQmlEngine>
#include <QJsonDocument>
namespace nymeaserver {
ScriptInterfaceEvent::ScriptInterfaceEvent(QObject *parent) : QObject(parent)
{
}
void ScriptInterfaceEvent::classBegin()
{
m_thingManager = reinterpret_cast<ThingManager*>(qmlEngine(this)->property("thingManager").toULongLong());
connect(m_thingManager, &ThingManager::eventTriggered, this, &ScriptInterfaceEvent::onEventTriggered);
}
void ScriptInterfaceEvent::componentComplete()
{
}
QString ScriptInterfaceEvent::interfaceName() const
{
return m_interfaceName;
}
void ScriptInterfaceEvent::setInterfaceName(const QString &interfaceName)
{
if (m_interfaceName != interfaceName) {
m_interfaceName = interfaceName;
emit interfaceNameChanged();
}
}
QString ScriptInterfaceEvent::eventName() const
{
return m_eventName;
}
void ScriptInterfaceEvent::setEventName(const QString &eventName)
{
if (m_eventName != eventName) {
m_eventName = eventName;
emit eventNameChanged();
}
}
void ScriptInterfaceEvent::onEventTriggered(const Event &event)
{
Thing *thing = m_thingManager->findConfiguredThing(event.thingId());
if (!thing->thingClass().interfaces().contains(m_interfaceName)) {
return;
}
if (!m_eventName.isEmpty() && thing->thingClass().eventTypes().findByName(m_eventName).id() != event.eventTypeId()) {
return;
}
QVariantMap params;
foreach (const Param &param, event.params()) {
params.insert(param.paramTypeId().toString().remove(QRegExp("[{}]")), param.value().toByteArray());
QString paramName = thing->thingClass().eventTypes().findById(event.eventTypeId()).paramTypes().findById(param.paramTypeId()).name();
params.insert(paramName, param.value().toByteArray());
}
// Note: Explicitly convert the params to a Json document because auto-casting from QVariantMap to the JS engine might drop some values.
emit triggered(event.thingId().toString().remove(QRegExp("[{}]")), QJsonDocument::fromVariant(params).toVariant().toMap());
}
}

View File

@ -0,0 +1,80 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef SCRIPTINTERFACEEVENT_H
#define SCRIPTINTERFACEEVENT_H
#include <QObject>
#include <QUuid>
#include <QQmlParserStatus>
#include "types/event.h"
#include "integrations/thingmanager.h"
namespace nymeaserver {
class ScriptParams;
class ScriptInterfaceEvent: public QObject, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
Q_PROPERTY(QString interfaceName READ interfaceName WRITE setInterfaceName NOTIFY interfaceNameChanged)
Q_PROPERTY(QString eventName READ eventName WRITE setEventName NOTIFY eventNameChanged)
public:
ScriptInterfaceEvent(QObject *parent = nullptr);
void classBegin() override;
void componentComplete() override;
QString interfaceName() const;
void setInterfaceName(const QString &interfaceName);
QString eventName() const;
void setEventName(const QString &eventName);
private slots:
void onEventTriggered(const Event &event);
signals:
void interfaceNameChanged();
void eventNameChanged();
void triggered(const QString &thingId, const QVariantMap &params);
private:
ThingManager *m_thingManager = nullptr;
QString m_interfaceName;
QString m_eventName;
};
}
#endif // SCRIPTINTERFACEEVENT_H

View File

@ -70,6 +70,9 @@ private slots:
void testScriptAlarm_data(); void testScriptAlarm_data();
void testScriptAlarm(); void testScriptAlarm();
void testInterfaceEvent();
void testInterfaceAction();
}; };
@ -383,6 +386,80 @@ void TestScripts::testScriptAlarm()
} }
void TestScripts::testInterfaceEvent()
{
QString script = QString("import QtQuick 2.0\n"
"import nymea 1.0\n"
"Item {\n"
" InterfaceEvent {\n"
" interfaceName: \"%1\"\n"
" eventName: \"%2\"\n"
" onTriggered: {\n"
" TestHelper.logEvent(thingId, eventName, params);\n"
" }\n"
" }\n"
"}\n").arg("power").arg("power");
qCDebug(dcTests()) << "Adding script:\n" << qUtf8Printable(script);
ScriptEngine::AddScriptReply reply = NymeaCore::instance()->scriptEngine()->addScript("TestEvent", script.toUtf8());
QCOMPARE(reply.scriptError, ScriptEngine::ScriptErrorNoError);
QSignalSpy spy(TestHelper::instance(), &TestHelper::eventLogged);
// Generate event by setting state value of powerState
Action action(mockPowerActionTypeId, m_mockThingId);
action.setParams(ParamList() << Param(mockPowerActionPowerParamTypeId, true));
NymeaCore::instance()->thingManager()->executeAction(action);
spy.wait(1);
QCOMPARE(spy.count(), 1);
QCOMPARE(spy.first().at(0).value<ThingId>(), m_mockThingId);
QCOMPARE(spy.first().at(1).toString(), QString("power"));
QVariantMap expectedParams;
expectedParams.insert(mockPowerEventTypeId.toString().remove(QRegExp("[{}]")), true);
expectedParams.insert("power", true);
QCOMPARE(spy.first().at(2).toMap(), expectedParams);
}
void TestScripts::testInterfaceAction()
{
QString script = QString("import QtQuick 2.0\n"
"import nymea 1.0\n"
"Item {\n"
" InterfaceAction {\n"
" id: interfaceAction\n"
" interfaceName: \"%1\"\n"
" actionName: \"%2\"\n"
" }\n"
" Connections {\n"
" target: TestHelper\n"
" onExecuteAction: {\n"
" interfaceAction.execute(params)\n"
" }\n"
" }\n"
"}\n").arg("power").arg("power");
qCDebug(dcTests()) << "Adding script:\n" << qUtf8Printable(script);
ScriptEngine::AddScriptReply reply = NymeaCore::instance()->scriptEngine()->addScript("TestInterfaceAction", script.toUtf8());
QCOMPARE(reply.scriptError, ScriptEngine::ScriptErrorNoError);
QSignalSpy spy(NymeaCore::instance()->thingManager(), &ThingManager::thingStateChanged);
QVariantMap params;
params.insert("power", true);
TestHelper::instance()->executeAction(params);
spy.wait(1);
QCOMPARE(spy.count(), 1);
QCOMPARE(spy.first().at(0).value<Thing*>()->id(), m_mockThingId);
QCOMPARE(spy.first().at(1).value<StateTypeId>(), mockPowerStateTypeId);
QCOMPARE(spy.first().at(2).toBool(), true);
}
#include "testscripts.moc" #include "testscripts.moc"
QTEST_MAIN(TestScripts) QTEST_MAIN(TestScripts)