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..48ce3dd2 100644 --- a/tests/unittest/CMakeLists.txt +++ b/tests/unittest/CMakeLists.txt @@ -9,14 +9,30 @@ if(${GTEST_FOUND}) message(WARNING "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!") endif() diff --git a/tests/unittest/FaceliftObserverTest.cpp b/tests/unittest/FaceliftObserverTest.cpp new file mode 100644 index 00000000..a8f284db --- /dev/null +++ b/tests/unittest/FaceliftObserverTest.cpp @@ -0,0 +1,265 @@ +#include +#include "IPCProxyBase.h" +#include "InterfaceBase.h" +#include "observer.h" +#include + +namespace { + +using namespace facelift; + +// MOCK class for callback +class Counter +{ +public: + Counter() : m_value{0} {} + + void incValue() { + ++m_value; + } + + int getValue() const { + return m_value; + } + +private: + int m_value; +}; +// MOCK class for IPCProxy +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: + const int countOfReceivedSignalsBeforeEmit = 0; + const int countOfEmittedProxySignals = 3; + const int countOfReceivedSignalsWhenReadyTrue = countOfEmittedProxySignals; + const int countOfReceivedSignalsWhenReadyFalse = 0; + + // Objects containing executable functions + Counter c1; + Counter c2; + // Installing objects/function to be executed in observers + // Observer to execute the callback type "callOnReady" + StandartObserver < std::function > obs1 { std::bind(&Counter::incValue, &c1) }; + // Observer to execute the callback 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, proxyReadyIsFalse) +{ + // Checking if proxy is not ready and signal exists, observer should not be executed. + // Set to Proxy in state "false" and set 2 observers + + // 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 spyObserver(&readyObserver, &IsReadyObserver::readyChanged); + ASSERT_EQ(spyObserver.isValid(), true); + spyObserver.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(spyObserver.count(), countOfReceivedSignalsBeforeEmit); + ASSERT_EQ(spyProxy.count(), countOfReceivedSignalsBeforeEmit); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + for (auto i=0; i::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 spyObserver(&readyObserver, &IsReadyObserver::readyChanged); + ASSERT_EQ(spyObserver.isValid(), true); + spyObserver.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(spyObserver.count(), countOfReceivedSignalsBeforeEmit); + ASSERT_EQ(spyProxy.count(), countOfReceivedSignalsBeforeEmit); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + for (auto i=0; i::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 spyObserver(&readyObserver, &IsReadyObserver::readyChanged); + ASSERT_EQ(spyObserver.isValid(), true); + spyObserver.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(spyObserver.count(), countOfReceivedSignalsBeforeEmit); + ASSERT_EQ(spyProxy.count(), countOfReceivedSignalsBeforeEmit); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + for (auto i=0; i::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 spyObserver(&readyObserver, &IsReadyObserver::readyChanged); + ASSERT_EQ(spyObserver.isValid(), true); + spyObserver.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(spyObserver.count(), countOfReceivedSignalsBeforeEmit); + ASSERT_EQ(spyProxy.count(), countOfReceivedSignalsBeforeEmit); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + for (auto i=0; i