diff --git a/openlp/core/api/zeroconf.py b/openlp/core/api/zeroconf.py new file mode 100644 index 000000000..d43e13aa6 --- /dev/null +++ b/openlp/core/api/zeroconf.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2019 OpenLP Developers # +# ---------------------------------------------------------------------- # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +########################################################################## +""" +The :mod:`~openlp.core.api.zeroconf` module runs a Zerconf server so that OpenLP can advertise the +RESTful API for devices on the network to discover. +""" +import socket +from time import sleep + +from zeroconf import ServiceInfo, Zeroconf + +from openlp.core.common import get_local_ip4 +from openlp.core.common.registry import Registry +from openlp.core.common.settings import Settings +from openlp.core.threading import ThreadWorker, run_thread + + +class ZeroconfWorker(ThreadWorker): + """ + This thread worker runs a Zeroconf service + """ + address = None + http_port = 4316 + ws_port = 4317 + _can_run = False + + def __init__(self, ip_address, http_port=4316, ws_port=4317): + """ + Create the worker for the Zeroconf service + """ + super().__init__() + self.address = socket.inet_aton(ip_address) + self.http_port = http_port + self.ws_port = ws_port + + def can_run(self): + """ + Check if the worker can continue to run. This is mostly so that we can override this method + and test the class. + """ + return self._can_run + + def start(self): + """ + Start the service + """ + http_info = ServiceInfo('_http._tcp.local.', 'OpenLP._http._tcp.local.', + address=self.address, port=self.http_port, properties={}) + ws_info = ServiceInfo('_ws._tcp.local.', 'OpenLP._ws._tcp.local.', + address=self.address, port=self.ws_port, properties={}) + zc = Zeroconf() + zc.register_service(http_info) + zc.register_service(ws_info) + self._can_run = True + while self.can_run(): + sleep(0.1) + zc.unregister_service(http_info) + zc.unregister_service(ws_info) + zc.close() + + def stop(self): + """ + Stop the service + """ + self._can_run = False + + +def start_zeroconf(): + """ + Start the Zeroconf service + """ + # When we're running tests, just skip this set up if this flag is set + if Registry().get_flag('no_web_server'): + return + ifaces = get_local_ip4() + for key in iter(ifaces): + address = ifaces.get(key)['ip'] + break + http_port = Settings().value('api/port') + ws_port = Settings().value('api/websocket port') + worker = ZeroconfWorker(address, http_port, ws_port) + run_thread(worker, 'api_zeroconf') diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 6ce938253..2313e4705 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -33,8 +33,9 @@ from tempfile import gettempdir from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.state import State -from openlp.core.api import websockets -from openlp.core.api.http import server +from openlp.core.api.websockets import WebSocketServer +from openlp.core.api.http.server import HttpServer +from openlp.core.api.zeroconf import start_zeroconf from openlp.core.common import add_actions, is_macosx, is_win from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.applocation import AppLocation @@ -495,8 +496,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert self.copy_data = False Settings().set_up_default_values() self.about_form = AboutForm(self) - self.ws_server = websockets.WebSocketServer() - self.http_server = server.HttpServer(self) + self.ws_server = WebSocketServer() + self.http_server = HttpServer(self) + start_zeroconf() SettingsForm(self) self.formatting_tag_form = FormattingTagForm(self) self.shortcut_form = ShortcutListForm(self) diff --git a/tests/functional/openlp_core/common/test_json.py b/tests/functional/openlp_core/common/test_json.py index d0b8c9506..4fd2ad1d6 100644 --- a/tests/functional/openlp_core/common/test_json.py +++ b/tests/functional/openlp_core/common/test_json.py @@ -31,7 +31,7 @@ from unittest.mock import patch from openlp.core.common.json import JSONMixin, OpenLPJSONDecoder, OpenLPJSONEncoder, PathSerializer, _registered_classes -class TestClassBase(object): +class BaseTestClass(object): """ Simple class to avoid repetition """ @@ -81,7 +81,7 @@ class TestJSONMixin(TestCase): Test that an instance of a JSONMixin subclass is properly serialized to a JSON string """ # GIVEN: A instance of a subclass of the JSONMixin class - class TestClass(TestClassBase, JSONMixin): + class TestClass(BaseTestClass, JSONMixin): _json_keys = ['a', 'b'] instance = TestClass(a=1, c=2) @@ -97,7 +97,7 @@ class TestJSONMixin(TestCase): Test that an instance of a JSONMixin subclass is properly deserialized from a JSON string """ # GIVEN: A subclass of the JSONMixin class - class TestClass(TestClassBase, JSONMixin): + class TestClass(BaseTestClass, JSONMixin): _json_keys = ['a', 'b'] # WHEN: Deserializing a JSON representation of the TestClass @@ -115,7 +115,7 @@ class TestJSONMixin(TestCase): Test that an instance of a JSONMixin subclass is properly serialized to a JSON string when using a custom name """ # GIVEN: A instance of a subclass of the JSONMixin class with a custom name - class TestClass(TestClassBase, JSONMixin, register_names=('AltName', )): + class TestClass(BaseTestClass, JSONMixin, register_names=('AltName', )): _json_keys = ['a', 'b'] _name = 'AltName' _version = 2 @@ -134,7 +134,7 @@ class TestJSONMixin(TestCase): name """ # GIVEN: A instance of a subclass of the JSONMixin class with a custom name - class TestClass(TestClassBase, JSONMixin, register_names=('AltName', )): + class TestClass(BaseTestClass, JSONMixin, register_names=('AltName', )): _json_keys = ['a', 'b'] _name = 'AltName' _version = 2 diff --git a/tests/interfaces/openlp_core/ui/test_mainwindow.py b/tests/interfaces/openlp_core/ui/test_mainwindow.py index 386f22c7d..42ca9a4ea 100644 --- a/tests/interfaces/openlp_core/ui/test_mainwindow.py +++ b/tests/interfaces/openlp_core/ui/test_mainwindow.py @@ -62,9 +62,10 @@ class TestMainWindow(TestCase, TestMixin): patch('openlp.core.ui.mainwindow.ServiceManager'), \ patch('openlp.core.ui.mainwindow.ThemeManager'), \ patch('openlp.core.ui.mainwindow.ProjectorManager'), \ - patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \ - patch('openlp.core.ui.mainwindow.PluginForm'), \ - patch('openlp.core.ui.mainwindow.server.HttpServer'): + patch('openlp.core.ui.mainwindow.HttpServer'), \ + patch('openlp.core.ui.mainwindow.WebSocketServer'), \ + patch('openlp.core.ui.mainwindow.start_zeroconf'), \ + patch('openlp.core.ui.mainwindow.PluginForm'): self.main_window = MainWindow() def tearDown(self): diff --git a/tests/openlp_core/api/test_zeroconf.py b/tests/openlp_core/api/test_zeroconf.py new file mode 100644 index 000000000..bcc764743 --- /dev/null +++ b/tests/openlp_core/api/test_zeroconf.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2019 OpenLP Developers # +# ---------------------------------------------------------------------- # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +########################################################################## +from unittest.mock import MagicMock, call, patch + +from openlp.core.api.zeroconf import ZeroconfWorker, start_zeroconf + + +@patch('openlp.core.api.zeroconf.socket.inet_aton') +def test_zeroconf_worker_constructor(mocked_inet_aton): + """Test creating the Zeroconf worker object""" + # GIVEN: A ZeroconfWorker class and a mocked inet_aton + mocked_inet_aton.return_value = 'processed_ip' + + # WHEN: An instance of the ZeroconfWorker is created + worker = ZeroconfWorker('127.0.0.1', 8000, 8001) + + # THEN: The inet_aton function should have been called and the attrs should be set + mocked_inet_aton.assert_called_once_with('127.0.0.1') + assert worker.address == 'processed_ip' + assert worker.http_port == 8000 + assert worker.ws_port == 8001 + + +@patch('openlp.core.api.zeroconf.ServiceInfo') +@patch('openlp.core.api.zeroconf.Zeroconf') +def test_zeroconf_worker_start(MockedZeroconf, MockedServiceInfo): + """Test the start() method of ZeroconfWorker""" + # GIVEN: A few mocks and a ZeroconfWorker instance with a mocked can_run method + mocked_http_info = MagicMock() + mocked_ws_info = MagicMock() + mocked_zc = MagicMock() + MockedServiceInfo.side_effect = [mocked_http_info, mocked_ws_info] + MockedZeroconf.return_value = mocked_zc + worker = ZeroconfWorker('127.0.0.1', 8000, 8001) + + # WHEN: The start() method is called + with patch.object(worker, 'can_run') as mocked_can_run: + mocked_can_run.side_effect = [True, False] + worker.start() + + # THEN: The correct calls are made + assert MockedServiceInfo.call_args_list == [ + call('_http._tcp.local.', 'OpenLP._http._tcp.local.', address=b'\x7f\x00\x00\x01', port=8000, properties={}), + call('_ws._tcp.local.', 'OpenLP._ws._tcp.local.', address=b'\x7f\x00\x00\x01', port=8001, properties={}) + ] + assert MockedZeroconf.call_count == 1 + assert mocked_zc.register_service.call_args_list == [call(mocked_http_info), call(mocked_ws_info)] + assert mocked_can_run.call_count == 2 + assert mocked_zc.unregister_service.call_args_list == [call(mocked_http_info), call(mocked_ws_info)] + assert mocked_zc.close.call_count == 1 + + +def test_zeroconf_worker_stop(): + """Test that the ZeroconfWorker.stop() method correctly stops the service""" + # GIVEN: A worker object with _can_run set to True + worker = ZeroconfWorker('127.0.0.1', 8000, 8001) + worker._can_run = True + + # WHEN: stop() is called + worker.stop() + + # THEN: _can_run should be False + assert worker._can_run is False + + +# @patch('openlp.core.api.zeroconf.get_local_ip4') +@patch('openlp.core.api.zeroconf.Registry') +@patch('openlp.core.api.zeroconf.Settings') +@patch('openlp.core.api.zeroconf.ZeroconfWorker') +@patch('openlp.core.api.zeroconf.run_thread') +def test_start_zeroconf(mocked_run_thread, MockedZeroconfWorker, MockedSettings, MockedRegistry): + """Test the start_zeroconf() function""" + # GIVEN: A whole bunch of stuff that's mocked out + MockedRegistry.return_value.get_flag.return_value = False + MockedSettings.return_value.value.side_effect = [8000, 8001] + mocked_worker = MagicMock() + MockedZeroconfWorker.return_value = mocked_worker + + # WHEN: start_zeroconf() is called + start_zeroconf() + + # THEN: A worker is added to the list of threads + mocked_run_thread.assert_called_once_with(mocked_worker, 'api_zeroconf') diff --git a/tests/openlp_core/projectors/test_projector_db.py b/tests/openlp_core/projectors/test_projector_db.py index a61689478..a96a25467 100644 --- a/tests/openlp_core/projectors/test_projector_db.py +++ b/tests/openlp_core/projectors/test_projector_db.py @@ -153,8 +153,9 @@ class TestProjectorDB(TestCase, TestMixin): patch('openlp.core.ui.mainwindow.ServiceManager'), \ patch('openlp.core.ui.mainwindow.ThemeManager'), \ patch('openlp.core.ui.mainwindow.ProjectorManager'), \ - patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \ - patch('openlp.core.ui.mainwindow.server.HttpServer'), \ + patch('openlp.core.ui.mainwindow.WebSocketServer'), \ + patch('openlp.core.ui.mainwindow.HttpServer'), \ + patch('openlp.core.ui.mainwindow.start_zeroconf'), \ patch('openlp.core.state.State.list_plugins') as mock_plugins: mock_plugins.return_value = [] self.main_window = MainWindow()