Merge lp:~raoul-snyman/openlp/zeroconf into lp:openlp

Proposed by Raoul Snyman
Status: Merged
Approved by: Raoul Snyman
Approved revision: 2890
Merged at revision: 2885
Proposed branch: lp:~raoul-snyman/openlp/zeroconf
Merge into: lp:openlp
Diff against target: 768 lines (+336/-128)
14 files modified
openlp/core/api/tab.py (+4/-6)
openlp/core/api/zeroconf.py (+99/-0)
openlp/core/common/__init__.py (+23/-32)
openlp/core/ui/mainwindow.py (+6/-4)
run_openlp.py (+17/-1)
scripts/appveyor.yml (+1/-1)
scripts/check_dependencies.py (+2/-1)
setup.py (+2/-1)
tests/functional/openlp_core/api/test_tab.py (+6/-6)
tests/functional/openlp_core/common/test_json.py (+5/-5)
tests/interfaces/openlp_core/ui/test_mainwindow.py (+4/-3)
tests/openlp_core/api/test_zeroconf.py (+112/-0)
tests/openlp_core/common/test_network_interfaces.py (+52/-66)
tests/openlp_core/projectors/test_projector_db.py (+3/-2)
To merge this branch: bzr merge lp:~raoul-snyman/openlp/zeroconf
Reviewer Review Type Date Requested Status
Tomas Groth Approve
Review via email: mp+369632@code.launchpad.net

This proposal supersedes a proposal from 2019-07-03.

Commit message

Add Zeroconf services to OpenLP so that external devices can find OpenLP on the network.

Description of the change

Add Zeroconf services to OpenLP so that external devices can find OpenLP on the network.

To post a comment you must log in.
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linux tests failed, please see https://ci.openlp.io/job/MP-02-Linux_Tests/201/ for more details

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linux tests passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linting passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

macOS tests failed, please see https://ci.openlp.io/job/MP-04-macOS-Tests/107/ for more details

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

macOS tests passed!

Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

Please see inline comments.

review: Needs Fixing
Revision history for this message
Raoul Snyman (raoul-snyman) : Posted in a previous version of this proposal
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

See inline

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linux tests failed, please see https://ci.openlp.io/job/MP-02-Linux_Tests/203/ for more details

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linux tests passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linting passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

macOS tests passed!

Revision history for this message
Tomas Groth (tomasgroth) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/api/tab.py'
2--- openlp/core/api/tab.py 2019-04-13 13:00:22 +0000
3+++ openlp/core/api/tab.py 2019-07-03 06:34:48 +0000
4@@ -24,7 +24,7 @@
5 """
6 from PyQt5 import QtCore, QtGui, QtWidgets
7
8-from openlp.core.common import get_local_ip4
9+from openlp.core.common import get_network_interfaces
10 from openlp.core.common.i18n import UiStrings, translate
11 from openlp.core.common.registry import Registry
12 from openlp.core.common.settings import Settings
13@@ -194,8 +194,7 @@
14 http_url_temp = http_url + 'main'
15 self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
16
17- @staticmethod
18- def get_ip_address(ip_address):
19+ def get_ip_address(self, ip_address):
20 """
21 returns the IP address in dependency of the passed address
22 ip_address == 0.0.0.0: return the IP address of the first valid interface
23@@ -203,9 +202,8 @@
24 """
25 if ip_address == ZERO_URL:
26 # In case we have more than one interface
27- ifaces = get_local_ip4()
28- for key in iter(ifaces):
29- ip_address = ifaces.get(key)['ip']
30+ for _, interface in get_network_interfaces().items():
31+ ip_address = interface['ip']
32 # We only want the first interface returned
33 break
34 return ip_address
35
36=== added file 'openlp/core/api/zeroconf.py'
37--- openlp/core/api/zeroconf.py 1970-01-01 00:00:00 +0000
38+++ openlp/core/api/zeroconf.py 2019-07-03 06:34:48 +0000
39@@ -0,0 +1,99 @@
40+# -*- coding: utf-8 -*-
41+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
42+
43+##########################################################################
44+# OpenLP - Open Source Lyrics Projection #
45+# ---------------------------------------------------------------------- #
46+# Copyright (c) 2008-2019 OpenLP Developers #
47+# ---------------------------------------------------------------------- #
48+# This program is free software: you can redistribute it and/or modify #
49+# it under the terms of the GNU General Public License as published by #
50+# the Free Software Foundation, either version 3 of the License, or #
51+# (at your option) any later version. #
52+# #
53+# This program is distributed in the hope that it will be useful, #
54+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
55+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
56+# GNU General Public License for more details. #
57+# #
58+# You should have received a copy of the GNU General Public License #
59+# along with this program. If not, see <https://www.gnu.org/licenses/>. #
60+##########################################################################
61+"""
62+The :mod:`~openlp.core.api.zeroconf` module runs a Zerconf server so that OpenLP can advertise the
63+RESTful API for devices on the network to discover.
64+"""
65+import socket
66+from time import sleep
67+
68+from zeroconf import ServiceInfo, Zeroconf
69+
70+from openlp.core.common import get_network_interfaces
71+from openlp.core.common.registry import Registry
72+from openlp.core.common.settings import Settings
73+from openlp.core.threading import ThreadWorker, run_thread
74+
75+
76+class ZeroconfWorker(ThreadWorker):
77+ """
78+ This thread worker runs a Zeroconf service
79+ """
80+ address = None
81+ http_port = 4316
82+ ws_port = 4317
83+ _can_run = False
84+
85+ def __init__(self, ip_address, http_port=4316, ws_port=4317):
86+ """
87+ Create the worker for the Zeroconf service
88+ """
89+ super().__init__()
90+ self.address = socket.inet_aton(ip_address)
91+ self.http_port = http_port
92+ self.ws_port = ws_port
93+
94+ def can_run(self):
95+ """
96+ Check if the worker can continue to run. This is mostly so that we can override this method
97+ and test the class.
98+ """
99+ return self._can_run
100+
101+ def start(self):
102+ """
103+ Start the service
104+ """
105+ http_info = ServiceInfo('_http._tcp.local.', 'OpenLP._http._tcp.local.',
106+ address=self.address, port=self.http_port, properties={})
107+ ws_info = ServiceInfo('_ws._tcp.local.', 'OpenLP._ws._tcp.local.',
108+ address=self.address, port=self.ws_port, properties={})
109+ zc = Zeroconf()
110+ zc.register_service(http_info)
111+ zc.register_service(ws_info)
112+ self._can_run = True
113+ while self.can_run():
114+ sleep(0.1)
115+ zc.unregister_service(http_info)
116+ zc.unregister_service(ws_info)
117+ zc.close()
118+ self.quit.emit()
119+
120+ def stop(self):
121+ """
122+ Stop the service
123+ """
124+ self._can_run = False
125+
126+
127+def start_zeroconf():
128+ """
129+ Start the Zeroconf service
130+ """
131+ # When we're running tests, just skip this set up if this flag is set
132+ if Registry().get_flag('no_web_server'):
133+ return
134+ http_port = Settings().value('api/port')
135+ ws_port = Settings().value('api/websocket port')
136+ for name, interface in get_network_interfaces().items():
137+ worker = ZeroconfWorker(interface['ip'], http_port, ws_port)
138+ run_thread(worker, 'api_zeroconf_{name}'.format(name=name))
139
140=== modified file 'openlp/core/common/__init__.py'
141--- openlp/core/common/__init__.py 2019-06-05 04:53:18 +0000
142+++ openlp/core/common/__init__.py 2019-07-03 06:34:48 +0000
143@@ -51,9 +51,10 @@
144 '\u2013': '-', '\u2014': '-', '\v': '\n\n', '\f': '\n\n'})
145 NEW_LINE_REGEX = re.compile(r' ?(\r\n?|\n) ?')
146 WHITESPACE_REGEX = re.compile(r'[ \t]+')
147-
148-
149-def get_local_ip4():
150+INTERFACE_FILTER = re.compile('lo|loopback|docker|tun', re.IGNORECASE)
151+
152+
153+def get_network_interfaces():
154 """
155 Creates a dictionary of local IPv4 interfaces on local machine.
156 If no active interfaces available, returns a dict of localhost IPv4 information
157@@ -61,43 +62,33 @@
158 :returns: Dict of interfaces
159 """
160 log.debug('Getting local IPv4 interface(es) information')
161- my_ip4 = {}
162- for iface in QNetworkInterface.allInterfaces():
163+ interfaces = {}
164+ for interface in QNetworkInterface.allInterfaces():
165+ interface_name = interface.name()
166+ if INTERFACE_FILTER.search(interface_name):
167+ log.debug('Filtering out interfaces we don\'t care about: {name}'.format(name=interface_name))
168+ continue
169 log.debug('Checking for isValid and flags == IsUP | IsRunning')
170- if not iface.isValid() or not (iface.flags() & (QNetworkInterface.IsUp | QNetworkInterface.IsRunning)):
171+ if not interface.isValid() or not (interface.flags() & (QNetworkInterface.IsUp | QNetworkInterface.IsRunning)):
172 continue
173 log.debug('Checking address(es) protocol')
174- for address in iface.addressEntries():
175+ for address in interface.addressEntries():
176 ip = address.ip()
177 log.debug('Checking for protocol == IPv4Protocol')
178 if ip.protocol() == QAbstractSocket.IPv4Protocol:
179 log.debug('Getting interface information')
180- my_ip4[iface.name()] = {'ip': ip.toString(),
181- 'broadcast': address.broadcast().toString(),
182- 'netmask': address.netmask().toString(),
183- 'prefix': address.prefixLength(),
184- 'localnet': QHostAddress(address.netmask().toIPv4Address() &
185- ip.toIPv4Address()).toString()
186- }
187- log.debug('Adding {iface} to active list'.format(iface=iface.name()))
188- if len(my_ip4) == 0:
189+ interfaces[interface_name] = {
190+ 'ip': ip.toString(),
191+ 'broadcast': address.broadcast().toString(),
192+ 'netmask': address.netmask().toString(),
193+ 'prefix': address.prefixLength(),
194+ 'localnet': QHostAddress(address.netmask().toIPv4Address() &
195+ ip.toIPv4Address()).toString()
196+ }
197+ log.debug('Adding {interface} to active list'.format(interface=interface.name()))
198+ if len(interfaces) == 0:
199 log.warning('No active IPv4 network interfaces detected')
200- return my_ip4
201- if 'localhost' in my_ip4:
202- log.debug('Renaming windows localhost to lo')
203- my_ip4['lo'] = my_ip4['localhost']
204- my_ip4.pop('localhost')
205- if len(my_ip4) == 1:
206- if 'lo' in my_ip4:
207- # No active interfaces - so leave localhost in there
208- log.warning('No active IPv4 interfaces found except localhost')
209- else:
210- # Since we have a valid IP4 interface, remove localhost
211- if 'lo' in my_ip4:
212- log.debug('Found at least one IPv4 interface, removing localhost')
213- my_ip4.pop('lo')
214-
215- return my_ip4
216+ return interfaces
217
218
219 def trace_error_handler(logger):
220
221=== modified file 'openlp/core/ui/mainwindow.py'
222--- openlp/core/ui/mainwindow.py 2019-05-24 18:50:51 +0000
223+++ openlp/core/ui/mainwindow.py 2019-07-03 06:34:48 +0000
224@@ -33,8 +33,9 @@
225 from PyQt5 import QtCore, QtGui, QtWidgets
226
227 from openlp.core.state import State
228-from openlp.core.api import websockets
229-from openlp.core.api.http import server
230+from openlp.core.api.websockets import WebSocketServer
231+from openlp.core.api.http.server import HttpServer
232+from openlp.core.api.zeroconf import start_zeroconf
233 from openlp.core.common import add_actions, is_macosx, is_win
234 from openlp.core.common.actions import ActionList, CategoryOrder
235 from openlp.core.common.applocation import AppLocation
236@@ -495,8 +496,9 @@
237 self.copy_data = False
238 Settings().set_up_default_values()
239 self.about_form = AboutForm(self)
240- self.ws_server = websockets.WebSocketServer()
241- self.http_server = server.HttpServer(self)
242+ self.ws_server = WebSocketServer()
243+ self.http_server = HttpServer(self)
244+ start_zeroconf()
245 SettingsForm(self)
246 self.formatting_tag_form = FormattingTagForm(self)
247 self.shortcut_form = ShortcutListForm(self)
248
249=== modified file 'run_openlp.py'
250--- run_openlp.py 2019-06-05 04:53:18 +0000
251+++ run_openlp.py 2019-07-03 06:34:48 +0000
252@@ -23,6 +23,7 @@
253 """
254 The entrypoint for OpenLP
255 """
256+import atexit
257 import faulthandler
258 import logging
259 import multiprocessing
260@@ -36,18 +37,33 @@
261 from openlp.core.common.path import create_paths
262
263 log = logging.getLogger(__name__)
264+error_log_file = None
265+
266+
267+def tear_down_fault_handling():
268+ """
269+ When Python exits, close the file we were using for the faulthandler
270+ """
271+ global error_log_file
272+ error_log_file.close()
273
274
275 def set_up_fault_handling():
276 """
277 Set up the Python fault handler
278 """
279+ global error_log_file
280 # Create the cache directory if it doesn't exist, and enable the fault handler to log to an error log file
281 try:
282 create_paths(AppLocation.get_directory(AppLocation.CacheDir))
283- faulthandler.enable((AppLocation.get_directory(AppLocation.CacheDir) / 'error.log').open('wb'))
284+ error_log_file = (AppLocation.get_directory(AppLocation.CacheDir) / 'error.log').open('wb')
285+ atexit.register(tear_down_fault_handling)
286+ faulthandler.enable(error_log_file)
287 except OSError:
288 log.exception('An exception occurred when enabling the fault handler')
289+ atexit.unregister(tear_down_fault_handling)
290+ if error_log_file:
291+ error_log_file.close()
292
293
294 def start():
295
296=== modified file 'scripts/appveyor.yml'
297--- scripts/appveyor.yml 2019-06-11 19:27:17 +0000
298+++ scripts/appveyor.yml 2019-07-03 06:34:48 +0000
299@@ -18,7 +18,7 @@
300
301 install:
302 # Install dependencies from pypi
303- - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc Pyro4"
304+ - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc Pyro4 zeroconf"
305
306 build: off
307
308
309=== modified file 'scripts/check_dependencies.py'
310--- scripts/check_dependencies.py 2019-06-11 05:01:02 +0000
311+++ scripts/check_dependencies.py 2019-07-03 06:34:48 +0000
312@@ -90,7 +90,8 @@
313 'requests',
314 'qtawesome',
315 'pymediainfo',
316- 'vlc'
317+ 'vlc',
318+ 'zeroconf'
319 ]
320
321
322
323=== modified file 'setup.py'
324--- setup.py 2019-05-25 14:43:43 +0000
325+++ setup.py 2019-07-03 06:34:48 +0000
326@@ -185,7 +185,8 @@
327 'SQLAlchemy >= 0.5',
328 'waitress',
329 'WebOb',
330- 'websockets'
331+ 'websockets',
332+ 'zeroconf'
333 ],
334 extras_require={
335 'agpl-pdf': ['PyMuPDF'],
336
337=== modified file 'tests/functional/openlp_core/api/test_tab.py'
338--- tests/functional/openlp_core/api/test_tab.py 2019-04-13 13:00:22 +0000
339+++ tests/functional/openlp_core/api/test_tab.py 2019-07-03 06:34:48 +0000
340@@ -28,7 +28,7 @@
341 from PyQt5 import QtWidgets
342
343 from openlp.core.api.tab import ApiTab
344-from openlp.core.common import get_local_ip4
345+from openlp.core.common import get_network_interfaces
346 from openlp.core.common.registry import Registry
347 from openlp.core.common.settings import Settings
348 from tests.helpers.testmixin import TestMixin
349@@ -62,7 +62,7 @@
350 Registry().create()
351 Registry().set_flag('website_version', '00-00-0000')
352 self.form = ApiTab(self.parent)
353- self.my_ip4_list = get_local_ip4()
354+ self.interfaces = get_network_interfaces()
355
356 def tearDown(self):
357 """
358@@ -77,9 +77,9 @@
359 Test the get_ip_address function with ZERO_URL
360 """
361 # GIVEN: list of local IP addresses for this machine
362- ip4_list = []
363- for ip4 in iter(self.my_ip4_list):
364- ip4_list.append(self.my_ip4_list.get(ip4)['ip'])
365+ ip_addresses = []
366+ for _, interface in self.interfaces.items():
367+ ip_addresses.append(interface['ip'])
368
369 # WHEN: the default ip address is given
370 ip_address = self.form.get_ip_address(ZERO_URL)
371@@ -87,7 +87,7 @@
372 # THEN: the default ip address will be returned
373 assert re.match(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', ip_address), \
374 'The return value should be a valid ip address'
375- assert ip_address in ip4_list, 'The return address should be in the list of local IP addresses'
376+ assert ip_address in ip_addresses, 'The return address should be in the list of local IP addresses'
377
378 def test_get_ip_address_with_ip(self):
379 """
380
381=== modified file 'tests/functional/openlp_core/common/test_json.py'
382--- tests/functional/openlp_core/common/test_json.py 2019-05-22 06:47:00 +0000
383+++ tests/functional/openlp_core/common/test_json.py 2019-07-03 06:34:48 +0000
384@@ -31,7 +31,7 @@
385 from openlp.core.common.json import JSONMixin, OpenLPJSONDecoder, OpenLPJSONEncoder, PathSerializer, _registered_classes
386
387
388-class TestClassBase(object):
389+class BaseTestClass(object):
390 """
391 Simple class to avoid repetition
392 """
393@@ -81,7 +81,7 @@
394 Test that an instance of a JSONMixin subclass is properly serialized to a JSON string
395 """
396 # GIVEN: A instance of a subclass of the JSONMixin class
397- class TestClass(TestClassBase, JSONMixin):
398+ class TestClass(BaseTestClass, JSONMixin):
399 _json_keys = ['a', 'b']
400
401 instance = TestClass(a=1, c=2)
402@@ -97,7 +97,7 @@
403 Test that an instance of a JSONMixin subclass is properly deserialized from a JSON string
404 """
405 # GIVEN: A subclass of the JSONMixin class
406- class TestClass(TestClassBase, JSONMixin):
407+ class TestClass(BaseTestClass, JSONMixin):
408 _json_keys = ['a', 'b']
409
410 # WHEN: Deserializing a JSON representation of the TestClass
411@@ -115,7 +115,7 @@
412 Test that an instance of a JSONMixin subclass is properly serialized to a JSON string when using a custom name
413 """
414 # GIVEN: A instance of a subclass of the JSONMixin class with a custom name
415- class TestClass(TestClassBase, JSONMixin, register_names=('AltName', )):
416+ class TestClass(BaseTestClass, JSONMixin, register_names=('AltName', )):
417 _json_keys = ['a', 'b']
418 _name = 'AltName'
419 _version = 2
420@@ -134,7 +134,7 @@
421 name
422 """
423 # GIVEN: A instance of a subclass of the JSONMixin class with a custom name
424- class TestClass(TestClassBase, JSONMixin, register_names=('AltName', )):
425+ class TestClass(BaseTestClass, JSONMixin, register_names=('AltName', )):
426 _json_keys = ['a', 'b']
427 _name = 'AltName'
428 _version = 2
429
430=== modified file 'tests/interfaces/openlp_core/ui/test_mainwindow.py'
431--- tests/interfaces/openlp_core/ui/test_mainwindow.py 2019-04-13 13:00:22 +0000
432+++ tests/interfaces/openlp_core/ui/test_mainwindow.py 2019-07-03 06:34:48 +0000
433@@ -62,9 +62,10 @@
434 patch('openlp.core.ui.mainwindow.ServiceManager'), \
435 patch('openlp.core.ui.mainwindow.ThemeManager'), \
436 patch('openlp.core.ui.mainwindow.ProjectorManager'), \
437- patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
438- patch('openlp.core.ui.mainwindow.PluginForm'), \
439- patch('openlp.core.ui.mainwindow.server.HttpServer'):
440+ patch('openlp.core.ui.mainwindow.HttpServer'), \
441+ patch('openlp.core.ui.mainwindow.WebSocketServer'), \
442+ patch('openlp.core.ui.mainwindow.start_zeroconf'), \
443+ patch('openlp.core.ui.mainwindow.PluginForm'):
444 self.main_window = MainWindow()
445
446 def tearDown(self):
447
448=== added directory 'tests/openlp_core/api'
449=== added file 'tests/openlp_core/api/test_zeroconf.py'
450--- tests/openlp_core/api/test_zeroconf.py 1970-01-01 00:00:00 +0000
451+++ tests/openlp_core/api/test_zeroconf.py 2019-07-03 06:34:48 +0000
452@@ -0,0 +1,112 @@
453+# -*- coding: utf-8 -*-
454+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
455+
456+##########################################################################
457+# OpenLP - Open Source Lyrics Projection #
458+# ---------------------------------------------------------------------- #
459+# Copyright (c) 2008-2019 OpenLP Developers #
460+# ---------------------------------------------------------------------- #
461+# This program is free software: you can redistribute it and/or modify #
462+# it under the terms of the GNU General Public License as published by #
463+# the Free Software Foundation, either version 3 of the License, or #
464+# (at your option) any later version. #
465+# #
466+# This program is distributed in the hope that it will be useful, #
467+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
468+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
469+# GNU General Public License for more details. #
470+# #
471+# You should have received a copy of the GNU General Public License #
472+# along with this program. If not, see <https://www.gnu.org/licenses/>. #
473+##########################################################################
474+from unittest.mock import MagicMock, call, patch
475+
476+from openlp.core.api.zeroconf import ZeroconfWorker, start_zeroconf
477+
478+
479+@patch('openlp.core.api.zeroconf.socket.inet_aton')
480+def test_zeroconf_worker_constructor(mocked_inet_aton):
481+ """Test creating the Zeroconf worker object"""
482+ # GIVEN: A ZeroconfWorker class and a mocked inet_aton
483+ mocked_inet_aton.return_value = 'processed_ip'
484+
485+ # WHEN: An instance of the ZeroconfWorker is created
486+ worker = ZeroconfWorker('127.0.0.1', 8000, 8001)
487+
488+ # THEN: The inet_aton function should have been called and the attrs should be set
489+ mocked_inet_aton.assert_called_once_with('127.0.0.1')
490+ assert worker.address == 'processed_ip'
491+ assert worker.http_port == 8000
492+ assert worker.ws_port == 8001
493+
494+
495+@patch('openlp.core.api.zeroconf.ServiceInfo')
496+@patch('openlp.core.api.zeroconf.Zeroconf')
497+def test_zeroconf_worker_start(MockedZeroconf, MockedServiceInfo):
498+ """Test the start() method of ZeroconfWorker"""
499+ # GIVEN: A few mocks and a ZeroconfWorker instance with a mocked can_run method
500+ mocked_http_info = MagicMock()
501+ mocked_ws_info = MagicMock()
502+ mocked_zc = MagicMock()
503+ MockedServiceInfo.side_effect = [mocked_http_info, mocked_ws_info]
504+ MockedZeroconf.return_value = mocked_zc
505+ worker = ZeroconfWorker('127.0.0.1', 8000, 8001)
506+
507+ # WHEN: The start() method is called
508+ with patch.object(worker, 'can_run') as mocked_can_run:
509+ mocked_can_run.side_effect = [True, False]
510+ worker.start()
511+
512+ # THEN: The correct calls are made
513+ assert MockedServiceInfo.call_args_list == [
514+ call('_http._tcp.local.', 'OpenLP._http._tcp.local.', address=b'\x7f\x00\x00\x01', port=8000, properties={}),
515+ call('_ws._tcp.local.', 'OpenLP._ws._tcp.local.', address=b'\x7f\x00\x00\x01', port=8001, properties={})
516+ ]
517+ assert MockedZeroconf.call_count == 1
518+ assert mocked_zc.register_service.call_args_list == [call(mocked_http_info), call(mocked_ws_info)]
519+ assert mocked_can_run.call_count == 2
520+ assert mocked_zc.unregister_service.call_args_list == [call(mocked_http_info), call(mocked_ws_info)]
521+ assert mocked_zc.close.call_count == 1
522+
523+
524+def test_zeroconf_worker_stop():
525+ """Test that the ZeroconfWorker.stop() method correctly stops the service"""
526+ # GIVEN: A worker object with _can_run set to True
527+ worker = ZeroconfWorker('127.0.0.1', 8000, 8001)
528+ worker._can_run = True
529+
530+ # WHEN: stop() is called
531+ worker.stop()
532+
533+ # THEN: _can_run should be False
534+ assert worker._can_run is False
535+
536+
537+@patch('openlp.core.api.zeroconf.get_network_interfaces')
538+@patch('openlp.core.api.zeroconf.Registry')
539+@patch('openlp.core.api.zeroconf.Settings')
540+@patch('openlp.core.api.zeroconf.ZeroconfWorker')
541+@patch('openlp.core.api.zeroconf.run_thread')
542+def test_start_zeroconf(mocked_run_thread, MockedZeroconfWorker, MockedSettings, MockedRegistry,
543+ mocked_get_network_interfaces):
544+ """Test the start_zeroconf() function"""
545+ # GIVEN: A whole bunch of stuff that's mocked out
546+ mocked_get_network_interfaces.return_value = {
547+ 'eth0': {
548+ 'ip': '192.168.1.191',
549+ 'broadcast': '192.168.1.255',
550+ 'netmask': '255.255.255.0',
551+ 'prefix': 24,
552+ 'localnet': '192.168.1.0'
553+ }
554+ }
555+ MockedRegistry.return_value.get_flag.return_value = False
556+ MockedSettings.return_value.value.side_effect = [8000, 8001]
557+ mocked_worker = MagicMock()
558+ MockedZeroconfWorker.return_value = mocked_worker
559+
560+ # WHEN: start_zeroconf() is called
561+ start_zeroconf()
562+
563+ # THEN: A worker is added to the list of threads
564+ mocked_run_thread.assert_called_once_with(mocked_worker, 'api_zeroconf_eth0')
565
566=== modified file 'tests/openlp_core/common/test_network_interfaces.py'
567--- tests/openlp_core/common/test_network_interfaces.py 2019-04-13 13:00:22 +0000
568+++ tests/openlp_core/common/test_network_interfaces.py 2019-07-03 06:34:48 +0000
569@@ -28,7 +28,7 @@
570 from PyQt5.QtNetwork import QHostAddress, QNetworkAddressEntry, QNetworkInterface
571
572 import openlp.core.common
573-from openlp.core.common import get_local_ip4
574+from openlp.core.common import get_network_interfaces
575 from tests.helpers.testmixin import TestMixin
576
577
578@@ -101,7 +101,7 @@
579 self.destroy_settings()
580
581 @patch.object(openlp.core.common, 'log')
582- def test_ip4_no_interfaces(self, mock_log):
583+ def test_network_interfaces_no_interfaces(self, mock_log):
584 """
585 Test no interfaces available
586 """
587@@ -109,115 +109,101 @@
588 call_debug = [call('Getting local IPv4 interface(es) information')]
589 call_warning = [call('No active IPv4 network interfaces detected')]
590
591- # WHEN: get_local_ip4 is called
592+ # WHEN: get_network_interfaces() is called
593 with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
594 mock_network_interface.allInterfaces.return_value = []
595- ifaces = get_local_ip4()
596+ interfaces = get_network_interfaces()
597
598 # THEN: There should not be any interfaces detected
599 mock_log.debug.assert_has_calls(call_debug)
600 mock_log.warning.assert_has_calls(call_warning)
601- assert not ifaces, 'There should have been no active interfaces listed'
602+ assert not interfaces, 'There should have been no active interfaces listed'
603
604 @patch.object(openlp.core.common, 'log')
605- def test_ip4_lo(self, mock_log):
606+ def test_network_interfaces_lo(self, mock_log):
607 """
608- Test get_local_ip4 returns proper dictionary with 'lo'
609+ Test get_network_interfaces() returns an empty dictionary if "lo" is the only network interface
610 """
611 # GIVEN: Test environment
612- call_debug = [call('Getting local IPv4 interface(es) information'),
613- call('Checking for isValid and flags == IsUP | IsRunning'),
614- call('Checking address(es) protocol'),
615- call('Checking for protocol == IPv4Protocol'),
616- call('Getting interface information'),
617- call('Adding lo to active list')]
618- call_warning = [call('No active IPv4 interfaces found except localhost')]
619+ call_debug = [
620+ call('Getting local IPv4 interface(es) information'),
621+ call("Filtering out interfaces we don't care about: lo")
622+ ]
623
624- # WHEN: get_local_ip4 is called
625+ # WHEN: get_network_interfaces() is called
626 with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
627 mock_network_interface.allInterfaces.return_value = [self.fake_lo]
628- ifaces = get_local_ip4()
629+ interfaces = get_network_interfaces()
630
631- # THEN: There should be a fake 'lo' interface
632+ # THEN: There should be no interfaces
633 mock_log.debug.assert_has_calls(call_debug)
634- mock_log.warning.assert_has_calls(call_warning)
635- assert ifaces == self.fake_lo.fake_data, "There should have been an 'lo' interface listed"
636+ assert interfaces == {}, 'There should be no interfaces listed'
637
638 @patch.object(openlp.core.common, 'log')
639- def test_ip4_localhost(self, mock_log):
640+ def test_network_interfaces_localhost(self, mock_log):
641 """
642- Test get_local_ip4 returns proper dictionary with 'lo' if interface is 'localhost'
643+ Test get_network_interfaces() returns an empty dictionary if "localhost" is the only network interface
644 """
645 # GIVEN: Test environment
646- call_debug = [call('Getting local IPv4 interface(es) information'),
647- call('Checking for isValid and flags == IsUP | IsRunning'),
648- call('Checking address(es) protocol'),
649- call('Checking for protocol == IPv4Protocol'),
650- call('Getting interface information'),
651- call('Adding localhost to active list'),
652- call('Renaming windows localhost to lo')]
653- call_warning = [call('No active IPv4 interfaces found except localhost')]
654+ call_debug = [
655+ call('Getting local IPv4 interface(es) information'),
656+ call("Filtering out interfaces we don't care about: localhost")
657+ ]
658
659- # WHEN: get_local_ip4 is called
660+ # WHEN: get_network_interfaces() is called
661 with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
662 mock_network_interface.allInterfaces.return_value = [self.fake_localhost]
663- ifaces = get_local_ip4()
664+ interfaces = get_network_interfaces()
665
666- # THEN: There should be a fake 'lo' interface
667+ # THEN: There should be no interfaces
668 mock_log.debug.assert_has_calls(call_debug)
669- mock_log.warning.assert_has_calls(call_warning)
670- assert ifaces == self.fake_lo.fake_data, "There should have been an 'lo' interface listed"
671+ assert interfaces == {}, 'There should be no interfaces listed'
672
673 @patch.object(openlp.core.common, 'log')
674- def test_ip4_eth25(self, mock_log):
675+ def test_network_interfaces_eth25(self, mock_log):
676 """
677- Test get_local_ip4 returns proper dictionary with 'eth25'
678+ Test get_network_interfaces() returns proper dictionary with 'eth25'
679 """
680 # GIVEN: Test environment
681- call_debug = [call('Getting local IPv4 interface(es) information'),
682- call('Checking for isValid and flags == IsUP | IsRunning'),
683- call('Checking address(es) protocol'),
684- call('Checking for protocol == IPv4Protocol'),
685- call('Getting interface information'),
686- call('Adding eth25 to active list')]
687- call_warning = []
688+ call_debug = [
689+ call('Getting local IPv4 interface(es) information'),
690+ call('Checking for isValid and flags == IsUP | IsRunning'),
691+ call('Checking address(es) protocol'),
692+ call('Checking for protocol == IPv4Protocol'),
693+ call('Getting interface information'),
694+ call('Adding eth25 to active list')
695+ ]
696
697- # WHEN: get_local_ip4 is called
698+ # WHEN: get_network_interfaces() is called
699 with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
700 mock_network_interface.allInterfaces.return_value = [self.fake_address]
701- ifaces = get_local_ip4()
702+ interfaces = get_network_interfaces()
703
704 # THEN: There should be a fake 'eth25' interface
705 mock_log.debug.assert_has_calls(call_debug)
706- mock_log.warning.assert_has_calls(call_warning)
707- assert ifaces == self.fake_address.fake_data
708+ assert interfaces == self.fake_address.fake_data
709
710 @patch.object(openlp.core.common, 'log')
711- def test_ip4_lo_eth25(self, mock_log):
712+ def test_network_interfaces_lo_eth25(self, mock_log):
713 """
714- Test get_local_ip4 returns proper dictionary with 'eth25'
715+ Test get_network_interfaces() returns proper dictionary with 'eth25'
716 """
717 # GIVEN: Test environment
718- call_debug = [call('Getting local IPv4 interface(es) information'),
719- call('Checking for isValid and flags == IsUP | IsRunning'),
720- call('Checking address(es) protocol'),
721- call('Checking for protocol == IPv4Protocol'),
722- call('Getting interface information'),
723- call('Adding lo to active list'),
724- call('Checking for isValid and flags == IsUP | IsRunning'),
725- call('Checking address(es) protocol'),
726- call('Checking for protocol == IPv4Protocol'),
727- call('Getting interface information'),
728- call('Adding eth25 to active list'),
729- call('Found at least one IPv4 interface, removing localhost')]
730- call_warning = []
731+ call_debug = [
732+ call('Getting local IPv4 interface(es) information'),
733+ call("Filtering out interfaces we don't care about: lo"),
734+ call('Checking for isValid and flags == IsUP | IsRunning'),
735+ call('Checking address(es) protocol'),
736+ call('Checking for protocol == IPv4Protocol'),
737+ call('Getting interface information'),
738+ call('Adding eth25 to active list')
739+ ]
740
741- # WHEN: get_local_ip4 is called
742+ # WHEN: get_network_interfaces() is called
743 with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
744 mock_network_interface.allInterfaces.return_value = [self.fake_lo, self.fake_address]
745- ifaces = get_local_ip4()
746+ interfaces = get_network_interfaces()
747
748 # THEN: There should be a fake 'eth25' interface
749 mock_log.debug.assert_has_calls(call_debug)
750- mock_log.warning.assert_has_calls(call_warning)
751- assert ifaces == self.fake_address.fake_data, "There should have been only 'eth25' interface listed"
752+ assert interfaces == self.fake_address.fake_data, "There should have been only 'eth25' interface listed"
753
754=== modified file 'tests/openlp_core/projectors/test_projector_db.py'
755--- tests/openlp_core/projectors/test_projector_db.py 2019-04-13 13:00:22 +0000
756+++ tests/openlp_core/projectors/test_projector_db.py 2019-07-03 06:34:48 +0000
757@@ -153,8 +153,9 @@
758 patch('openlp.core.ui.mainwindow.ServiceManager'), \
759 patch('openlp.core.ui.mainwindow.ThemeManager'), \
760 patch('openlp.core.ui.mainwindow.ProjectorManager'), \
761- patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
762- patch('openlp.core.ui.mainwindow.server.HttpServer'), \
763+ patch('openlp.core.ui.mainwindow.WebSocketServer'), \
764+ patch('openlp.core.ui.mainwindow.HttpServer'), \
765+ patch('openlp.core.ui.mainwindow.start_zeroconf'), \
766 patch('openlp.core.state.State.list_plugins') as mock_plugins:
767 mock_plugins.return_value = []
768 self.main_window = MainWindow()