From b87014060892e9d5e3a1d33c86fb16a969c592f1 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 11 Jul 2020 01:05:24 +0200 Subject: [PATCH] Add tests, fix shutdown --- .../python/pynymealogginghandler.h | 2 - libnymea-core/integrations/python/pything.h | 1 - .../integrations/pythonintegrationplugin.cpp | 17 ++- .../integrations/pythonintegrationplugin.h | 2 + plugins/pymock/integrationpluginpymock.py | 7 +- tests/auto/auto.pro | 1 + tests/auto/pythonplugins/pythonplugins.pro | 6 + .../auto/pythonplugins/testpythonplugins.cpp | 140 ++++++++++++++++++ 8 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 tests/auto/pythonplugins/pythonplugins.pro create mode 100644 tests/auto/pythonplugins/testpythonplugins.cpp diff --git a/libnymea-core/integrations/python/pynymealogginghandler.h b/libnymea-core/integrations/python/pynymealogginghandler.h index 69599a40..2fa5d3c9 100644 --- a/libnymea-core/integrations/python/pynymealogginghandler.h +++ b/libnymea-core/integrations/python/pynymealogginghandler.h @@ -24,8 +24,6 @@ static int PyNymeaLoggingHandler_init(PyNymeaLoggingHandler */*self*/, PyObject static void PyNymeaLoggingHandler_dealloc(PyNymeaLoggingHandler * self) // destruct the object { - // FIXME: Why is this not called? Seems we're leaking... - Q_ASSERT(false); Py_TYPE(self)->tp_free(self); } diff --git a/libnymea-core/integrations/python/pything.h b/libnymea-core/integrations/python/pything.h index 96bec099..5ab70716 100644 --- a/libnymea-core/integrations/python/pything.h +++ b/libnymea-core/integrations/python/pything.h @@ -73,7 +73,6 @@ static void PyThing_setThing(PyThing *self, Thing *thing) self->pyStates = PyList_New(thing->states().count()); for (int i = 0; i < thing->states().count(); i++) { - qWarning() << "i" << i; State state = thing->states().at(i); PyObject *pyState = Py_BuildValue("{s:s, s:O}", "stateTypeId", state.stateTypeId().toString().toUtf8().data(), diff --git a/libnymea-core/integrations/pythonintegrationplugin.cpp b/libnymea-core/integrations/pythonintegrationplugin.cpp index ee26620c..9ae6f5de 100644 --- a/libnymea-core/integrations/pythonintegrationplugin.cpp +++ b/libnymea-core/integrations/pythonintegrationplugin.cpp @@ -18,6 +18,7 @@ #include #include #include +#include PyThreadState* PythonIntegrationPlugin::s_mainThread = nullptr; PyObject* PythonIntegrationPlugin::s_nymeaModule = nullptr; @@ -71,7 +72,6 @@ static PyModuleDef nymea_module = "nymea module for python based integration plugins", // const char* m_doc; -1, // Py_ssize_t m_size; nymea_methods, // PyMethodDef *m_methods - // inquiry m_reload; traverseproc m_traverse; inquiry m_clear; freefunc m_free; nullptr, nullptr, nullptr, nullptr }; @@ -274,9 +274,16 @@ PythonIntegrationPlugin::PythonIntegrationPlugin(QObject *parent) : IntegrationP PythonIntegrationPlugin::~PythonIntegrationPlugin() { - PyGILState_STATE s = PyGILState_Ensure(); + PyGILState_Ensure(); + + while (!m_runningThreads.isEmpty()) { + PyObject *loop = m_runningThreads.keys().first(); + PyObject *stop = PyObject_GetAttrString(loop, "stop"); + PyObject_CallFunctionObjArgs(stop, nullptr); + } + Py_XDECREF(s_plugins.take(this)); - PyGILState_Release(s); + Py_FinalizeEx(); } void PythonIntegrationPlugin::initPython() @@ -723,7 +730,7 @@ bool PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObje dumpError(); PyObject *run_until_complete = PyObject_GetAttrString(loop, "run_until_complete"); - QtConcurrent::run([run_until_complete, task, loop, result](){ + QFuture future = QtConcurrent::run([this, run_until_complete, task, loop, result](){ PyGILState_STATE g = PyGILState_Ensure(); // auto s = PyThreadState_New(PyInterpreterState_Main()); // PyThreadState *previousThreadState = PyThreadState_Swap(s); @@ -732,11 +739,13 @@ bool PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObje Py_DECREF(task); Py_DECREF(run_until_complete); Py_DECREF(result); + m_runningThreads.remove(loop); // PyThreadState_Swap(previousThreadState); // PyThreadState_Clear(s); // PyThreadState_Delete(s); PyGILState_Release(g); }); + m_runningThreads.insert(loop, future); Py_DECREF(create_task); Py_DECREF(add_done_callback); diff --git a/libnymea-core/integrations/pythonintegrationplugin.h b/libnymea-core/integrations/pythonintegrationplugin.h index fbe2093e..9a0e5af4 100644 --- a/libnymea-core/integrations/pythonintegrationplugin.h +++ b/libnymea-core/integrations/pythonintegrationplugin.h @@ -82,6 +82,8 @@ private: // Need to keep a copy of plugin params and sync that in a thread-safe manner ParamList m_pluginConfigCopy; + + QHash> m_runningThreads; }; #endif // PYTHONINTEGRATIONPLUGIN_H diff --git a/plugins/pymock/integrationpluginpymock.py b/plugins/pymock/integrationpluginpymock.py index 28abe6f1..63b2220e 100644 --- a/plugins/pymock/integrationpluginpymock.py +++ b/plugins/pymock/integrationpluginpymock.py @@ -89,7 +89,7 @@ async def setupThing(info): async def postSetupThing(thing): - logger.log("postSetupThing for", thing.name, thing.params[0].value) + logger.log("postSetupThing for", thing.name) thing.nameChangedHandler = lambda thing : logger.log("Thing name changed", thing.name) if thing.thingClassId == pyMockAutoThingClassId: @@ -111,3 +111,8 @@ def autoThings(): if thing.thingClassId == pyMockAutoThingClassId: autoThings.append(thing) return autoThings + + +# Intentionally commented out to also have a test case for unimplmented functions +# def thingRemoved(thing): +# logger.log("thingRemoved for", thing.name) diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index c6431395..db10112b 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -13,6 +13,7 @@ SUBDIRS = \ loggingloading \ mqttbroker \ plugins \ + pythonplugins \ rules \ scripts \ states \ diff --git a/tests/auto/pythonplugins/pythonplugins.pro b/tests/auto/pythonplugins/pythonplugins.pro new file mode 100644 index 00000000..438e619e --- /dev/null +++ b/tests/auto/pythonplugins/pythonplugins.pro @@ -0,0 +1,6 @@ +TARGET = testactions + +include(../../../nymea.pri) +include(../autotests.pri) + +SOURCES += testpythonplugins.cpp diff --git a/tests/auto/pythonplugins/testpythonplugins.cpp b/tests/auto/pythonplugins/testpythonplugins.cpp new file mode 100644 index 00000000..e3b08b4e --- /dev/null +++ b/tests/auto/pythonplugins/testpythonplugins.cpp @@ -0,0 +1,140 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 . +* +* 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 "nymeatestbase.h" + +#include "integrations/thing.h" + +ThingClassId pyMockThingClassId = ThingClassId("1761c256-99b1-41bd-988a-a76087f6a4f1"); +ThingClassId pyMockDiscoveryPairingThingClassId = ThingClassId("248c5046-847b-44d0-ab7c-684ff79197dc"); +ParamTypeId pyMockDiscoveryPairingResultCountDiscoveryParamTypeID = ParamTypeId("ef5f6b90-e9d8-4e77-a14d-6725cfb07116"); + +using namespace nymeaserver; + +class TestPythonPlugins: public NymeaTestBase +{ + Q_OBJECT + +private: + inline void verifyThingError(const QVariant &response, Thing::ThingError error = Thing::ThingErrorNoError) { + verifyError(response, "thingError", enumValueName(error)); + } + +private slots: + + void initTestCase(); + + void setupAndRemoveThing(); + void testDiscoverPairAndRemoveThing(); + + +}; + + +void TestPythonPlugins::initTestCase() +{ + NymeaTestBase::initTestCase(); + QLoggingCategory::setFilterRules("*.debug=false\n" + "Tests.debug=true\n" + "PyMock.debug=true\n" + ); +} + +void TestPythonPlugins::setupAndRemoveThing() +{ + QVariantMap resultCountParam; + resultCountParam.insert("paramTypeId", pyMockDiscoveryPairingResultCountDiscoveryParamTypeID); + resultCountParam.insert("value", 2); + + QVariantList discoveryParams; + discoveryParams.append(resultCountParam); + + QVariantMap params; + params.insert("thingClassId", pyMockThingClassId); + params.insert("name", "Py test thing"); + QVariant response = injectAndWait("Integrations.AddThing", params); + + verifyThingError(response, Thing::ThingErrorNoError); + ThingId thingId = response.toMap().value("params").toMap().value("thingId").toUuid(); + qCDebug(dcTests()) << "New thing id" << thingId; + + params.clear(); + params.insert("thingId", thingId); + injectAndWait("Integrations.RemoveThing", params); + verifyThingError(response, Thing::ThingErrorNoError); +} + +void TestPythonPlugins::testDiscoverPairAndRemoveThing() +{ + // Discover + QVariantMap resultCountParam; + resultCountParam.insert("paramTypeId", pyMockDiscoveryPairingResultCountDiscoveryParamTypeID); + resultCountParam.insert("value", 2); + + QVariantList discoveryParams; + discoveryParams.append(resultCountParam); + + QVariantMap params; + params.insert("thingClassId", pyMockDiscoveryPairingThingClassId); + params.insert("discoveryParams", discoveryParams); + QVariant response = injectAndWait("Integrations.DiscoverThings", params); + + verifyThingError(response, Thing::ThingErrorNoError); + QCOMPARE(response.toMap().value("params").toMap().value("thingDescriptors").toList().count(), 2); + + ThingDescriptorId descriptorId = response.toMap().value("params").toMap().value("thingDescriptors").toList().first().toMap().value("id").toUuid(); + + // Pair + params.clear(); + params.insert("thingDescriptorId", descriptorId); + response = injectAndWait("Integrations.PairThing", params); + verifyThingError(response, Thing::ThingErrorNoError); + + qWarning() << "respo" << response.toMap().value("params").toMap(); + PairingTransactionId transactionId = response.toMap().value("params").toMap().value("pairingTransactionId").toUuid(); + qWarning() << "transactionId" << transactionId; + + params.clear(); + params.insert("pairingTransactionId", transactionId); + params.insert("username", "john"); + params.insert("secret", "smith"); + response = injectAndWait("Integrations.ConfirmPairing", params); + verifyThingError(response, Thing::ThingErrorNoError); + ThingId thingId = response.toMap().value("params").toMap().value("thingId").toUuid(); + + // Remove + params.clear(); + params.insert("thingId", thingId); + response = injectAndWait("Integrations.RemoveThing", params); + verifyThingError(response, Thing::ThingErrorNoError); +} + +#include "testpythonplugins.moc" +QTEST_MAIN(TestPythonPlugins)