diff --git a/src/ipc/ipc-common/CMakeLists.txt b/src/ipc/ipc-common/CMakeLists.txt index 42413c67..c3b3283d 100644 --- a/src/ipc/ipc-common/CMakeLists.txt +++ b/src/ipc/ipc-common/CMakeLists.txt @@ -23,6 +23,7 @@ facelift_add_library(FaceliftIPCCommonLib IPCServiceAdapterBase.h NewIPCServiceAdapterBase.h IPCAttachedPropertyFactory.h + observer.h HEADERS_NO_MOC AppendDBUSSignatureFunction.h ipc-serialization.h diff --git a/src/ipc/ipc-common/IPCProxyBase.h b/src/ipc/ipc-common/IPCProxyBase.h index 25701b8d..6e9e80cc 100644 --- a/src/ipc/ipc-common/IPCProxyBase.h +++ b/src/ipc/ipc-common/IPCProxyBase.h @@ -35,12 +35,6 @@ #include "IPCProxyBinderBase.h" -#if defined(FaceliftIPCCommonLib_LIBRARY) -# define FaceliftIPCCommonLib_EXPORT Q_DECL_EXPORT -#else -# define FaceliftIPCCommonLib_EXPORT Q_DECL_IMPORT -#endif - namespace facelift { template diff --git a/src/ipc/ipc-common/observer.h b/src/ipc/ipc-common/observer.h new file mode 100644 index 00000000..2db456b0 --- /dev/null +++ b/src/ipc/ipc-common/observer.h @@ -0,0 +1,129 @@ +/********************************************************************** +** +** Copyright (C) 2020 Luxoft Sweden AB +** +** This file is part of the FaceLift project +** +** Permission is hereby granted, freIPCServiceAdapterBasee of charge, to any person +** obtaining a copy of this software and associated documentation files +** (the "Software"), to deal in the Software without restriction, +** including without limitation the rights to use, copy, modify, merge, +** publish, distribute, sublicense, and/or sell copies of the Software, +** and to permit persons to whom the Software is furnished to do so, +** subject to the following conditions: +** +** The above copyright notice and this permission notice shall be +** included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +** BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +** ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +** CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +** SOFTWARE. +** +** SPDX-License-Identifier: MIT +** +**********************************************************************/ + +#pragma once + +#include +#include +#include + +namespace facelift { + +class IObserver : public QObject +{ + Q_OBJECT +public: + virtual void IsReadyObserver(const std::shared_ptr &connection) = 0; +}; + +class IsReadyObserver: public QObject +{ + Q_OBJECT + QVector< QPointer > m_observers{}; + bool isReady{}; + +public: + IsReadyObserver() {} + + template + void watch(T const *object, S signal, F function) + { + QObject::connect(object, std::move(signal), object, [this, function, object](){ + std::function functor { std::bind(function, object) }; + if(functor != nullptr) { + isReady = functor(); + } + }); + QObject::connect(object, std::move(signal), this, &IsReadyObserver::onReadyChanged ); + } + + // Set observers + void setObservers(const QVector< QPointer > &observers) { + m_observers.clear(); + for (auto observer: observers) { + Q_ASSERT (observer != nullptr); + m_observers.push_back(observer); + auto connection = std::make_shared(); + *connection = QObject::connect(this, &IsReadyObserver::readyChanged, observer, [observer, connection](){ + observer->IsReadyObserver( connection ); + }); + } + } + + // Get observers + const QVector< QPointer > &getObservers() const { + return m_observers; + } + + Q_SIGNAL void readyChanged(); + + void onReadyChanged() { + if(isReady) { + emit readyChanged(); + } + } +}; + +// Single-time observer which will unregister itself when done +template +class SingleTimeObserver : public IObserver +{ + T m_function; + +public: + explicit SingleTimeObserver(T function) : m_function{function} {} + ~SingleTimeObserver() = default; + + void IsReadyObserver(const std::shared_ptr &connection) override { + if(m_function != nullptr) { + m_function(); + } + QObject::disconnect(*connection); + } +}; + +// Standard observer which will work for each signal +template +class StandartObserver : public IObserver +{ + T m_function; + +public: + explicit StandartObserver(T function) : m_function{function} {} + ~StandartObserver() = default; + + void IsReadyObserver(const std::shared_ptr &) override { + if(m_function != nullptr) { + m_function(); + } + } +}; + +} diff --git a/tests/unittest/CMakeLists.txt b/tests/unittest/CMakeLists.txt index 01b98f4a..2bb215c4 100644 --- a/tests/unittest/CMakeLists.txt +++ b/tests/unittest/CMakeLists.txt @@ -6,18 +6,34 @@ if(${GTEST_FOUND}) add_library(GTest::GMock UNKNOWN IMPORTED) set_target_properties(GTest::GMock PROPERTIES IMPORTED_LOCATION ${GMOCK_LIBRARY}) else() - message(WARNING "Google test/mock not found.") + message(ERROR "Google test/mock not found.") endif() + find_package(Qt5 COMPONENTS Test REQUIRED) + if(${QT5TEST_NOTFOUND}) + message(ERROR "Required package Qt5Test not found.") + endif() find_package(Threads REQUIRED) - set(FACELIFT_GTEST_LIBRARIES ${GTEST_BOTH_LIBRARIES} GTest::GMock Threads::Threads) + if(${THREADS_NOTFOUND}) + message(ERROR "Required package Threads not found.") + endif() + set(FACELIFT_GTEST_LIBRARIES ${GTEST_BOTH_LIBRARIES} GTest::GMock Qt5::Test Threads::Threads) include_directories(${GTEST_INCLUDE_DIRS}) facelift_add_test(UnitTests SOURCES FaceliftUtilsTest.cpp - LINK_LIBRARIES ${FACELIFT_GTEST_LIBRARIES} FaceliftCommonLib) + LINK_LIBRARIES + ${FACELIFT_GTEST_LIBRARIES} + FaceliftCommonLib + ) + facelift_add_test(UnitTestsObserver + SOURCES FaceliftObserverTest.cpp + LINK_LIBRARIES + ${FACELIFT_GTEST_LIBRARIES} + FaceliftIPCCommonLib + ) else() - message(WARNING "Required package google test not found!") + message(ERROR "Required package google test not found!") endif() diff --git a/tests/unittest/FaceliftObserverTest.cpp b/tests/unittest/FaceliftObserverTest.cpp new file mode 100644 index 00000000..291210a6 --- /dev/null +++ b/tests/unittest/FaceliftObserverTest.cpp @@ -0,0 +1,336 @@ +#include +#include "IPCProxyBase.h" +#include "InterfaceBase.h" +#include "observer.h" +#include + +namespace { + +using namespace facelift; + +/********************************************************************** +** The main goal is to have a flexible service, present as independent/extern component, that reacts on certain states of one object of inspection and execute vary functors either once or permanent. +** In general it is intend for a Qt objects a certain kind of types, that has the "ready" state, it is necessary to provide the ability to track this state using the "IsReadyObserver" component and an arbitrary number of observers of two types: "callOnReady" and "callOnceReady", which are functors. The ready state is determined by a specific signal that emitted from inspected object and "ready" state. +** The "IsReadyObserver" is the watcher of the signal of interest in "Proxy" and contain observers with functors to execute. +** INSTALLATION: the user creates an "IsReadyObserver" and sets in it: the object's address, the object's interface signal and the object's readiness function. After that set/load observers to "IsReadyObserver". +** For example: +** readyObserver.watch( &proxy, &IPCProxy::readyChanged, &IPCProxy::ready ); +** CONDITION FOR PROCESSING: for the inspected object, the interface signal has changed, while the ready property is in the "ready" state. In this case, the functors in observers will be executed once. +** If the interface signal for the inspected object has changed, but the ready property is in the "not ready" state. In this case, the functors in observers will not executed. +** REQUIREMENT: One change in the interface signal and "ready" state corresponds to a single execution of the functor. No signal can be missed. +** Main steps: +** - Creation of main objects: object for inspection "Proxy" (this is one of more possible types), object for observation "IsReadyObserver", objects for executing "Observers". +** - Installing objects/function to be executed in observers depending on the need: "callOnReady" or "callOnceReady". +** - Setting up to "IsReadyObserver" to watch out for signal in "Proxy". +** - Catching signal and execution of functors. +** - Completion of work. +**********************************************************************/ + +// MOC class for functor +class Counter +{ +public: + Counter() { m_value = 0; } + + void incValue() { + ++m_value; + } + int getValue() const { + return m_value; + } +private: + int m_value; +}; +// MOC class for proxy +template +class IPCProxy : public IPCProxyBase +{ + bool m_serviceReady{}; +public: + IPCProxy( QObject *parent ): IPCProxyBase(parent) {} + bool readyTest() const { + return m_serviceReady; + } + bool setReadyTest(bool ready) { + return m_serviceReady = ready; + } +}; + +class IPCProxyTest : public ::testing::Test +{ +public: + // Objects containing executable functions + Counter c1; + Counter c2; + // Installing objects/function to be executed in observers + // Observer to execute the task type "callOnReady" + StandartObserver < std::function > obs1 { std::bind(&Counter::incValue, &c1) }; + // Observer to execute the task type "callOnceReady" + SingleTimeObserver< std::function > obs2 { std::bind(&Counter::incValue, &c2) }; + // Observer with nullptr + StandartObserver < std::function > obs3 { nullptr }; + + // The "Proxy" is the source of the signal of interest + IPCProxy proxy {nullptr}; + // The "IsReadyObserver" is the watcher of the signal of interest in "Proxy" and contain observers to execute: "callOnReady", "callOnceReady" + IsReadyObserver readyObserver{}; + + ~IPCProxyTest() {} +}; + +TEST_F(IPCProxyTest, MinimalCoreTest) +{ + /********************************************************************** + ** INPUT: property of Proxy ready() in state disabled, trigger changeReady() is toggles 3 times. + ** OUTPUT: No observer should be executed. + **********************************************************************/ + // Set state ready() of Proxy to disable + proxy.setReadyTest(false); + // Setting up an "IsReadyObserver" to watch out for signal in "Proxy" + readyObserver.watch( &proxy, &IPCProxy::readyChanged, &IPCProxy::readyTest ); + // Set observers to "IsReadyObserver" + const auto expected = QVector< QPointer >{ &obs1, &obs2 }; + readyObserver.setObservers(expected); + + // Check values before calling a signals + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") + + // Set signal tracking for "IsReadyObserver" + QSignalSpy spyReadyObserver(&readyObserver, &IsReadyObserver::readyChanged ); + ASSERT_EQ( spyReadyObserver.isValid(), true); + spyReadyObserver.clear(); + + // Set signal tracking for "Proxy" + QSignalSpy spyProxy(&proxy, &IPCProxy::readyChanged ); + ASSERT_EQ( spyProxy.isValid(), true); + spyProxy.clear(); + + // Check that the signals on "IsReadyObserver" and "Proxy" are absent + ASSERT_EQ(spyReadyObserver.count(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + proxy.readyChanged(); + proxy.readyChanged(); + proxy.readyChanged(); + + // Check the number of received signals on "Proxy" + ASSERT_EQ(spyProxy.count(), 3); + // Check the number of received signals on "IsReadyObserver" + ASSERT_EQ(spyReadyObserver.count(), 0); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") + + /********************************************************************** + ** INPUT: property of Proxy ready() in state enabled, trigger changeReady() is toggles 3 times. + ** OUTPUT: All observer should be executed. + **********************************************************************/ + // Set state ready() of Proxy to enable + proxy.setReadyTest(true); + // Check values before calling a signals + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") + + // Set signal tracking for "IsReadyObserver" + ASSERT_EQ( spyReadyObserver.isValid(), true); + spyReadyObserver.clear(); + + // Set signal tracking for "Proxy" + ASSERT_EQ( spyProxy.isValid(), true); + spyProxy.clear(); + + // Check that the signals on "IsReadyObserver" and "Proxy" are absent + ASSERT_EQ(spyReadyObserver.count(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + proxy.readyChanged(); + proxy.readyChanged(); + proxy.readyChanged(); + + // Check the number of received signals on "Proxy" + ASSERT_EQ(spyProxy.count(), 3); + // Check the number of received signals on "IsReadyObserver" + ASSERT_EQ(spyReadyObserver.count(), 3); + // No one signal must be lost or skipped + ASSERT_EQ(spyProxy.count(), spyReadyObserver.count()); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 3); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 1); // for SingleTimeObserver ("callOnceReady") +} + +TEST_F(IPCProxyTest, nullptrObservers) +{ + /********************************************************************** + ** INPUT: property of Proxy ready() in state disabled, trigger changeReady() is toggles 3 times. + ** OUTPUT: No observer should be executed. + **********************************************************************/ + // Set state ready() of Proxy to disable + proxy.setReadyTest(false); + // Setting up an "IsReadyObserver" to watch out for signal in "Proxy" + readyObserver.watch( &proxy, &IPCProxy::readyChanged, &IPCProxy::readyTest ); + // Set observers to "IsReadyObserver" + const auto expected = QVector< QPointer >{ &obs1, nullptr, &obs2 }; + readyObserver.setObservers(expected); + + // Check values before calling a signals + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") + + // Set signal tracking for "IsReadyObserver" + QSignalSpy spyReadyObserver(&readyObserver, &IsReadyObserver::readyChanged ); + ASSERT_EQ( spyReadyObserver.isValid(), true); + spyReadyObserver.clear(); + + // Set signal tracking for "Proxy" + QSignalSpy spyProxy(&proxy, &IPCProxy::readyChanged ); + ASSERT_EQ( spyProxy.isValid(), true); + spyProxy.clear(); + + // Check that the signals on "IsReadyObserver" and "Proxy" are absent + ASSERT_EQ(spyReadyObserver.count(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + proxy.readyChanged(); + proxy.readyChanged(); + proxy.readyChanged(); + + // Check the number of received signals on "Proxy" + ASSERT_EQ(spyProxy.count(), 3); + // Check the number of received signals on "IsReadyObserver" + ASSERT_EQ(spyReadyObserver.count(), 0); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") + + /********************************************************************** + ** INPUT: property of Proxy ready() in state enabled, trigger changeReady() is toggles 3 times. + ** OUTPUT: All observer should be executed. + **********************************************************************/ + // Set state ready() of Proxy to enable + proxy.setReadyTest(true); + // Check values before calling a signals + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") + + // Set signal tracking for "IsReadyObserver" + ASSERT_EQ( spyReadyObserver.isValid(), true); + spyReadyObserver.clear(); + + // Set signal tracking for "Proxy" + ASSERT_EQ( spyProxy.isValid(), true); + spyProxy.clear(); + + // Check that the signals on "IsReadyObserver" and "Proxy" are absent + ASSERT_EQ(spyReadyObserver.count(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + proxy.readyChanged(); + proxy.readyChanged(); + proxy.readyChanged(); + + // Check the number of received signals on "Proxy" + ASSERT_EQ(spyProxy.count(), 3); + // Check the number of received signals on "IsReadyObserver" + ASSERT_EQ(spyReadyObserver.count(), 3); + // No one signal must be lost or skipped + ASSERT_EQ(spyProxy.count(), spyReadyObserver.count()); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 3); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 1); // for SingleTimeObserver ("callOnceReady") +} + +TEST_F(IPCProxyTest, nullptrFunction) +{ + /********************************************************************** + ** INPUT: property of Proxy ready() in state disabled, trigger changeReady() is toggles 3 times. + ** OUTPUT: No observer should be executed. + **********************************************************************/ + // Set state ready() of Proxy to disable + proxy.setReadyTest(false); + // Setting up an "IsReadyObserver" to watch out for signal in "Proxy" + readyObserver.watch( &proxy, &IPCProxy::readyChanged, &IPCProxy::readyTest ); + // Set observers to "IsReadyObserver" + const auto expected = QVector< QPointer >{ &obs1, &obs3, &obs2 }; + readyObserver.setObservers(expected); + + // Check values before calling a signals + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") + + // Set signal tracking for "IsReadyObserver" + QSignalSpy spyReadyObserver(&readyObserver, &IsReadyObserver::readyChanged ); + ASSERT_EQ( spyReadyObserver.isValid(), true); + spyReadyObserver.clear(); + + // Set signal tracking for "Proxy" + QSignalSpy spyProxy(&proxy, &IPCProxy::readyChanged ); + ASSERT_EQ( spyProxy.isValid(), true); + spyProxy.clear(); + + // Check that the signals on "IsReadyObserver" and "Proxy" are absent + ASSERT_EQ(spyReadyObserver.count(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + proxy.readyChanged(); + proxy.readyChanged(); + proxy.readyChanged(); + + // Check the number of received signals on "Proxy" + ASSERT_EQ(spyProxy.count(), 3); + // Check the number of received signals on "IsReadyObserver" + ASSERT_EQ(spyReadyObserver.count(), 0); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") + + /********************************************************************** + ** INPUT: property of Proxy ready() in state enabled, trigger changeReady() is toggles 3 times. + ** OUTPUT: All observer should be executed. + **********************************************************************/ + // Set state ready() of Proxy to enable + proxy.setReadyTest(true); + // Check values before calling a signals + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") + + // Set signal tracking for "IsReadyObserver" + ASSERT_EQ( spyReadyObserver.isValid(), true); + spyReadyObserver.clear(); + + // Set signal tracking for "Proxy" + ASSERT_EQ( spyProxy.isValid(), true); + spyProxy.clear(); + + // Check that the signals on "IsReadyObserver" and "Proxy" are absent + ASSERT_EQ(spyReadyObserver.count(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + proxy.readyChanged(); + proxy.readyChanged(); + proxy.readyChanged(); + + // Check the number of received signals on "Proxy" + ASSERT_EQ(spyProxy.count(), 3); + // Check the number of received signals on "IsReadyObserver" + ASSERT_EQ(spyReadyObserver.count(), 3); + // No one signal must be lost or skipped + ASSERT_EQ(spyProxy.count(), spyReadyObserver.count()); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 3); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 1); // for SingleTimeObserver ("callOnceReady") +} + +} // end namespace