From b05e7f9081af63e83675130e3a399dfd2ce05347 Mon Sep 17 00:00:00 2001 From: ColinDuquesnoy Date: Sat, 2 Dec 2017 20:12:22 +0100 Subject: [PATCH] SingleInstance application decorator See #161 --- 3rdparty/boost-di-extensions/Factory.hpp | 59 +++++ CMakeLists.txt | 2 +- docs/developers/PlantUml/Application.puml | 53 ++--- .../Application/QtApplication.cpp | 2 +- .../Application/QtApplication.hpp | 2 +- .../Application/SingleInstance.cpp | 132 +++++++++++ .../Application/SingleInstance.hpp | 62 ++++++ .../Infrastructure/Network/LocalServer.cpp | 35 +++ .../Infrastructure/Network/LocalServer.hpp | 39 ++++ .../Infrastructure/Network/LocalSocket.cpp | 50 +++++ .../Infrastructure/Network/LocalSocket.hpp | 41 ++++ .../{Application => }/Application.cpp | 0 .../{Application => }/Application.hpp | 6 +- .../Network/LocalServerIntegrationTests.cpp | 62 ++++++ .../Application/FakeApplication.cpp | 28 --- .../Application/FakeApplication.hpp | 31 ++- .../Application/FakeQtApplication.hpp | 3 +- .../Application/SingleInstanceTests.cpp | 206 ++++++++++++++++++ .../Network/FakeLocalServer.hpp | 51 +++++ .../Network/FakeLocalSocket.hpp | 63 ++++++ .../Application/ApplicationTests.cpp | 5 +- 21 files changed, 857 insertions(+), 75 deletions(-) create mode 100644 3rdparty/boost-di-extensions/Factory.hpp rename lib/MellowPlayer/{Presentation => Infrastructure}/Application/QtApplication.cpp (96%) rename lib/MellowPlayer/{Presentation => Infrastructure}/Application/QtApplication.hpp (97%) create mode 100644 lib/MellowPlayer/Infrastructure/Application/SingleInstance.cpp create mode 100644 lib/MellowPlayer/Infrastructure/Application/SingleInstance.hpp create mode 100644 lib/MellowPlayer/Infrastructure/Network/LocalServer.cpp create mode 100644 lib/MellowPlayer/Infrastructure/Network/LocalServer.hpp create mode 100644 lib/MellowPlayer/Infrastructure/Network/LocalSocket.cpp create mode 100644 lib/MellowPlayer/Infrastructure/Network/LocalSocket.hpp rename lib/MellowPlayer/Presentation/{Application => }/Application.cpp (100%) rename lib/MellowPlayer/Presentation/{Application => }/Application.hpp (82%) create mode 100644 tests/IntegrationTests/Infrastructure/Network/LocalServerIntegrationTests.cpp delete mode 100644 tests/UnitTests/Infrastructure/Application/FakeApplication.cpp rename tests/UnitTests/{Presentation => Infrastructure}/Application/FakeQtApplication.hpp (93%) create mode 100644 tests/UnitTests/Infrastructure/Application/SingleInstanceTests.cpp create mode 100644 tests/UnitTests/Infrastructure/Network/FakeLocalServer.hpp create mode 100644 tests/UnitTests/Infrastructure/Network/FakeLocalSocket.hpp diff --git a/3rdparty/boost-di-extensions/Factory.hpp b/3rdparty/boost-di-extensions/Factory.hpp new file mode 100644 index 00000000..c2bfdff1 --- /dev/null +++ b/3rdparty/boost-di-extensions/Factory.hpp @@ -0,0 +1,59 @@ +// +// Copyright (c) 2012-2017 Kris Jusiak (kris at jusiak dot net) +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// taken from https://raw.githubusercontent.com/boost-experimental/di/cpp14/extension/injections/factory.cpp and +// adapted to fit our coding conventions +#pragma once + +#include +#include +#include + +namespace di = boost::di; + + +template +struct IFactory { + virtual ~IFactory() noexcept = default; + virtual std::unique_ptr create(TArgs&&...) = 0; +}; + +template +struct factory_impl; + +template +struct factory_impl> : IFactory { + explicit factory_impl(const TInjector& injector) : injector_((TInjector&)injector) {} + + std::unique_ptr create(TArgs&&... args) override { + // clang-format off + auto injector = di::make_injector( + std::move(injector_) +#if (__clang_major__ == 3) && (__clang_minor__ > 4) || defined(__GCC___) || defined(__MSVC__) + , di::bind().to(std::forward(args))[di::override]... +#else // wknd for clang 3.4 + , di::core::dependency(std::forward(args))... +#endif + ); + // clang-format on + + auto object = injector.template create>(); + injector_ = std::move(injector); + return std::move(object); + } + +private: + TInjector& injector_; +}; + +template +struct Factory { + template + auto operator()(const TInjector& injector, const TDependency&) const { + static auto sp = std::make_shared>(injector); + return sp; + } +}; diff --git a/CMakeLists.txt b/CMakeLists.txt index 54ea61b1..5bd92a98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,4 +83,4 @@ if (BUILD_TESTS) else() message(" [ ] LCOV Code Coverage Report") endif() -endif() \ No newline at end of file +endif() diff --git a/docs/developers/PlantUml/Application.puml b/docs/developers/PlantUml/Application.puml index 15b2140d..e0cb749d 100644 --- a/docs/developers/PlantUml/Application.puml +++ b/docs/developers/PlantUml/Application.puml @@ -54,41 +54,28 @@ namespace MellowPlayer.Infrastructure { } ApplicationDecorator <|-- WithLogging + + interface IQtApplication #PaleGreen { + setApplicationName(QString value) + setApplicationDisplayName(QString value) + setApplicationVersion(QString value) + setOrganizationDomain(QString value) + setOrganizationName(QString value) + setWindowIcon(const QIcon& icon) + exec() + exit(int returnCode) + installTranslator(QTranslator* translator) + setFont(const QFont& font) + --signals-- + aboutToQuit + commitDataRequest + } + class QtApplication #PaleGreen { + } + IQtApplication <|- QtApplication } namespace MellowPlayer.Presentation { - interface IQtApplication #PaleGreen { - setApplicationName(QString value) - setApplicationDisplayName(QString value) - setApplicationVersion(QString value) - setOrganizationDomain(QString value) - setOrganizationName(QString value) - setWindowIcon(const QIcon& icon) - exec() - exit(int returnCode) - installTranslator(QTranslator* translator) - setFont(const QFont& font) - --signals-- - aboutToQuit - commitDataRequest - } - class QtApplication #PaleGreen { - + setApplicationName(QString value) - + setApplicationDisplayName(QString value) - + setApplicationVersion(QString value) - + setOrganizationDomain(QString value) - + setOrganizationName(QString value) - + setWindowIcon(const QIcon& icon) - + exec() - + exit(int returnCode) - + installTranslator(QTranslator* translator) - + setFont(const QFont& font) - --signals-- - + aboutToQuit - + commitDataRequest - - } - IQtApplication <|-- QtApplication class Application #PaleGreen { + quitRequested: signal + requestQuit() @@ -103,7 +90,7 @@ namespace MellowPlayer.Presentation { } MellowPlayer.Infrastructure.IApplication <|.. Application ContextProperty <|- Application - Application -down-> IQtApplication + Application -> MellowPlayer.Infrastructure.IQtApplication interface IQmlApplicationEngine #PaleGreen { setContextProperty(QString, QObject*) diff --git a/lib/MellowPlayer/Presentation/Application/QtApplication.cpp b/lib/MellowPlayer/Infrastructure/Application/QtApplication.cpp similarity index 96% rename from lib/MellowPlayer/Presentation/Application/QtApplication.cpp rename to lib/MellowPlayer/Infrastructure/Application/QtApplication.cpp index a73a5a60..bb9980af 100644 --- a/lib/MellowPlayer/Presentation/Application/QtApplication.cpp +++ b/lib/MellowPlayer/Infrastructure/Application/QtApplication.cpp @@ -3,7 +3,7 @@ #include using namespace std; -using namespace MellowPlayer::Presentation; +using namespace MellowPlayer::Infrastructure; QtApplication::QtApplication(int argc, char** argv) : qApplication_(argc, argv) diff --git a/lib/MellowPlayer/Presentation/Application/QtApplication.hpp b/lib/MellowPlayer/Infrastructure/Application/QtApplication.hpp similarity index 97% rename from lib/MellowPlayer/Presentation/Application/QtApplication.hpp rename to lib/MellowPlayer/Infrastructure/Application/QtApplication.hpp index 16a1063d..222c4be0 100644 --- a/lib/MellowPlayer/Presentation/Application/QtApplication.hpp +++ b/lib/MellowPlayer/Infrastructure/Application/QtApplication.hpp @@ -5,7 +5,7 @@ class QTranslator; class QIcon; -namespace MellowPlayer::Presentation +namespace MellowPlayer::Infrastructure { class IQtApplication : public QObject { diff --git a/lib/MellowPlayer/Infrastructure/Application/SingleInstance.cpp b/lib/MellowPlayer/Infrastructure/Application/SingleInstance.cpp new file mode 100644 index 00000000..4cfbf2e9 --- /dev/null +++ b/lib/MellowPlayer/Infrastructure/Application/SingleInstance.cpp @@ -0,0 +1,132 @@ +#include "SingleInstance.hpp" +#include "QtApplication.hpp" +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace MellowPlayer::Domain; +using namespace MellowPlayer::Infrastructure; + +const QString SingleInstance::playPauseAction_ = "play-pause"; +const QString SingleInstance::nextAction_ = "next"; +const QString SingleInstance::previousAction_ = "previous"; +const QString SingleInstance::restoreWindowAction_ = "restore-window"; +const QString SingleInstance::toggleFavoriteAction_ = "toggle-favorite"; + +SingleInstance::SingleInstance(IApplication& application, + IQtApplication& qtApplication, + IPlayer& currentPlayer, + ICommandLineArguments& commandLineArguments, + IFactory& localServerFactory, + IFactory& localSocketFactory) + : ApplicationDecorator(application), + logger_(LoggingManager::logger("SingleInstance")), + qtApplication_(qtApplication), + currentPlayer_(currentPlayer), + commandLineArguments_(commandLineArguments), + localServerFactory_(localServerFactory), + localSocketFactory_(localSocketFactory), + lockFile_(QDir::tempPath() + QDir::separator() + qApp->applicationName() + ".lock"), + isPrimary_(false) +{ + lockFile_.setStaleLockTime(0); +} + +void SingleInstance::initialize() +{ + if (lockFile_.tryLock(100)) { + LOG_DEBUG(logger_, "Initializing primary application"); + isPrimary_ = true; + application_.initialize(); + } +} + +int SingleInstance::run() +{ + return isPrimary_ ? runPrimaryApplication() : runSecondaryApplication(); +} + +bool SingleInstance::isPrimary() const +{ + return isPrimary_; +} + +int SingleInstance::runPrimaryApplication() +{ + LOG_DEBUG(logger_, "Running primary application"); + + localServer_ = localServerFactory_.create(qApp->applicationName()); + connect(localServer_.get(), &ILocalServer::newConnection, this, &SingleInstance::onSecondaryApplicationConnection); + localServer_->listen(); + + return application_.run(); +} + +void SingleInstance::onSecondaryApplicationConnection() +{ + LOG_DEBUG(logger_, "Another application was started, showing this one instead"); + localSocket_ = localServer_->nextPendingConnection(); + connect(localSocket_.get(), &ILocalSocket::readyRead, this, &SingleInstance::onSecondaryApplicationActionRequest); +} + +void SingleInstance::onSecondaryApplicationActionRequest() +{ + QString action = QString(localSocket_->readAll()).split("\n")[0]; + LOG_DEBUG(logger_, "Secondary application request: " << action); + + if (action == playPauseAction_) + currentPlayer_.togglePlayPause(); + else if (action == nextAction_) + currentPlayer_.next(); + else if (action == previousAction_) + currentPlayer_.previous(); + else if (action == toggleFavoriteAction_) + currentPlayer_.toggleFavoriteSong(); + else + application_.restoreWindow(); +} + +int SingleInstance::runSecondaryApplication() +{ + LOG_DEBUG(logger_, "Running secondary application"); + + localSocket_ = localSocketFactory_.create(); + localSocket_->connectToServer(qApp->applicationName(), QIODevice::WriteOnly); + connect(localSocket_.get(), &ILocalSocket::connected, this, &SingleInstance::onConnectedToPrimaryApplication); + connect(localSocket_.get(), &ILocalSocket::error, this, &SingleInstance::onConnectionErrorWithPrimaryApplication); + + return qtApplication_.exec(); +} + +void SingleInstance::onConnectedToPrimaryApplication() +{ + LOG_INFO(logger_, "connection with the primary application succeeded, transmitting command line arguments " + "and quitting..."); + QString action = requestedAcion(); + localSocket_->write(action + "\n"); + qtApplication_.exit(1); +} + +void SingleInstance::onConnectionErrorWithPrimaryApplication() +{ + LOG_INFO(logger_, "could not connect to the primary application, quitting..."); + qtApplication_.exit(2); +} + +QString SingleInstance::requestedAcion() const +{ + if (commandLineArguments_.playPauseRequested()) + return playPauseAction_; + else if (commandLineArguments_.nextRequested()) + return nextAction_; + else if (commandLineArguments_.previousRequested()) + return previousAction_; + else if (commandLineArguments_.toggleFavoriteRequested()) + return toggleFavoriteAction_; + return restoreWindowAction_; +} diff --git a/lib/MellowPlayer/Infrastructure/Application/SingleInstance.hpp b/lib/MellowPlayer/Infrastructure/Application/SingleInstance.hpp new file mode 100644 index 00000000..768e2c92 --- /dev/null +++ b/lib/MellowPlayer/Infrastructure/Application/SingleInstance.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "ApplicationDecorator.hpp" +#include +#include + +namespace MellowPlayer::Domain +{ + class ILogger; + class IPlayer; +} + +namespace MellowPlayer::Infrastructure +{ + class IQtApplication; + class ILocalServer; + class ILocalSocket; + class ICommandLineArguments; + + class SingleInstance: public ApplicationDecorator + { + public: + SingleInstance(IApplication& application, + IQtApplication& qtApplication, + Domain::IPlayer& currentPlayer, + ICommandLineArguments& commandLineArguments, + IFactory& localServer, + IFactory& localSocket); + + void initialize() override final; + int run() override final; + + bool isPrimary() const; + + private: + int runPrimaryApplication(); + void onSecondaryApplicationConnection(); + void onSecondaryApplicationActionRequest(); + + int runSecondaryApplication(); + void onConnectedToPrimaryApplication(); + void onConnectionErrorWithPrimaryApplication(); + QString requestedAcion() const; + + Domain::ILogger& logger_; + IQtApplication& qtApplication_; + Domain::IPlayer& currentPlayer_; + ICommandLineArguments& commandLineArguments_; + IFactory& localServerFactory_; + IFactory& localSocketFactory_; + std::unique_ptr localServer_; + std::unique_ptr localSocket_; + QLockFile lockFile_; + bool isPrimary_; + + static const QString playPauseAction_; + static const QString nextAction_; + static const QString previousAction_; + static const QString restoreWindowAction_; + static const QString toggleFavoriteAction_; + }; +} diff --git a/lib/MellowPlayer/Infrastructure/Network/LocalServer.cpp b/lib/MellowPlayer/Infrastructure/Network/LocalServer.cpp new file mode 100644 index 00000000..e272cf91 --- /dev/null +++ b/lib/MellowPlayer/Infrastructure/Network/LocalServer.cpp @@ -0,0 +1,35 @@ +#include "LocalServer.hpp" +#include "LocalSocket.hpp" + +using namespace std; +using namespace MellowPlayer::Infrastructure; + +LocalServer::LocalServer(IFactory& localSocketFactory, const QString& serverName) + : localSocketFactory_(localSocketFactory), serverName_(serverName) +{ + QLocalServer::removeServer(serverName); + connect(&qLocalServer_, &QLocalServer::newConnection, this, &ILocalServer::newConnection); +} + +LocalServer::~LocalServer() +{ + close(); +} + +void LocalServer::close() +{ + qLocalServer_.close(); +} + +bool LocalServer::listen() +{ + return qLocalServer_.listen(serverName_); +} + +unique_ptr LocalServer::nextPendingConnection() +{ + QLocalSocket* qLocalSocket = qLocalServer_.nextPendingConnection(); + unique_ptr localSocket = localSocketFactory_.create(); + localSocket->setQLocalSocket(qLocalSocket); + return localSocket; +} diff --git a/lib/MellowPlayer/Infrastructure/Network/LocalServer.hpp b/lib/MellowPlayer/Infrastructure/Network/LocalServer.hpp new file mode 100644 index 00000000..cf52d8ec --- /dev/null +++ b/lib/MellowPlayer/Infrastructure/Network/LocalServer.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +namespace MellowPlayer::Infrastructure +{ + class ILocalSocket; + + class ILocalServer: public QObject + { + Q_OBJECT + public: + virtual void close() = 0; + virtual bool listen() = 0; + virtual std::unique_ptr nextPendingConnection() = 0; + + signals: + void newConnection(); + }; + + class LocalServer: public ILocalServer + { + public: + LocalServer(IFactory& localSocketFactory, const QString& serverName); + ~LocalServer(); + + void close() override; + bool listen() override; + std::unique_ptr nextPendingConnection() override; + + private: + IFactory& localSocketFactory_; + QString serverName_; + QLocalServer qLocalServer_; + }; +} diff --git a/lib/MellowPlayer/Infrastructure/Network/LocalSocket.cpp b/lib/MellowPlayer/Infrastructure/Network/LocalSocket.cpp new file mode 100644 index 00000000..f4d6f62c --- /dev/null +++ b/lib/MellowPlayer/Infrastructure/Network/LocalSocket.cpp @@ -0,0 +1,50 @@ +#include "LocalSocket.hpp" + +using namespace MellowPlayer::Infrastructure; + +LocalSocket::LocalSocket(): qLocalSocket_(new QLocalSocket(this)) +{ + connect(qLocalSocket_, &QLocalSocket::connected, this, &ILocalSocket::connected); + connect(qLocalSocket_, &QLocalSocket::readyRead, this, &ILocalSocket::readyRead); + connect(qLocalSocket_, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(error())); +} + +LocalSocket::~LocalSocket() +{ + disconnectFromServer(); +} + +void LocalSocket::connectToServer(const QString& name, QIODevice::OpenMode openMode) +{ + qLocalSocket_->connectToServer(name, openMode); +} + +void LocalSocket::disconnectFromServer() +{ + qLocalSocket_->disconnectFromServer(); +} + +void LocalSocket::write(const QString& data) +{ + if (qLocalSocket_->state() == QLocalSocket::ConnectedState) + { + qLocalSocket_->write(data.toLocal8Bit()); + qLocalSocket_->waitForBytesWritten(); + } + else + throw std::logic_error("cannot write data on a socket that is not connected"); +} + +void LocalSocket::setQLocalSocket(QLocalSocket* localSocket) +{ + delete qLocalSocket_; + qLocalSocket_ = localSocket; + connect(qLocalSocket_, &QLocalSocket::connected, this, &ILocalSocket::connected); + connect(qLocalSocket_, &QLocalSocket::readyRead, this, &ILocalSocket::readyRead); + connect(qLocalSocket_, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(error())); +} + +QString LocalSocket::readAll() +{ + return qLocalSocket_->readAll(); +} diff --git a/lib/MellowPlayer/Infrastructure/Network/LocalSocket.hpp b/lib/MellowPlayer/Infrastructure/Network/LocalSocket.hpp new file mode 100644 index 00000000..8f2224f9 --- /dev/null +++ b/lib/MellowPlayer/Infrastructure/Network/LocalSocket.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +namespace MellowPlayer::Infrastructure +{ + class ILocalSocket: public QObject + { + Q_OBJECT + public: + virtual void connectToServer(const QString &name, QIODevice::OpenMode openMode = QIODevice::ReadWrite) = 0; + virtual void disconnectFromServer() = 0; + virtual void write(const QString& data) = 0; + virtual QString readAll() = 0; + virtual void setQLocalSocket(QLocalSocket* localSocket) = 0; + + signals: + void connected(); + void error(); + void readyRead(); + }; + + class LocalSocket: public ILocalSocket + { + public: + LocalSocket(); + + virtual ~LocalSocket(); + + void connectToServer(const QString& name, QIODevice::OpenMode openMode) override; + void disconnectFromServer() override; + void write(const QString& data) override; + QString readAll() override; + void setQLocalSocket(QLocalSocket* localSocket) override; + + private: + QLocalSocket* qLocalSocket_; + }; +} diff --git a/lib/MellowPlayer/Presentation/Application/Application.cpp b/lib/MellowPlayer/Presentation/Application.cpp similarity index 100% rename from lib/MellowPlayer/Presentation/Application/Application.cpp rename to lib/MellowPlayer/Presentation/Application.cpp diff --git a/lib/MellowPlayer/Presentation/Application/Application.hpp b/lib/MellowPlayer/Presentation/Application.hpp similarity index 82% rename from lib/MellowPlayer/Presentation/Application/Application.hpp rename to lib/MellowPlayer/Presentation/Application.hpp index c423d7cc..66c117ec 100644 --- a/lib/MellowPlayer/Presentation/Application/Application.hpp +++ b/lib/MellowPlayer/Presentation/Application.hpp @@ -4,7 +4,7 @@ #include #include #include -#include "QtApplication.hpp" +#include class ApplicationStrings : public QObject { @@ -22,7 +22,7 @@ namespace MellowPlayer::Presentation { Q_OBJECT public: - Application(IQtApplication& qtApplication, IContextProperties& contextProperties); + Application(Infrastructure::IQtApplication& qtApplication, IContextProperties& contextProperties); // ContextProperty QString name() const override; @@ -45,7 +45,7 @@ namespace MellowPlayer::Presentation void registerMetaTypes(); void setupFont(); - IQtApplication& qtApp_; + Infrastructure::IQtApplication& qtApp_; QTranslator translator_; bool restartRequested_; }; diff --git a/tests/IntegrationTests/Infrastructure/Network/LocalServerIntegrationTests.cpp b/tests/IntegrationTests/Infrastructure/Network/LocalServerIntegrationTests.cpp new file mode 100644 index 00000000..adad4138 --- /dev/null +++ b/tests/IntegrationTests/Infrastructure/Network/LocalServerIntegrationTests.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include + +using namespace std; +using namespace MellowPlayer::Infrastructure; + +class LocalSocketFactory: public IFactory +{ +public: + std::unique_ptr create() override + { + return make_unique(); + } +}; + +SCENARIO("LocalServer and LocalSocket integration tests") +{ + GIVEN("A local server and a local socket") + { + QString serverName = "MellowPlayerLocalServerIntegrationTests"; + LocalSocketFactory factory; + LocalServer server(factory, serverName); + server.listen(); + + LocalSocket socket; + unique_ptr newConnection = nullptr; + QObject::connect(&server, &ILocalServer::newConnection, [&]() { + newConnection = server.nextPendingConnection(); + }); + + WHEN("I connect the socket to the server") + { + socket.connectToServer(serverName, QIODevice::WriteOnly); + + QTest::qWait(1000); + + THEN("a new connection has been received") + { + REQUIRE(newConnection != nullptr); + } + + AND_WHEN("writing on the socket") + { + QString receivedData; + + QObject::connect(newConnection.get(), &ILocalSocket::readyRead, [&]() { + receivedData = newConnection->readAll(); + }); + socket.write("foo\n"); + + QTest::qWait(1000); + + THEN("data is received on the server") + { + REQUIRE(receivedData == "foo\n"); + } + } + } + } +} diff --git a/tests/UnitTests/Infrastructure/Application/FakeApplication.cpp b/tests/UnitTests/Infrastructure/Application/FakeApplication.cpp deleted file mode 100644 index 7f32d66c..00000000 --- a/tests/UnitTests/Infrastructure/Application/FakeApplication.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "FakeApplication.hpp" - -void MellowPlayer::Infrastructure::Tests::FakeApplication::initialize() -{ - isInitialized = true; - emit initialized(); -} - -int MellowPlayer::Infrastructure::Tests::FakeApplication::run() -{ - isRunning = true; - return returnCode; -} - -void MellowPlayer::Infrastructure::Tests::FakeApplication::quit() -{ - quitRequested = true; -} - -void MellowPlayer::Infrastructure::Tests::FakeApplication::restart() -{ - restartRequested = true; -} - -void MellowPlayer::Infrastructure::Tests::FakeApplication::restoreWindow() -{ - restoreWindowRequested = true; -} diff --git a/tests/UnitTests/Infrastructure/Application/FakeApplication.hpp b/tests/UnitTests/Infrastructure/Application/FakeApplication.hpp index 498085f4..57ee5dae 100644 --- a/tests/UnitTests/Infrastructure/Application/FakeApplication.hpp +++ b/tests/UnitTests/Infrastructure/Application/FakeApplication.hpp @@ -7,11 +7,32 @@ namespace MellowPlayer::Infrastructure::Tests class FakeApplication: public IApplication { public: - void initialize() override; - int run() override; - void quit() override; - void restart() override; - void restoreWindow() override; + void initialize() override + { + isInitialized = true; + emit initialized(); + } + + int run() override + { + isRunning = true; + return returnCode; + } + + void quit() override + { + quitRequested = true; + } + + void restart() override + { + restartRequested = true; + } + + void restoreWindow() override + { + restoreWindowRequested = true; + } int returnCode = 0; bool isInitialized = false; diff --git a/tests/UnitTests/Presentation/Application/FakeQtApplication.hpp b/tests/UnitTests/Infrastructure/Application/FakeQtApplication.hpp similarity index 93% rename from tests/UnitTests/Presentation/Application/FakeQtApplication.hpp rename to tests/UnitTests/Infrastructure/Application/FakeQtApplication.hpp index 2fc00bf7..5e31c446 100644 --- a/tests/UnitTests/Presentation/Application/FakeQtApplication.hpp +++ b/tests/UnitTests/Infrastructure/Application/FakeQtApplication.hpp @@ -2,8 +2,9 @@ #include #include +#include -namespace MellowPlayer::Presentation::Tests +namespace MellowPlayer::Infrastructure::Tests { class FakeQtApplication: public IQtApplication { diff --git a/tests/UnitTests/Infrastructure/Application/SingleInstanceTests.cpp b/tests/UnitTests/Infrastructure/Application/SingleInstanceTests.cpp new file mode 100644 index 00000000..d34fa88f --- /dev/null +++ b/tests/UnitTests/Infrastructure/Application/SingleInstanceTests.cpp @@ -0,0 +1,206 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace MellowPlayer::Infrastructure; +using namespace MellowPlayer::Infrastructure::Tests; + +SCENARIO("SingleInstance tests") +{ + FakeLocalSocketFactory primaryLocalSocketFactory; + FakeLocalServerFactory primaryLocalServerFactory; + + FakeLocalSocketFactory secondaryLocalSocketFactory; + FakeLocalServerFactory secondaryLocalServerFactory; + FakeCommandLineArguments commandLineArguments; + + auto playerMock = PlayerMock::get(); + + GIVEN("Two SingleInstance application") + { + FakeApplication primaryDecorated; + FakeQtApplication primaryQtApplication; + SingleInstance primaryInstance(primaryDecorated, + primaryQtApplication, + playerMock.get(), + commandLineArguments, + primaryLocalServerFactory, + primaryLocalSocketFactory); + + FakeApplication secondaryDecorated; + FakeQtApplication secondaryQtApplication; + SingleInstance secondaryInstance(secondaryDecorated, + secondaryQtApplication, + playerMock.get(), + commandLineArguments, + secondaryLocalServerFactory, + secondaryLocalSocketFactory); + + WHEN("I initialize the first application") + { + QSignalSpy initSpy(&primaryInstance, &IApplication::initialized); + primaryInstance.initialize(); + + THEN("application is primary") + { + REQUIRE(primaryInstance.isPrimary()); + } + + AND_THEN("decorated is initialized too") + { + REQUIRE(primaryDecorated.isInitialized); + } + + AND_THEN("initialized signal has been emitted") + { + REQUIRE(initSpy.count() == 1); + } + + AND_WHEN("I run the first application") + { + primaryInstance.run(); + + THEN("decorated is running") + { + REQUIRE(primaryDecorated.isRunning); + } + + AND_THEN("server is listening") + { + REQUIRE(primaryLocalServerFactory.lastServerCreated->isListening); + } + + FakeLocalServer* primaryServer = primaryLocalServerFactory.lastServerCreated; + emit primaryServer->newConnection(); + FakeLocalSocket* secondarySocket = primaryServer->newConnectionSocket; + + AND_WHEN("the first application receive a 'play-pause' action request from a secondary application") + { + secondarySocket->data = "play-pause"; + emit secondarySocket->readyRead(); + + THEN("it toggle play/pause on the current player") + { + Verify(Method(playerMock, togglePlayPause)).Once(); + } + } + + AND_WHEN("the first application receive a 'next' action request from a secondary application") + { + secondarySocket->data = "next"; + emit secondarySocket->readyRead(); + + THEN("it skips to the next song") + { + Verify(Method(playerMock, next)).Once(); + } + } + + AND_WHEN("the first application receive a 'previous' action request from a secondary application") + { + secondarySocket->data = "previous"; + emit secondarySocket->readyRead(); + + THEN("it skips to the previous songs") + { + Verify(Method(playerMock, previous)).Once(); + } + } + + AND_WHEN("the first application receive a 'restore-window' action request from a secondary application") + { + secondarySocket->data = "restore-windowe"; + emit secondarySocket->readyRead(); + + THEN("it restores the main window") + { + REQUIRE(primaryDecorated.restoreWindowRequested); + } + } + + AND_WHEN("the first application receive a 'toggle-favorite' action request from a secondary application") + { + secondarySocket->data = "toggle-favorite"; + emit secondarySocket->readyRead(); + + THEN("it toggle favorite on the current songs") + { + Verify(Method(playerMock, toggleFavoriteSong)).Once(); + } + } + } + + AND_WHEN("I initialize the second application") + { + QSignalSpy secondaryInitSpy(&primaryInstance, &IApplication::initialized); + secondaryInstance.initialize(); + + THEN("the second application is not primary") + { + REQUIRE(!secondaryInstance.isPrimary()); + } + + AND_THEN("decorated is not initialized") + { + REQUIRE(!secondaryDecorated.isInitialized); + } + + AND_THEN("initialized signal has not been emitted") + { + REQUIRE(secondaryInitSpy.count() == 0); + } + + AND_WHEN("I run the second application") + { + secondaryInstance.run(); + + THEN("decorated is not running") + { + REQUIRE(!secondaryDecorated.isRunning); + } + + AND_THEN("socket is connected to server") + { + REQUIRE(secondaryLocalSocketFactory.lastSocketCreated->isConnected); + } + + AND_THEN("qt event loop is running") + { + REQUIRE(secondaryQtApplication.isRunning); + } + + AND_WHEN("it is connected to the primary instance") + { + emit secondaryLocalSocketFactory.lastSocketCreated->connected(); + + THEN("it tells the primary application to restore the main window") + { + REQUIRE(secondaryLocalSocketFactory.lastSocketCreated->writtenData == "restore-window\n"); + } + + AND_THEN("it quits the application with exit code 1") + { + REQUIRE(secondaryQtApplication.requestedExitCode == 1); + } + } + + AND_WHEN("it failed to connect to the primary instance") + { + emit secondaryLocalSocketFactory.lastSocketCreated->error(); + + THEN("it quits the application with exit code 2") + { + REQUIRE(secondaryQtApplication.requestedExitCode == 2); + } + } + } + } + } + } +} diff --git a/tests/UnitTests/Infrastructure/Network/FakeLocalServer.hpp b/tests/UnitTests/Infrastructure/Network/FakeLocalServer.hpp new file mode 100644 index 00000000..39fa92d8 --- /dev/null +++ b/tests/UnitTests/Infrastructure/Network/FakeLocalServer.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include "FakeLocalSocket.hpp" + +namespace MellowPlayer::Infrastructure::Tests +{ + class FakeLocalServer: public ILocalServer + { + public: + virtual ~FakeLocalServer() + { + close(); + } + + void close() override + { + closed = true; + } + + bool listen() override + { + isListening = true; + return true; + } + + std::unique_ptr nextPendingConnection() override + { + auto socket = std::make_unique(); + newConnectionSocket = socket.get(); + return std::move(socket); + } + + bool closed = false; + bool isListening = false; + FakeLocalSocket* newConnectionSocket; + }; + + class FakeLocalServerFactory: public IFactory + { + public: + std::unique_ptr create(QString&&) override + { + auto localServer = std::make_unique(); + lastServerCreated = localServer.get(); + return std::move(localServer); + } + + FakeLocalServer* lastServerCreated; + }; +} diff --git a/tests/UnitTests/Infrastructure/Network/FakeLocalSocket.hpp b/tests/UnitTests/Infrastructure/Network/FakeLocalSocket.hpp new file mode 100644 index 00000000..7d7f3532 --- /dev/null +++ b/tests/UnitTests/Infrastructure/Network/FakeLocalSocket.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include + +namespace MellowPlayer::Infrastructure::Tests +{ + class FakeLocalSocket: public ILocalSocket + { + public: + virtual ~FakeLocalSocket() + { + disconnectFromServer(); + } + + void connectToServer(const QString& name, QIODevice::OpenMode) override + { + serverName = name; + isConnected = true; + } + + void disconnectFromServer() override + { + isDisconnected = true; + } + + void write(const QString& data) override + { + writtenData += data; + } + + QString readAll() override + { + return data; + } + + QString writtenData; + QLocalSocket* qLocalSocket = nullptr; + bool isConnected = false; + bool isDisconnected = false; + QString serverName; + QString data; + + protected: + void setQLocalSocket(QLocalSocket* localSocket) override + { + qLocalSocket = localSocket; + } + }; + + class FakeLocalSocketFactory: public IFactory + { + public: + std::unique_ptr create() override + { + auto socket = std::make_unique(); + lastSocketCreated = socket.get(); + return std::move(socket); + } + + FakeLocalSocket* lastSocketCreated; + }; +} diff --git a/tests/UnitTests/Presentation/Application/ApplicationTests.cpp b/tests/UnitTests/Presentation/Application/ApplicationTests.cpp index efe88851..8ce2468c 100644 --- a/tests/UnitTests/Presentation/Application/ApplicationTests.cpp +++ b/tests/UnitTests/Presentation/Application/ApplicationTests.cpp @@ -1,12 +1,13 @@ #include #include -#include +#include #include -#include "FakeQtApplication.hpp" +#include "UnitTests/Infrastructure/Application/FakeQtApplication.hpp" #include using namespace MellowPlayer::Domain; using namespace MellowPlayer::Infrastructure; +using namespace MellowPlayer::Infrastructure::Tests; using namespace MellowPlayer::Presentation; using namespace MellowPlayer::Presentation::Tests;