Add deprecation warnings

This commit is contained in:
Michael Zanetti 2019-11-04 11:47:16 +01:00
parent 5be6e6e19d
commit fa1cd3605c
7 changed files with 313 additions and 17 deletions

View File

@ -385,7 +385,7 @@ DeviceHandler::DeviceHandler(QObject *parent) :
params.clear(); returns.clear();
description = "Emitted whenever an Event is triggered.";
params.insert("event", objectRef<Event>());
registerNotification("EventTriggered", description, params, "Please use Devices.EventTriggered instead.");
registerNotification("EventTriggered", description, params);
connect(NymeaCore::instance(), &NymeaCore::eventTriggered, this, [this](const Event &event){
QVariantMap params;
params.insert("event", pack(event));

View File

@ -467,13 +467,17 @@ bool JsonRPCServerImplementation::registerExperienceHandler(JsonHandler *handler
/*! Send a JSON success response to the client with the given \a clientId,
* \a commandId and \a params to the inerted \l{TransportInterface}.
*/
void JsonRPCServerImplementation::sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap &params)
void JsonRPCServerImplementation::sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap &params, const QString &deprecationWarning)
{
QVariantMap response;
response.insert("id", commandId);
response.insert("status", "success");
response.insert("params", params);
if (!deprecationWarning.isEmpty()) {
response.insert("deprecationWarning", deprecationWarning);
}
QByteArray data = QJsonDocument::fromVariant(response).toJson(QJsonDocument::Compact);
qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data;
interface->sendData(clientId, data);
@ -694,7 +698,15 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac
Q_ASSERT_X((targetNamespace == "JSONRPC" && method == "Introspect") || validator.validateReturns(reply->data(), targetNamespace + '.' + method, m_api).success(),
validator.result().where().toUtf8(),
validator.result().errorString().toUtf8() + "\nReturn value:\n" + QJsonDocument::fromVariant(reply->data()).toJson());
sendResponse(interface, clientId, commandId, reply->data());
QString deprecationWarning;
if (m_api.value("methods").toMap().value(targetNamespace + '.' + method).toMap().contains("deprecated")) {
deprecationWarning = m_api.value("methods").toMap().value(targetNamespace + '.' + method).toMap().value("deprecated").toString();
qCWarning(dcJsonRpc()) << "Client uses deprecated API. Please update client implementation!";
qCWarning(dcJsonRpc()) << targetNamespace + '.' + method + ':' << deprecationWarning;
}
sendResponse(interface, clientId, commandId, reply->data(), deprecationWarning);
reply->deleteLater();
}
}
@ -704,6 +716,16 @@ void JsonRPCServerImplementation::sendNotification(const QVariantMap &params)
JsonHandler *handler = qobject_cast<JsonHandler *>(sender());
QMetaMethod method = handler->metaObject()->method(senderSignalIndex());
QList<QUuid> clientsToBeNotified;
foreach (const QUuid &clientId, m_clientNotifications.keys()) {
if (m_clientNotifications.value(clientId).contains(handler->name())) {
clientsToBeNotified.append(clientId);
}
}
if (clientsToBeNotified.isEmpty()) {
return;
}
QVariantMap notification;
notification.insert("id", m_notificationId++);
notification.insert("notification", handler->name() + "." + method.name());
@ -713,14 +735,20 @@ void JsonRPCServerImplementation::sendNotification(const QVariantMap &params)
Q_ASSERT_X(validator.validateNotificationParams(params, handler->name() + '.' + method.name(), m_api).success(),
validator.result().where().toUtf8(),
validator.result().errorString().toUtf8());
if (m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().contains("deprecated")) {
QString deprecationMessage = m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().value("deprecated").toString();
qCWarning(dcJsonRpc()) << "Client uses deprecated API. Please update client implementation!";
qCWarning(dcJsonRpc()) << handler->name() + '.' + method.name() + ':' << deprecationMessage;
notification.insert("deprecationWarning", deprecationMessage);
}
QByteArray data = QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact);
qCDebug(dcJsonRpc()) << "Sending notification:" << handler->name() + "." + method.name();
qCDebug(dcJsonRpcTraffic()) << "Notification content:" << data;
foreach (const QUuid &clientId, m_clientNotifications.keys()) {
if (m_clientNotifications.value(clientId).contains(handler->name())) {
m_clientTransports.value(clientId)->sendData(clientId, data);
}
foreach (const QUuid &clientId, clientsToBeNotified) {
qCDebug(dcJsonRpc()) << "Sending notification:" << handler->name() + "." + method.name();
m_clientTransports.value(clientId)->sendData(clientId, data);
}
}
@ -735,10 +763,19 @@ void JsonRPCServerImplementation::asyncReplyFinished()
}
if (!reply->timedOut()) {
JsonValidator validator;
Q_ASSERT_X(validator.validateReturns(reply->data(), reply->handler()->name() + '.' + reply->method(), m_api).success()
QString method = reply->handler()->name() + '.' + reply->method();
Q_ASSERT_X(validator.validateReturns(reply->data(), method, m_api).success()
,validator.result().where().toUtf8()
,validator.result().errorString().toUtf8() + "\nReturn value:\n" + QJsonDocument::fromVariant(reply->data()).toJson());
sendResponse(interface, reply->clientId(), reply->commandId(), reply->data());
QString deprecationWarning;
if (m_api.value("methods").toMap().value(method).toMap().contains("deprecated")) {
deprecationWarning = m_api.value("methods").toMap().value(method).toMap().value("deprecated").toString();
qCWarning(dcJsonRpc()) << "Client uses deprecated API. Please update client implementation!";
qCWarning(dcJsonRpc()) << method + ':' << deprecationWarning;
}
sendResponse(interface, reply->clientId(), reply->commandId(), reply->data(), deprecationWarning);
} else {
qCWarning(dcJsonRpc()) << "RPC call timed out:" << reply->handler()->name() << ":" << reply->method();
sendErrorResponse(interface, reply->clientId(), reply->commandId(), "Command timed out");

View File

@ -78,7 +78,7 @@ private:
bool registerHandler(JsonHandler *handler);
QHash<QString, JsonHandler *> handlers() const;
void sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap &params = QVariantMap());
void sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap &params = QVariantMap(), const QString &deprecationWarning = QString());
void sendErrorResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error);
void sendUnauthorizedResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error);
QVariantMap createWelcomeMessage(TransportInterface *interface, const QUuid &clientId) const;

View File

@ -307,7 +307,6 @@ QVariant JsonHandler::pack(const QMetaObject &metaObject, const void *value) con
// Manually converting QList<int>... Only QVariantList is known to the meta system
if (propertyTypeName.startsWith("QList<int>")) {
qWarning() << "Packing list" << metaProperty.name() << propertyValue.toList();
QVariantList list;
foreach (int entry, propertyValue.value<QList<int>>()) {
list << entry;
@ -350,7 +349,6 @@ QVariant JsonHandler::unpack(const QMetaObject &metaObject, const QVariant &valu
// If it's a list object, loop over count
if (m_listMetaObjects.contains(typeName)) {
qWarning() << "** Unpacking" << typeName;
if (value.type() != QVariant::List) {
qCWarning(dcJsonRpc()) << "Cannot unpack" << typeName << ". Value is not in list format:" << value;
return QVariant();
@ -367,7 +365,6 @@ QVariant JsonHandler::unpack(const QMetaObject &metaObject, const QVariant &valu
foreach (const QVariant &variant, list) {
QVariant value = unpack(entryMetaObject, variant);
qWarning() << "Putting" << value << putMethod.name() << ptr << typeId;
putMethod.invokeOnGadget(ptr, Q_ARG(QVariant, value));
}
@ -378,7 +375,6 @@ QVariant JsonHandler::unpack(const QMetaObject &metaObject, const QVariant &valu
// if it's an object, loop over all properties
if (m_metaObjects.contains(typeName)) {
qWarning() << "*** Unpacking" << typeName;
QVariantMap map = value.toMap();
int typeId = QMetaType::type(metaObject.className());
Q_ASSERT_X(typeId != 0, this->metaObject()->className(), QString("Cannot handle unregistered meta type %1").arg(typeName).toUtf8());
@ -403,7 +399,6 @@ QVariant JsonHandler::unpack(const QMetaObject &metaObject, const QVariant &valu
// recurse into child lists
if (m_listMetaObjects.contains(propertyTypeName)) {
QMetaObject propertyMetaObject = m_listMetaObjects.value(propertyTypeName);
qWarning() << "Entering list object" << propertyTypeName << propertyMetaObject.className();
metaProperty.writeOnGadget(ptr, unpack(propertyMetaObject, variant));
continue;
}

View File

@ -304,6 +304,7 @@
},
"methods": {
"Actions.ExecuteAction": {
"deprecated": "Please use Devices.ExecuteAction instead.",
"description": "Execute a single action.",
"params": {
"actionTypeId": "Uuid",
@ -316,6 +317,7 @@
}
},
"Actions.ExecuteBrowserItem": {
"deprecated": "Please use Devices.ExecuteBrowserItem instead.",
"description": "Execute the item identified by itemId on the given device.",
"params": {
"deviceId": "Uuid",
@ -326,6 +328,7 @@
}
},
"Actions.ExecuteBrowserItemAction": {
"deprecated": "Please use Devices.ExecuteBrowserItem instead.",
"description": "Execute the action for the browser item identified by actionTypeId and the itemId on the given device.",
"params": {
"actionTypeId": "Uuid",
@ -338,7 +341,8 @@
}
},
"Actions.GetActionType": {
"description": "Get the ActionType for the given ActionTypeId",
"deprecated": "Please use the Devices namespace instead.",
"description": "Get the ActionType for the given ActionTypeId.",
"params": {
"actionTypeId": "Uuid"
},
@ -599,6 +603,40 @@
"deviceError": "$ref:DeviceError"
}
},
"Devices.ExecuteAction": {
"description": "Execute a single action.",
"params": {
"actionTypeId": "Uuid",
"deviceId": "Uuid",
"o:params": "$ref:ParamList"
},
"returns": {
"deviceError": "$ref:DeviceError",
"o:displayMessage": "String"
}
},
"Devices.ExecuteBrowserItem": {
"description": "Execute the item identified by itemId on the given device.",
"params": {
"deviceId": "Uuid",
"itemId": "String"
},
"returns": {
"deviceError": "$ref:DeviceError"
}
},
"Devices.ExecuteBrowserItemAction": {
"description": "Execute the action for the browser item identified by actionTypeId and the itemId on the given device.",
"params": {
"actionTypeId": "Uuid",
"deviceId": "Uuid",
"itemId": "String",
"o:params": "$ref:ParamList"
},
"returns": {
"deviceError": "$ref:DeviceError"
}
},
"Devices.GetActionTypes": {
"description": "Get action types for a specified deviceClassId.",
"params": {
@ -784,6 +822,7 @@
}
},
"Events.GetEventType": {
"deprecated": "Please use the Devices namespace instead.",
"description": "Get the EventType for the given eventTypeId.",
"params": {
"eventTypeId": "Uuid"
@ -1185,6 +1224,7 @@
}
},
"States.GetStateType": {
"deprecated": "Please use the Devices namespace instead.",
"description": "Get the StateType for the given stateTypeId.",
"params": {
"stateTypeId": "Uuid"
@ -1441,6 +1481,12 @@
"value": "Variant"
}
},
"Devices.EventTriggered": {
"description": "Emitted whenever an Event is triggered.",
"params": {
"event": "$ref:Event"
}
},
"Devices.PluginConfigurationChanged": {
"description": "Emitted whenever a plugin's configuration is changed.",
"params": {
@ -1457,6 +1503,7 @@
}
},
"Events.EventTriggered": {
"deprecated": "Please use Devices.EventTriggered instead.",
"description": "Emitted whenever an Event is triggered.",
"params": {
"event": "$ref:Event"

View File

@ -26,6 +26,8 @@
#include "devices/devicediscoveryinfo.h"
#include "devices/devicesetupinfo.h"
#include "servers/mocktcpserver.h"
using namespace nymeaserver;
class TestDevices : public NymeaTestBase
@ -117,6 +119,14 @@ private slots:
void testExecuteBrowserItemAction_data();
void testExecuteBrowserItemAction();
void executeAction_data();
void executeAction();
void triggerEvent();
void triggerStateChangeEvent();
void params();
// Keep those at last as they will remove devices
void removeDevice_data();
void removeDevice();
@ -1777,6 +1787,181 @@ void TestDevices::testExecuteBrowserItemAction()
}
void TestDevices::executeAction_data()
{
QTest::addColumn<DeviceId>("deviceId");
QTest::addColumn<ActionTypeId>("actionTypeId");
QTest::addColumn<QVariantList>("actionParams");
QTest::addColumn<Device::DeviceError>("error");
QVariantList params;
QVariantMap param1;
param1.insert("paramTypeId", mockWithParamsActionParam1ParamTypeId);
param1.insert("value", 5);
params.append(param1);
QVariantMap param2;
param2.insert("paramTypeId", mockWithParamsActionParam2ParamTypeId);
param2.insert("value", true);
params.append(param2);
QTest::newRow("valid action") << m_mockDeviceId << mockWithParamsActionTypeId << params << Device::DeviceErrorNoError;
QTest::newRow("invalid deviceId") << DeviceId::createDeviceId() << mockWithParamsActionTypeId << params << Device::DeviceErrorDeviceNotFound;
QTest::newRow("invalid actionTypeId") << m_mockDeviceId << ActionTypeId::createActionTypeId() << params << Device::DeviceErrorActionTypeNotFound;
QTest::newRow("missing params") << m_mockDeviceId << mockWithParamsActionTypeId << QVariantList() << Device::DeviceErrorMissingParameter;
QTest::newRow("async action") << m_mockDeviceId << mockAsyncActionTypeId << QVariantList() << Device::DeviceErrorNoError;
QTest::newRow("broken action") << m_mockDeviceId << mockFailingActionTypeId << QVariantList() << Device::DeviceErrorSetupFailed;
QTest::newRow("async broken action") << m_mockDeviceId << mockAsyncFailingActionTypeId << QVariantList() << Device::DeviceErrorSetupFailed;
}
void TestDevices::executeAction()
{
QFETCH(DeviceId, deviceId);
QFETCH(ActionTypeId, actionTypeId);
QFETCH(QVariantList, actionParams);
QFETCH(Device::DeviceError, error);
QVariantMap params;
params.insert("actionTypeId", actionTypeId);
params.insert("deviceId", deviceId);
params.insert("params", actionParams);
QVariant response = injectAndWait("Devices.ExecuteAction", params);
qDebug() << "executeActionresponse" << response;
verifyError(response, "deviceError", enumValueName(error));
// Fetch action execution history from mock device
QNetworkAccessManager nam;
QSignalSpy spy(&nam, SIGNAL(finished(QNetworkReply*)));
QNetworkRequest request(QUrl(QString("http://localhost:%1/actionhistory").arg(m_mockDevice1Port)));
QNetworkReply *reply = nam.get(request);
spy.wait();
QCOMPARE(spy.count(), 1);
reply->deleteLater();
QByteArray data = reply->readAll();
if (error == Device::DeviceErrorNoError) {
QVERIFY2(actionTypeId == ActionTypeId(data), QString("ActionTypeId mismatch. Got %1, Expected: %2")
.arg(ActionTypeId(data).toString()).arg(actionTypeId.toString()).toLatin1().data());
} else {
QVERIFY2(data.length() == 0, QString("Data is %1, should be empty.").arg(QString(data)).toLatin1().data());
}
// cleanup for the next run
spy.clear();
request.setUrl(QUrl(QString("http://localhost:%1/clearactionhistory").arg(m_mockDevice1Port)));
reply = nam.get(request);
spy.wait();
QCOMPARE(spy.count(), 1);
reply->deleteLater();
spy.clear();
request.setUrl(QUrl(QString("http://localhost:%1/actionhistory").arg(m_mockDevice1Port)));
reply = nam.get(request);
spy.wait();
QCOMPARE(spy.count(), 1);
reply->deleteLater();
data = reply->readAll();
qDebug() << "cleared data:" << data;
}
void TestDevices::triggerEvent()
{
enableNotifications({"Devices"});
QList<Device*> devices = NymeaCore::instance()->deviceManager()->findConfiguredDevices(mockDeviceClassId);
QVERIFY2(devices.count() > 0, "There needs to be at least one configured Mock Device for this test");
Device *device = devices.first();
QSignalSpy spy(NymeaCore::instance(), SIGNAL(eventTriggered(const Event&)));
QSignalSpy notificationSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
// Setup connection to mock client
QNetworkAccessManager nam;
// trigger event in mock device
int port = device->paramValue(mockDeviceHttpportParamTypeId).toInt();
QNetworkRequest request(QUrl(QString("http://localhost:%1/generateevent?eventtypeid=%2").arg(port).arg(mockEvent1EventTypeId.toString())));
QNetworkReply *reply = nam.get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
// Lets wait for the notification
spy.wait();
QVERIFY(spy.count() > 0);
for (int i = 0; i < spy.count(); i++ ){
Event event = spy.at(i).at(0).value<Event>();
if (event.deviceId() == device->id()) {
// Make sure the event contains all the stuff we expect
QCOMPARE(event.eventTypeId(), mockEvent1EventTypeId);
}
}
// Check for the notification on JSON API
QVariantList notifications;
notifications = checkNotifications(notificationSpy, "Devices.EventTriggered");
QVERIFY2(notifications.count() == 1, "Should get Devices.EventTriggered notification");
QVariantMap notificationContent = notifications.first().toMap().value("params").toMap();
QCOMPARE(notificationContent.value("event").toMap().value("deviceId").toUuid().toString(), device->id().toString());
QCOMPARE(notificationContent.value("event").toMap().value("eventTypeId").toUuid().toString(), mockEvent1EventTypeId.toString());
}
void TestDevices::triggerStateChangeEvent()
{
enableNotifications({"Devices"});
QList<Device*> devices = NymeaCore::instance()->deviceManager()->findConfiguredDevices(mockDeviceClassId);
QVERIFY2(devices.count() > 0, "There needs to be at least one configured Mock Device for this test");
Device *device = devices.first();
QSignalSpy spy(NymeaCore::instance(), SIGNAL(eventTriggered(const Event&)));
QSignalSpy notificationSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
// Setup connection to mock client
QNetworkAccessManager nam;
// trigger state changed event in mock device
int port = device->paramValue(mockDeviceHttpportParamTypeId).toInt();
QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(port).arg(mockIntStateTypeId.toString()).arg(11)));
QNetworkReply *reply = nam.get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
// Lets wait for the notification
spy.wait();
QVERIFY(spy.count() > 0);
for (int i = 0; i < spy.count(); i++ ){
Event event = spy.at(i).at(0).value<Event>();
if (event.deviceId() == device->id()) {
// Make sure the event contains all the stuff we expect
QCOMPARE(event.eventTypeId().toString(), mockIntStateTypeId.toString());
QCOMPARE(event.param(ParamTypeId(mockIntStateTypeId.toString())).value().toInt(), 11);
}
}
// Check for the notification on JSON API
QVariantList notifications;
notifications = checkNotifications(notificationSpy, "Devices.EventTriggered");
QVERIFY2(notifications.count() == 1, "Should get Devices.EventTriggered notification");
QVariantMap notificationContent = notifications.first().toMap().value("params").toMap();
QCOMPARE(notificationContent.value("event").toMap().value("deviceId").toUuid().toString(), device->id().toString());
QCOMPARE(notificationContent.value("event").toMap().value("eventTypeId").toUuid().toString(), mockIntEventTypeId.toString());
}
void TestDevices::params()
{
Event event;
ParamList params;
ParamTypeId id = ParamTypeId::createParamTypeId();
Param p(id, "foo bar");
params.append(p);
event.setParams(params);
QVERIFY(event.param(id).value().toString() == "foo bar");
QVERIFY(!event.param(ParamTypeId::createParamTypeId()).value().isValid());
}
#include "testdevices.moc"
QTEST_MAIN(TestDevices)

View File

@ -22,6 +22,8 @@
#include "nymeatestbase.h"
#include "nymeacore.h"
#include "servers/mocktcpserver.h"
using namespace nymeaserver;
class TestEvents: public NymeaTestBase
@ -40,11 +42,14 @@ private slots:
void TestEvents::triggerEvent()
{
enableNotifications({"Events"});
QList<Device*> devices = NymeaCore::instance()->deviceManager()->findConfiguredDevices(mockDeviceClassId);
QVERIFY2(devices.count() > 0, "There needs to be at least one configured Mock Device for this test");
Device *device = devices.first();
QSignalSpy spy(NymeaCore::instance(), SIGNAL(eventTriggered(const Event&)));
QSignalSpy notificationSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
// Setup connection to mock client
QNetworkAccessManager nam;
@ -65,15 +70,28 @@ void TestEvents::triggerEvent()
QCOMPARE(event.eventTypeId(), mockEvent1EventTypeId);
}
}
// Check for the notification on JSON API
QVariantList notifications;
notifications = checkNotifications(notificationSpy, "Events.EventTriggered");
QVERIFY2(notifications.count() == 1, "Should get Events.EventTriggered notification");
QVERIFY2(notifications.first().toMap().contains("deprecationWarning"), "Deprecation warning not included in notification");
QVariantMap notificationContent = notifications.first().toMap().value("params").toMap();
QCOMPARE(notificationContent.value("event").toMap().value("deviceId").toUuid().toString(), device->id().toString());
QCOMPARE(notificationContent.value("event").toMap().value("eventTypeId").toUuid().toString(), mockEvent1EventTypeId.toString());
}
void TestEvents::triggerStateChangeEvent()
{
enableNotifications({"Events"});
QList<Device*> devices = NymeaCore::instance()->deviceManager()->findConfiguredDevices(mockDeviceClassId);
QVERIFY2(devices.count() > 0, "There needs to be at least one configured Mock Device for this test");
Device *device = devices.first();
QSignalSpy spy(NymeaCore::instance(), SIGNAL(eventTriggered(const Event&)));
QSignalSpy notificationSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
// Setup connection to mock client
QNetworkAccessManager nam;
@ -95,6 +113,17 @@ void TestEvents::triggerStateChangeEvent()
QCOMPARE(event.param(ParamTypeId(mockIntStateTypeId.toString())).value().toInt(), 11);
}
}
// Check for the notification on JSON API
QVariantList notifications;
notifications = checkNotifications(notificationSpy, "Events.EventTriggered");
QVERIFY2(notifications.count() == 1, "Should get Devices.EventTriggered notification");
QVERIFY2(notifications.first().toMap().contains("deprecationWarning"), "Deprecation warning not included in notification!");
QVariantMap notificationContent = notifications.first().toMap().value("params").toMap();
QCOMPARE(notificationContent.value("event").toMap().value("deviceId").toUuid().toString(), device->id().toString());
QCOMPARE(notificationContent.value("event").toMap().value("eventTypeId").toUuid().toString(), mockIntEventTypeId.toString());
}
void TestEvents::params()
@ -130,6 +159,9 @@ void TestEvents::getEventType()
verifyError(response, "deviceError", enumValueName(error));
qCDebug(dcTests()) << "*content" << response;
QVERIFY2(response.toMap().contains("deprecationWarning"), "Deprecation warning not shown in reply");
if (error == Device::DeviceErrorNoError) {
QVERIFY2(EventTypeId(response.toMap().value("params").toMap().value("eventType").toMap().value("id").toString()) == eventTypeId, "Didn't get a reply for the same actionTypeId as requested.");
}