From acf860f58b11657d76f8867393e6d0a0c2911858 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/IPCProxyBase.h | 6 - src/ipc/ipc-common/observer.h | 129 ++++++++++ tests/unittest/CMakeLists.txt | 24 +- tests/unittest/FaceliftObserverTest.cpp | 318 ++++++++++++++++++++++++ 5 files changed, 468 insertions(+), 10 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/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..32c2c3ad --- /dev/null +++ b/tests/unittest/FaceliftObserverTest.cpp @@ -0,0 +1,318 @@ +#include +#include "IPCProxyBase.h" +#include "InterfaceBase.h" +#include +#include "observer.h" + +namespace { + +using namespace facelift; + +// 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 IPCProxyBaseTest : 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{}; + + ~IPCProxyBaseTest() {} +}; + +TEST_F(IPCProxyBaseTest, 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(IPCProxyBaseTest, 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, nullptr, nullptr, nullptr, nullptr, nullptr,}; + 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(IPCProxyBaseTest, 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, &obs3, }; + 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