added methods to query states and properly test state changes on notifications

pull/1/head
Michael Zanetti 2014-04-04 22:47:37 +02:00
parent dc5aa1a2b8
commit 044be78064
17 changed files with 237 additions and 55 deletions

View File

@ -105,10 +105,22 @@ void Device::setStates(const QList<State> &states)
m_states = states;
}
bool Device::hasState(const QUuid &stateTypeId) const
{
foreach (const State &state, m_states) {
if (state.stateTypeId() == stateTypeId) {
return true;
}
}
return false;
}
/*! For convenience, this finds the \l{State} matching the given \a stateTypeId and returns the current valie in this Device. */
QVariant Device::stateValue(const QUuid &stateTypeId) const
{
qDebug() << "device has states:" << m_states.count();
foreach (const State &state, m_states) {
qDebug() << "checking" << stateTypeId << state.stateTypeId();
if (state.stateTypeId() == stateTypeId) {
return state.value();
}

View File

@ -45,6 +45,7 @@ public:
QList<State> states() const;
void setStates(const QList<State> &states);
bool hasState(const QUuid &stateTypeId) const;
QVariant stateValue(const QUuid &stateTypeId) const;
void setStateValue(const QUuid &stateTypeId, const QVariant &value);

View File

@ -123,8 +123,11 @@ QList<DeviceClass> DeviceManager::supportedDevices() const
}
/*! Add a new configured device for the given \l{DeviceClass} and the given parameters.
\a deviceClassId must refer to an existing \{DeviceClass} and \a params must match the parameter description in the \l{DeviceClass}. */
DeviceManager::DeviceError DeviceManager::addConfiguredDevice(const QUuid &deviceClassId, const QVariantMap &params)
\a deviceClassId must refer to an existing \{DeviceClass} and \a params must match the parameter description in the \l{DeviceClass}.
Optionally you can supply an id yourself if you must keep track of the added device. If you don't supply it, a new one will
be generated.
*/
DeviceManager::DeviceError DeviceManager::addConfiguredDevice(const QUuid &deviceClassId, const QVariantMap &params, const QUuid id)
{
DeviceClass deviceClass = findDeviceClass(deviceClassId);
if (deviceClass.id().isNull()) {
@ -138,6 +141,11 @@ DeviceManager::DeviceError DeviceManager::addConfiguredDevice(const QUuid &devic
}
// TODO: Check if parameter type matches
}
foreach(Device *device, m_configuredDevices) {
if (device->id() == id) {
return DeviceErrorDuplicateUuid;
}
}
DevicePlugin *plugin = m_devicePlugins.value(deviceClass.pluginId());
if (!plugin) {
@ -145,7 +153,7 @@ DeviceManager::DeviceError DeviceManager::addConfiguredDevice(const QUuid &devic
return DeviceErrorPluginNotFound;
}
Device *device = new Device(plugin->pluginId(), deviceClassId, this);
Device *device = new Device(plugin->pluginId(), id, deviceClassId, this);
device->setName(deviceClass.name());
device->setParams(params);
if (setupDevice(device)) {

View File

@ -49,7 +49,8 @@ public:
DeviceErrorActionTypeNotFound,
DeviceErrorMissingParameter,
DeviceErrorPluginNotFound,
DeviceErrorSetupFailed
DeviceErrorSetupFailed,
DeviceErrorDuplicateUuid
};
explicit DeviceManager(QObject *parent = 0);
@ -59,7 +60,7 @@ public:
QList<DeviceClass> supportedDevices() const;
QList<Device*> configuredDevices() const;
DeviceError addConfiguredDevice(const QUuid &deviceClassId, const QVariantMap &params);
DeviceError addConfiguredDevice(const QUuid &deviceClassId, const QVariantMap &params, const QUuid id = QUuid::createUuid());
Device* findConfiguredDevice(const QUuid &id) const;
QList<Device*> findConfiguredDevices(const QUuid &deviceClassId) const;

View File

@ -119,7 +119,7 @@ bool DevicePluginMock::deviceCreated(Device *device)
m_daemons.insert(device, daemon);
if (!daemon->isListening()) {
qDebug() << "couldn't setup mockdevice";
qDebug() << "HTTP port opening failed.";
return false;
}

View File

@ -66,6 +66,7 @@ DeviceHandler::DeviceHandler(QObject *parent) :
setParams("AddConfiguredDevice", params);
returns.insert("success", "bool");
returns.insert("errorMessage", "string");
returns.insert("deviceId", "uuid");
setReturns("AddConfiguredDevice", returns);
params.clear(); returns.clear();
@ -94,6 +95,26 @@ DeviceHandler::DeviceHandler(QObject *parent) :
returns.insert("actionTypes", actions);
setReturns("GetActionTypes", returns);
params.clear(); returns.clear();
setDescription("GetStateTypes", "Get state types for a specified deviceClassId.");
params.insert("deviceClassId", "uuid");
setParams("GetStateTypes", params);
QVariantList states;
states.append(JsonTypes::stateTypeRef());
returns.insert("stateTypes", actions);
setReturns("GetStateTypes", returns);
params.clear(); returns.clear();
setDescription("GetStateValue", "Get the value of the given device and the given stateType");
params.insert("deviceId", "uuid");
params.insert("stateTypeId", "uuid");
setParams("GetStateValue", params);
returns.insert("success", "bool");
returns.insert("errorMessage", "string");
returns.insert("value", "variant");
setReturns("GetStateValue", returns);
// Notifications
params.clear(); returns.clear();
setDescription("StateChanged", "Emitted whenever a State of a device changes.");
params.insert("deviceId", "uuid");
@ -149,11 +170,13 @@ QVariantMap DeviceHandler::AddConfiguredDevice(const QVariantMap &params)
{
QUuid deviceClass = params.value("deviceClassId").toUuid();
QVariantMap deviceParams = params.value("deviceParams").toMap();
DeviceManager::DeviceError status = GuhCore::instance()->deviceManager()->addConfiguredDevice(deviceClass, deviceParams);
QUuid newDeviceId = QUuid::createUuid();
DeviceManager::DeviceError status = GuhCore::instance()->deviceManager()->addConfiguredDevice(deviceClass, deviceParams, newDeviceId);
QVariantMap returns;
switch(status) {
case DeviceManager::DeviceErrorNoError:
returns.insert("success", true);
returns.insert("deviceId", newDeviceId);
break;
case DeviceManager::DeviceErrorDeviceClassNotFound:
returns.insert("errorMessage", "Error creating device. Device class not found.");
@ -212,18 +235,47 @@ QVariantMap DeviceHandler::GetActionTypes(const QVariantMap &params) const
return returns;
}
QVariantMap DeviceHandler::GetStateTypes(const QVariantMap &params) const
{
QVariantMap returns;
QVariantList stateList;
DeviceClass deviceClass = GuhCore::instance()->deviceManager()->findDeviceClass(params.value("deviceClassId").toUuid());
foreach (const StateType &stateType, deviceClass.states()) {
stateList.append(JsonTypes::packStateType(stateType));
}
returns.insert("stateTypes", stateList);
return returns;
}
QVariantMap DeviceHandler::GetStateValue(const QVariantMap &params) const
{
QVariantMap returns;
Device *device = GuhCore::instance()->deviceManager()->findConfiguredDevice(params.value("deviceId").toUuid());
if (!device) {
returns.insert("success", false);
returns.insert("errorMessage", "No such device");
return returns;
}
if (!device->hasState(params.value("stateTypeId").toUuid())) {
returns.insert("success", false);
returns.insert("errorMessage", QString("Device %1 %2 doesn't have such a state.").arg(device->name()).arg(device->id().toString()));
return returns;
}
QVariant stateValue = device->stateValue(params.value("stateTypeId").toUuid());
returns.insert("success", true);
returns.insert("value", stateValue);
return returns;
}
void DeviceHandler::deviceStateChanged(Device *device, const QUuid &stateTypeId, const QVariant &value)
{
qDebug() << "************************************";
QVariantMap notification;
notification.insert("notification", "Device.StateChanged");
QVariantMap params;
params.insert("deviceId", device->id());
params.insert("stateTypeId", stateTypeId);
params.insert("value", value);
notification.insert("params", params);
emit StateChanged(notification);
emit StateChanged(params);
}

View File

@ -43,6 +43,10 @@ public:
Q_INVOKABLE QVariantMap GetActionTypes(const QVariantMap &params) const;
Q_INVOKABLE QVariantMap GetStateTypes(const QVariantMap &params) const;
Q_INVOKABLE QVariantMap GetStateValue(const QVariantMap &params) const;
signals:
void StateChanged(const QVariantMap &params);

View File

@ -32,7 +32,6 @@ QVariantMap JsonHandler::introspect(QMetaMethod::MethodType type)
QVariantMap data;
for (int i = 0; i < metaObject()->methodCount(); ++i) {
QMetaMethod method = metaObject()->method(i);
qDebug() << "checking method" << method.methodType() << method.methodSignature() << method.name();
if (method.methodType() != type) {
continue;

View File

@ -70,8 +70,8 @@ JsonRPCServer::JsonRPCServer(QObject *parent):
params.clear(); returns.clear();
setDescription("SetNotificationStatus", "Enable/Disable notifications for this connections.");
setParams("SetNotificationStatus", params);
returns.insert("status", "string");
// returns.insert("enabled", "bool");
returns.insert("success", "bool");
returns.insert("enabled", "bool");
setReturns("SetNotificationStatus", returns);
// Now set up the logic
@ -124,7 +124,8 @@ QVariantMap JsonRPCServer::SetNotificationStatus(const QVariantMap &params)
// qDebug() << "got client socket" << clientId;
m_clients[clientId] = params.value("enabled").toBool();
QVariantMap returns;
returns.insert("status", "success");
returns.insert("success", "true");
returns.insert("errorMessage", "No error");
returns.insert("enabled", m_clients[clientId]);
return returns;
}

View File

@ -179,6 +179,16 @@ QVariantMap JsonTypes::packAction(const Action &action)
return variant;
}
QVariantMap JsonTypes::packStateType(const StateType &stateType)
{
QVariantMap variantMap;
variantMap.insert("id", stateType.id());
variantMap.insert("name", stateType.name());
variantMap.insert("type", QVariant::typeToName(stateType.type()));
variantMap.insert("defaultValue", stateType.defaultValue());
return variantMap;
}
QVariantMap JsonTypes::packDeviceClass(const DeviceClass &deviceClass)
{
QVariantMap variant;
@ -186,30 +196,15 @@ QVariantMap JsonTypes::packDeviceClass(const DeviceClass &deviceClass)
variant.insert("id", deviceClass.id());
QVariantList stateTypes;
foreach (const StateType &stateType, deviceClass.states()) {
QVariantMap stateMap;
stateMap.insert("id", stateType.id().toString());
stateMap.insert("name", stateType.name());
stateMap.insert("type", QVariant::typeToName(stateType.type()));
stateTypes.append(stateMap);
stateTypes.append(packStateType(stateType));
}
QVariantList eventTypes;
foreach (const EventType &eventType, deviceClass.events()) {
QVariantMap eventMap;
eventMap.insert("id", eventType.id().toString());
eventMap.insert("name", eventType.name());
eventMap.insert("params", eventType.parameters());
eventTypes.append(eventMap);
eventTypes.append(packEventType(eventType));
}
QVariantList actionTypes;
foreach (const ActionType &actionType, deviceClass.actions()) {
QVariantMap actionMap;
actionMap.insert("id", actionType.id().toString());
actionMap.insert("name", actionType.name());
actionMap.insert("params", actionType.parameters());
actionTypes.append(actionMap);
actionTypes.append(packActionType(actionType));
}
variant.insert("params", deviceClass.params());
variant.insert("states", stateTypes);

View File

@ -79,6 +79,7 @@ public:
static QVariantMap packEvent(const Event &event);
static QVariantMap packActionType(const ActionType &actionType);
static QVariantMap packAction(const Action &action);
static QVariantMap packStateType(const StateType &stateType);
static QVariantMap packDeviceClass(const DeviceClass &deviceClass);
static QVariantMap packPlugin(DevicePlugin *plugin);
static QVariantMap packDevice(Device *device);

View File

@ -44,6 +44,8 @@ RulesHandler::RulesHandler(QObject *parent) :
actions.append(JsonTypes::actionRef());
params.insert("actions", actions);
setParams("AddRule", params);
returns.insert("success", "bool");
returns.insert("errorMessage", "string");
setReturns("AddRule", returns);
params.clear(); returns.clear();

View File

@ -66,8 +66,3 @@ bool MockTcpServer::stopServer()
qDebug() << "should stop server";
return true;
}
void MockTcpServer::sendToAll(QByteArray data)
{
qDebug() << "should send to all clients:" << data;
}

View File

@ -46,7 +46,6 @@ signals:
public slots:
bool startServer();
bool stopServer();
void sendToAll(QByteArray data);
private:
static QList<MockTcpServer*> s_allServers;

View File

@ -22,39 +22,50 @@
#include <QtTest/QtTest>
#include <QCoreApplication>
//#include <QSignalSpy>
#include <QTcpSocket>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
Q_IMPORT_PLUGIN(DevicePluginMock)
int mockDevice1Port = 1337;
int mockDevice2Port = 7331;
class TestJSONRPC: public QObject
{
Q_OBJECT
private slots:
void initTestcase();
void cleanupTestCase();
void introspect();
void version();
void getSupportedDevices();
void enableDisableNotifications_data();
void enableDisableNotifications();
void version();
void stateChangeEmitsNotifications();
private:
QVariant injectAndWait(const QByteArray data);
QVariant injectAndWait(const QString &method, const QVariantMap &params);
private:
MockTcpServer *m_mockTcpServer;
QUuid m_clientId;
int m_commandId;
QUuid m_mockDeviceId;
};
void TestJSONRPC::initTestcase()
{
QCoreApplication::instance()->setOrganizationName("guh-test");
qDebug() << "creating core";
m_commandId = 0;
GuhCore::instance();
qDebug() << "creating spy";
// Wait for the DeviceManager to signal that it has loaded plugins and everything
QSignalSpy spy(GuhCore::instance()->deviceManager(), SIGNAL(loaded()));
@ -65,13 +76,38 @@ void TestJSONRPC::initTestcase()
QCOMPARE(MockTcpServer::servers().count(), 1);
m_mockTcpServer = MockTcpServer::servers().first();
m_clientId = QUuid::createUuid();
// Lets add one instance of the mockdevice
QVariantMap params;
params.insert("deviceClassId", "{753f0d32-0468-4d08-82ed-1964aab03298}");
QVariantMap deviceParams;
deviceParams.insert("httpport", mockDevice1Port);
params.insert("deviceParams", deviceParams);
QVariant response = injectAndWait("Devices.AddConfiguredDevice", params);
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
m_mockDeviceId = response.toMap().value("params").toMap().value("deviceId").toUuid();
QVERIFY2(!m_mockDeviceId.isNull(), "Newly created mock device must not be null.");
}
QVariant TestJSONRPC::injectAndWait(const QByteArray data)
void TestJSONRPC::cleanupTestCase()
{
QSettings settings;
settings.clear();
}
QVariant TestJSONRPC::injectAndWait(const QString &method, const QVariantMap &params = QVariantMap())
{
QVariantMap call;
call.insert("id", m_commandId++);
call.insert("method", method);
call.insert("params", params);
QJsonDocument jsonDoc = QJsonDocument::fromVariant(call);
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
m_mockTcpServer->injectData(m_clientId, data);
m_mockTcpServer->injectData(m_clientId, jsonDoc.toJson());
if (spy.count() == 0) {
spy.wait();
@ -79,7 +115,7 @@ QVariant TestJSONRPC::injectAndWait(const QByteArray data)
// Make sure the response it a valid JSON string
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.takeFirst().last().toByteArray(), &error);
jsonDoc = QJsonDocument::fromJson(spy.takeFirst().last().toByteArray(), &error);
return jsonDoc.toVariant();
}
@ -106,13 +142,13 @@ void TestJSONRPC::introspect()
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().last().toByteArray(), &error);
QCOMPARE(error.error, QJsonParseError::NoError);
// Make sure the response's id is the same as our command
// Make sure the response\"s id is the same as our command
QCOMPARE(jsonDoc.toVariant().toMap().value("id").toInt(), 42);
}
void TestJSONRPC::getSupportedDevices()
{
QVariant supportedDevices = injectAndWait("{\"id\":1, \"method\":\"Devices.GetSupportedDevices\"}");
QVariant supportedDevices = injectAndWait("Devices.GetSupportedDevices");
// Make sure there is exactly 1 supported device class with the name Mock Wifi Device
QCOMPARE(supportedDevices.toMap().value("params").toMap().value("deviceClasses").toList().count(), 1);
@ -132,19 +168,81 @@ void TestJSONRPC::enableDisableNotifications()
{
QFETCH(QString, enabled);
QVariant response = injectAndWait(QString("{\"id\":1, \"method\":\"JSONRPC.SetNotificationStatus\", \"params\":{\"enabled\": " + enabled + " }}").toLatin1());
QVariantMap params;
params.insert("enabled", enabled);
QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", params);
QCOMPARE(response.toMap().value("params").toMap().value("status").toString(), QString("success"));
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
QCOMPARE(response.toMap().value("params").toMap().value("enabled").toString(), enabled);
}
void TestJSONRPC::version()
{
QVariant response = injectAndWait("{\"id\":1, \"method\":\"JSONRPC.Version\"}");
QVariant response = injectAndWait("JSONRPC.Version");
QCOMPARE(response.toMap().value("params").toMap().value("version").toString(), QString("0.0.0"));
}
void TestJSONRPC::stateChangeEmitsNotifications()
{
QVariantMap params;
params.insert("enabled", true);
QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", params);
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
// Setup connection to mock client
QNetworkAccessManager nam;
QSignalSpy mockSpy(&nam, SIGNAL(finished()));
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
// trigger state change in mock device
int newVal = 1111;
QUuid stateTypeId("80baec19-54de-4948-ac46-31eabfaceb83");
QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(mockDevice1Port).arg(stateTypeId.toString()).arg(newVal)));
QNetworkReply *reply = nam.get(request);
reply->deleteLater();
// Lets wait for the notification
clientSpy.wait();
QCOMPARE(clientSpy.count(), 1);
// Make sure the notification contains all the stuff we expect
QJsonDocument jsonDoc = QJsonDocument::fromJson(clientSpy.at(0).at(1).toByteArray());
QCOMPARE(jsonDoc.toVariant().toMap().value("notification").toString(), QString("Devices.StateChanged"));
QCOMPARE(jsonDoc.toVariant().toMap().value("params").toMap().value("stateTypeId").toUuid(), stateTypeId);
QCOMPARE(jsonDoc.toVariant().toMap().value("params").toMap().value("value").toInt(), newVal);
// Now turn off notifications
params.clear();
params.insert("enabled", false);
response = injectAndWait("JSONRPC.SetNotificationStatus", params);
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
// Fire the a statechange once again
clientSpy.clear();
newVal = 42;
request.setUrl(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(mockDevice1Port).arg(stateTypeId.toString()).arg(newVal)));
reply = nam.get(request);
reply->deleteLater();
// Lets wait a max of 100ms for the notification
clientSpy.wait(100);
// but make sure it doesn't come
QCOMPARE(clientSpy.count(), 0);
// Now check that the state has indeed changed even though we didn't get a notification
params.clear();
params.insert("deviceId", m_mockDeviceId);
params.insert("stateTypeId", stateTypeId);
response = injectAndWait("Devices.GetStateValue", params);
qDebug() << "response" << response;
QCOMPARE(response.toMap().value("params").toMap().value("value").toInt(), newVal);
}
QTEST_MAIN(TestJSONRPC)
#include "testjsonrpc.moc"

View File

@ -0,0 +1,7 @@
#!/bin/bash
if [ -z $3 ]; then
echo "usage: $0 host deviceId stateTypeId"
else
(echo '{"id":1, "method":"Devices.GetDeviceState", "params":{"deviceId":"'$2'", "stateTypeId":"'$3'"}}'; sleep 1) | nc $1 1234
fi

7
tests/scripts/getstatetypes.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
if [ -z $2 ]; then
echo "usage: $0 host deviceClassId"
else
(echo '{"id":1, "method":"Devices.GetStateTypes", "params":{"deviceClassId":"'$2'"}}'; sleep 1) | nc $1 1234
fi