openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #34297
[Merge] lp:~raoul-snyman/openlp/zeroconf into lp:openlp
Raoul Snyman has proposed merging lp:~raoul-snyman/openlp/zeroconf into lp:openlp.
Commit message:
Add Zeroconf services to OpenLP so that external devices can find OpenLP on the network.
Requested reviews:
OpenLP Core (openlp-core)
For more details, see:
https://code.launchpad.net/~raoul-snyman/openlp/zeroconf/+merge/369554
Add Zeroconf services to OpenLP so that external devices can find OpenLP on the network.
--
Your team OpenLP Core is requested to review the proposed merge of lp:~raoul-snyman/openlp/zeroconf into lp:openlp.
=== added file 'openlp/core/api/zeroconf.py'
--- openlp/core/api/zeroconf.py 1970-01-01 00:00:00 +0000
+++ openlp/core/api/zeroconf.py 2019-07-01 22:48:21 +0000
@@ -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 <https://www.gnu.org/licenses/>. #
+##########################################################################
+"""
+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')
=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py 2019-05-24 18:50:51 +0000
+++ openlp/core/ui/mainwindow.py 2019-07-01 22:48:21 +0000
@@ -33,8 +33,9 @@
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 @@
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)
=== modified file 'tests/functional/openlp_core/common/test_json.py'
--- tests/functional/openlp_core/common/test_json.py 2019-05-22 06:47:00 +0000
+++ tests/functional/openlp_core/common/test_json.py 2019-07-01 22:48:21 +0000
@@ -31,7 +31,7 @@
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 @@
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 @@
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 @@
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 @@
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
=== modified file 'tests/interfaces/openlp_core/ui/test_mainwindow.py'
--- tests/interfaces/openlp_core/ui/test_mainwindow.py 2019-04-13 13:00:22 +0000
+++ tests/interfaces/openlp_core/ui/test_mainwindow.py 2019-07-01 22:48:21 +0000
@@ -62,9 +62,10 @@
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):
=== added directory 'tests/openlp_core/api'
=== added file 'tests/openlp_core/api/test_zeroconf.py'
--- tests/openlp_core/api/test_zeroconf.py 1970-01-01 00:00:00 +0000
+++ tests/openlp_core/api/test_zeroconf.py 2019-07-01 22:48:21 +0000
@@ -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 <https://www.gnu.org/licenses/>. #
+##########################################################################
+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')
=== modified file 'tests/openlp_core/projectors/test_projector_db.py'
--- tests/openlp_core/projectors/test_projector_db.py 2019-04-13 13:00:22 +0000
+++ tests/openlp_core/projectors/test_projector_db.py 2019-07-01 22:48:21 +0000
@@ -153,8 +153,9 @@
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()
References