From af34354997b561ac7d7c6a7f4c9746a6179ec81d Mon Sep 17 00:00:00 2001 From: Vitalii Arteev Date: Wed, 14 Oct 2020 17:35:24 +0200 Subject: [PATCH] Add auxiliary API for Proxy --- src/ipc/ipc-common/CMakeLists.txt | 1 + src/ipc/ipc-common/observer.h | 196 ++++++++++++ tests/unittest/CMakeLists.txt | 20 +- tests/unittest/FaceliftObserverTest.cpp | 384 ++++++++++++++++++++++++ 4 files changed, 599 insertions(+), 2 deletions(-) create mode 100644 src/ipc/ipc-common/observer.h create mode 100644 tests/unittest/FaceliftObserverTest.cpp 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/observer.h b/src/ipc/ipc-common/observer.h new file mode 100644 index 00000000..e98f0679 --- /dev/null +++ b/src/ipc/ipc-common/observer.h @@ -0,0 +1,196 @@ +/********************************************************************** +** +** Copyright (C) 2018 Luxoft Sweden AB +** +** This file is part of the FaceLift project +** +** Permission is hereby granted, free 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 +#include +#include + +namespace facelift { + +class Observer : public QObject +{ + Q_OBJECT +}; + +// Single-time observer which will unregister itself when done +template +class SingleTimeObserver : public Observer +{ + T m_function; + +public: + explicit SingleTimeObserver(T function) : m_function{function} {} + ~SingleTimeObserver() = default; + + void IsReadyObserver(const std::shared_ptr &connection) { + if (m_function != nullptr) { + m_function(); + } + QObject::disconnect(*connection); + } +}; + +// Standard observer which will work for each signal +template +class StandartObserver : public Observer +{ + T m_function; + +public: + explicit StandartObserver(T function) : m_function{function} {} + ~StandartObserver() = default; + + void IsReadyObserver(const std::shared_ptr &) { + if (m_function != nullptr) { + m_function(); + } + } +}; + +class IsReadyObserver: public QObject +{ + Q_OBJECT + QVector< std::shared_ptr< Observer > > m_observers{}; + bool isReady{}; + +public: + IsReadyObserver() {} + + // Set the object, signal to watch for + template + void watch(T const *object, S signal) + { + QObject::connect(object, std::move(signal), this, &IsReadyObserver::onReadyChanged); + isReady = true; + } + + // Set the object, signal, function to watch for + 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 observer + template + void setObserver(const std::shared_ptr &observer) + { + Q_ASSERT (observer != nullptr); + m_observers.push_back(observer); + auto connection = std::make_shared(); + *connection = QObject::connect(this, &IsReadyObserver::readyChanged, observer.get(), [wp_observer = std::weak_ptr(observer), connection]() { + auto sp_observer = wp_observer.lock(); + if (sp_observer){ + sp_observer->IsReadyObserver( connection ); + } + }); + } + + // Delete observer + void delObserver(const std::weak_ptr &observer) + { + m_observers.erase( std::remove(m_observers.begin(), m_observers.end(), std::shared_ptr< Observer >(observer)), m_observers.end() ); + } + + // Clear all observers + void clear() + { + m_observers.clear(); + } + + // Get count observers + auto count() + { + return getObservers().size(); + } + + // Set "callOnReady" observer + template + const std::weak_ptr setStandartObserver(T function) + { + auto observer = std::make_shared< StandartObserver < std::function > > ( function ); + setObserver( observer ); + return observer; + } + + // Set "callOnReady" observer + template + const std::weak_ptr setStandartObserver(T object, F function, Args&& ... args) + { + auto observer = std::make_shared< StandartObserver < std::function > > ( std::bind(function, object, std::forward(args) ...) ); + setObserver( observer ); + return observer; + } + + // Set "callOnceReady" observer + template + const std::weak_ptr setSingleTimeObserver(T function) + { + auto observer = std::make_shared< SingleTimeObserver < std::function > > ( function ); + setObserver( observer ); + return observer; + } + + // Set "callOnceReady" observer + template + const std::weak_ptr setSingleTimeObserver(T object, F function, Args&& ... args) + { + auto observer = std::make_shared< SingleTimeObserver < std::function > > ( std::bind(function, object, std::forward(args) ...) ); + setObserver( observer ); + return observer; + } + + // Get observers + const QVector< std::shared_ptr > &getObservers() const + { + return m_observers; + } + + Q_SIGNAL void readyChanged(); + + void onReadyChanged() + { + if (isReady) { + emit readyChanged(); + } + } +}; + +} 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..7895a466 --- /dev/null +++ b/tests/unittest/FaceliftObserverTest.cpp @@ -0,0 +1,384 @@ +/********************************************************************** +** +** Copyright (C) 2020 Luxoft Sweden AB +** +** This file is part of the FaceLift project +** +** Permission is hereby granted, free 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 +** +**********************************************************************/ + +#include +#include "IPCProxyBase.h" +#include "InterfaceBase.h" +#include "observer.h" +#include + +namespace { + +using namespace facelift; + +// MOCK for function +int foo(int val){ + std::cout << " the input variable is: " << val << std::endl; + return val; +} + +// MOCK class for callback +class Counter +{ +public: + Counter() : m_value{0} {} + + void incValue() { + ++m_value; + } + void addValue(int val) { + m_value += val; + } + 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: + // Objects containing executable functions + Counter c1; + Counter c2; + Counter c3; + // 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, addDeleteObservers) +{ + // Checking correctness of add and delete observers + // Add to readyObserver a 2 observers, then delete 1 observer, then add 1 observer, then delete each of them + + // 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 sso = readyObserver.setStandartObserver(&c1, &Counter::incValue); + const auto sto1 = readyObserver.setSingleTimeObserver(&c2, &Counter::incValue); + ASSERT_EQ(readyObserver.count(), 2); + readyObserver.delObserver(sso); + ASSERT_EQ(readyObserver.count(), 1); + const auto sto2 = readyObserver.setSingleTimeObserver(&c3, &Counter::incValue); + ASSERT_EQ(readyObserver.count(), 2); + readyObserver.delObserver(sto2); + ASSERT_EQ(readyObserver.count(), 1); + readyObserver.delObserver(sto1); + ASSERT_EQ(readyObserver.count(), 0); +} +TEST_F(IPCProxyTest, clearObservers) +{ + // Checking correctness of clearing observers + // Add to readyObserver a some observers, then clear them + + // 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" + readyObserver.setStandartObserver(&c1, &Counter::incValue); + readyObserver.setSingleTimeObserver(&c2, &Counter::incValue); + readyObserver.setSingleTimeObserver(&c3, &Counter::incValue); + ASSERT_EQ(readyObserver.count(), 3); + readyObserver.clear(); + ASSERT_EQ(readyObserver.count(), 0); +} +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" + readyObserver.setStandartObserver(&c1, &Counter::incValue); + readyObserver.setSingleTimeObserver(&c2, &Counter::incValue); + + // 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(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + for (auto i=0; i<3; ++i) { + 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(spyObserver.count(), 0); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") +} +TEST_F(IPCProxyTest, proxyReadyIsTrue) +{ + // Checking if proxy is ready and signal exists, all observer should be executed. + // Set to Proxy in state "true" and set 2 observers + + // Set state ready() of Proxy to enable + proxy.setReadyTest(true); + + // Setting up an "IsReadyObserver" to watch out for signal in "Proxy" + readyObserver.watch(&proxy, &IPCProxy::readyChanged, &IPCProxy::readyTest); + // Set observers to "IsReadyObserver" + readyObserver.setStandartObserver(&c1, &Counter::incValue); + readyObserver.setSingleTimeObserver(&c2, &Counter::incValue); + readyObserver.setStandartObserver( []() { foo(42); } ); + + // 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(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + for (auto i=0; i<3; ++i) { + 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(spyObserver.count(), 3); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 3); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 1); // for SingleTimeObserver ("callOnceReady") +} +TEST_F(IPCProxyTest, setNullptrObservers) +{ + // Checking if proxy is ready and signal exists and set nullptr instead address of observer, all valid observer should be executed. + // Set to Proxy in state "true" and set 2 observers and 1 nullptr + + // Set state ready() of Proxy to enable + proxy.setReadyTest(true); + + // Setting up an "IsReadyObserver" to watch out for signal in "Proxy" + readyObserver.watch(&proxy, &IPCProxy::readyChanged, &IPCProxy::readyTest); + // Set observers to "IsReadyObserver" + readyObserver.setStandartObserver(&c1, &Counter::incValue); + readyObserver.setStandartObserver( nullptr ); + readyObserver.setSingleTimeObserver(&c2, &Counter::incValue); + + // 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(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + for (auto i=0; i<3; ++i) { + 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(spyObserver.count(), 3); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 3); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 1); // for SingleTimeObserver ("callOnceReady") +} + +TEST_F(IPCProxyTest, deleteObserverRun) +{ + // Checking if proxy is ready and signal exists and 1 from 3 observers with nullptr callback, all valid observer should be executed. + // Set to Proxy in state "true" and set 3 observers, 1 from them with nullptr callback + + // Set state ready() of Proxy to enable + proxy.setReadyTest(true); + + // Setting up an "IsReadyObserver" to watch out for signal in "Proxy" + readyObserver.watch(&proxy, &IPCProxy::readyChanged, &IPCProxy::readyTest); + // Set observers to "IsReadyObserver" + const auto sso = readyObserver.setStandartObserver(&c1, &Counter::incValue); + readyObserver.setSingleTimeObserver(&c2, &Counter::incValue); + readyObserver.setStandartObserver(&c3, &Counter::addValue, 50); + // 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(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Clear observers + readyObserver.delObserver(sso); + ASSERT_EQ(readyObserver.count(), 2); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + for (auto i=0; i<3; ++i) { + 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(spyObserver.count(), 3); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 1); // for SingleTimeObserver ("callOnceReady") + ASSERT_EQ(c3.getValue(), 150); // for StandartObserver ("callOnReady") +} +TEST_F(IPCProxyTest, clearObserverRun) +{ + // Checking if proxy is ready and signal exists and 1 from 3 observers with nullptr callback, all valid observer should be executed. + // Set to Proxy in state "true" and set 3 observers, 1 from them with nullptr callback + + // Set state ready() of Proxy to enable + proxy.setReadyTest(true); + + // Setting up an "IsReadyObserver" to watch out for signal in "Proxy" + readyObserver.watch(&proxy, &IPCProxy::readyChanged, &IPCProxy::readyTest); + // Set observers to "IsReadyObserver" + const auto sso = readyObserver.setStandartObserver(&c1, &Counter::incValue); + readyObserver.setSingleTimeObserver(&c2, &Counter::incValue); + + // 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(), 0); + ASSERT_EQ(spyProxy.count(), 0); + + // Clear observers + readyObserver.clear(); + ASSERT_EQ(readyObserver.count(), 0); + + // Generate a some signals on the "Proxy" which is tracking on "IsReadyObserver" + for (auto i=0; i<3; ++i) { + 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(spyObserver.count(), 3); + + // Check values after signals call + ASSERT_EQ(c1.getValue(), 0); // for StandartObserver ("callOnReady") + ASSERT_EQ(c2.getValue(), 0); // for SingleTimeObserver ("callOnceReady") +} +} // end namespace