Merge lp:~raoul-snyman/openlp/zeroconf into lp:openlp
- zeroconf
- Merge into trunk
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 |
Related bugs: |
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 | # |
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:/
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:/
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() |
Linux tests failed, please see https:/ /ci.openlp. io/job/ MP-02-Linux_ Tests/201/ for more details