Merge lp:~alisonken1/openlp/pjlink2-p into lp:openlp

Proposed by Ken Roberts
Status: Merged
Merged at revision: 2813
Proposed branch: lp:~alisonken1/openlp/pjlink2-p
Merge into: lp:openlp
Diff against target: 852 lines (+595/-40)
10 files modified
openlp/core/api/tab.py (+7/-11)
openlp/core/common/__init__.py (+39/-0)
openlp/core/projectors/manager.py (+2/-5)
openlp/core/projectors/pjlink.py (+143/-18)
tests/functional/openlp_core/api/test_tab.py (+11/-0)
tests/openlp_core/__init__.py (+26/-0)
tests/openlp_core/projectors/__init__.py (+1/-1)
tests/openlp_core/projectors/test_projector_pjlink_udp.py (+360/-0)
tests/openlp_core/projectors/test_projectorsourceform.py (+0/-2)
tests/resources/projector/data.py (+6/-3)
To merge this branch: bzr merge lp:~alisonken1/openlp/pjlink2-p
Reviewer Review Type Date Requested Status
Tomas Groth Approve
Tim Bentley Approve
Raoul Snyman Pending
Review via email: mp+337519@code.launchpad.net

This proposal supersedes a proposal from 2018-02-11.

Commit message

PJLink2 Update P

Description of the change

- manager: Remove unused signal disconnect projectorNetwork.disconnect()
- Change PJLinkUDP.pjlink_udp_commands to dict with link to processing methods
- Add test_projector_pjlink_udp.test_process_ackn_duplicate
- Add test_projector_pjlink_udp.test_process_ackn_multiple
- Add test_projector_pjlink_udp.test_process_ackn_single
- Add test_projector_pjlink_udp.test_process_srch
- Add PJLinkUDP.get_datagram method
- Add PJLinkUDP._trash_udp_buffer method
- Add PJLinkUDP.process_ackn method
- Add PJLinkUDP.process_srch method
- Move projector tests to tests/openlp_core/projectors/
- Update resources.projector.data.TESTx_DATA to add mac_adx
- Added dictionary of IP interfaces/addresses to core.common
- Refactor tab.ApiTab.get_ip_address() to use MY_IP4 dict
- Fix tests for api.tab.ApiTab.get_ip_address()
- Move dictionary of IP interfaces to function

--------------------------------------------------------------------------------
lp:~alisonken1/openlp/pjlink2-p (revision 2811)
https://ci.openlp.io/job/Branch-01-Pull/2446/ [SUCCESS]
https://ci.openlp.io/job/Branch-02a-Linux-Tests/2347/ [SUCCESS]
https://ci.openlp.io/job/Branch-02b-macOS-Tests/141/ [FAILURE]

Passed local tests so not sure what happened now

To post a comment you must log in.
Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

One question and a request to refactor.

review: Needs Fixing
Revision history for this message
Ken Roberts (alisonken1) wrote : Posted in a previous version of this proposal

>> === modified file 'openlp/.version'
>> --- openlp/.version 2016-12-12 22:16:23 +0000
>> +++ openlp/.version 2018-02-10 09:09:49 +0000
>> @@ -1 +1 @@
>> -2.5.0
>> +2.5-bzr2809
>
> why?

huh?
Didn't know that happened. Must have been when I was twiddling with
pip install trying something out.
Reverted

Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

Looks ok and a good refactor.
Not 100% sure about the inline code for the Network lookup.
Let TGC or Superfly comment on that.

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

Just the one request, everything else looks spiffy.

review: Needs Fixing
Revision history for this message
Tim Bentley (trb143) :
review: Approve
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 2017-12-29 09:15:48 +0000
3+++ openlp/core/api/tab.py 2018-02-12 01:05:51 +0000
4@@ -24,6 +24,7 @@
5 """
6 from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets
7
8+from openlp.core.common import get_local_ip4
9 from openlp.core.common.i18n import UiStrings, translate
10 from openlp.core.common.registry import Registry
11 from openlp.core.common.settings import Settings
12@@ -219,17 +220,12 @@
13 else: return ip_address
14 """
15 if ip_address == ZERO_URL:
16- interfaces = QtNetwork.QNetworkInterface.allInterfaces()
17- for interface in interfaces:
18- if not interface.isValid():
19- continue
20- if not (interface.flags() & (QtNetwork.QNetworkInterface.IsUp | QtNetwork.QNetworkInterface.IsRunning)):
21- continue
22- for address in interface.addressEntries():
23- ip = address.ip()
24- if ip.protocol() == QtNetwork.QAbstractSocket.IPv4Protocol and \
25- ip != QtNetwork.QHostAddress.LocalHost:
26- return ip.toString()
27+ # In case we have more than one interface
28+ ifaces = get_local_ip4()
29+ for key in iter(ifaces):
30+ ip_address = ifaces.get(key)['ip']
31+ # We only want the first interface returned
32+ break
33 return ip_address
34
35 def load(self):
36
37=== modified file 'openlp/core/common/__init__.py'
38--- openlp/core/common/__init__.py 2017-12-29 09:15:48 +0000
39+++ openlp/core/common/__init__.py 2018-02-12 01:05:51 +0000
40@@ -36,6 +36,7 @@
41
42 from PyQt5 import QtGui
43 from PyQt5.QtCore import QCryptographicHash as QHash
44+from PyQt5.QtNetwork import QAbstractSocket, QHostAddress, QNetworkInterface
45 from chardet.universaldetector import UniversalDetector
46
47 log = logging.getLogger(__name__ + '.__init__')
48@@ -52,6 +53,44 @@
49 WHITESPACE_REGEX = re.compile(r'[ \t]+')
50
51
52+def get_local_ip4():
53+ """
54+ Creates a dictionary of local IPv4 interfaces on local machine.
55+ If no active interfaces available, returns a dict of localhost IPv4 information
56+
57+ :returns: Dict of interfaces
58+ """
59+ # Get the local IPv4 active address(es) that are NOT localhost (lo or '127.0.0.1')
60+ log.debug('Getting local IPv4 interface(es) information')
61+ MY_IP4 = {}
62+ for iface in QNetworkInterface.allInterfaces():
63+ if not iface.isValid() or not (iface.flags() & (QNetworkInterface.IsUp | QNetworkInterface.IsRunning)):
64+ continue
65+ for address in iface.addressEntries():
66+ ip = address.ip()
67+ # NOTE: Next line will skip if interface is localhost - keep for now until we decide about it later
68+ # if (ip.protocol() == QAbstractSocket.IPv4Protocol) and (ip != QHostAddress.LocalHost):
69+ if (ip.protocol() == QAbstractSocket.IPv4Protocol):
70+ MY_IP4[iface.name()] = {'ip': ip.toString(),
71+ 'broadcast': address.broadcast().toString(),
72+ 'netmask': address.netmask().toString(),
73+ 'prefix': address.prefixLength(),
74+ 'localnet': QHostAddress(address.netmask().toIPv4Address() &
75+ ip.toIPv4Address()).toString()
76+ }
77+ log.debug('Adding {iface} to active list'.format(iface=iface.name()))
78+ if len(MY_IP4) == 1:
79+ if 'lo' in MY_IP4:
80+ # No active interfaces - so leave localhost in there
81+ log.warning('No active IPv4 interfaces found except localhost')
82+ else:
83+ # Since we have a valid IP4 interface, remove localhost
84+ log.debug('Found at least one IPv4 interface, removing localhost')
85+ MY_IP4.pop('lo')
86+
87+ return MY_IP4
88+
89+
90 def trace_error_handler(logger):
91 """
92 Log the calling path of an exception
93
94=== modified file 'openlp/core/projectors/manager.py'
95--- openlp/core/projectors/manager.py 2018-01-13 05:41:42 +0000
96+++ openlp/core/projectors/manager.py 2018-02-12 01:05:51 +0000
97@@ -308,7 +308,6 @@
98 self.settings_section = 'projector'
99 self.projectordb = projectordb
100 self.projector_list = []
101- self.pjlink_udp = PJLinkUDP(self.projector_list)
102 self.source_select_form = None
103
104 def bootstrap_initialise(self):
105@@ -323,6 +322,7 @@
106 else:
107 log.debug('Using existing ProjectorDB() instance')
108 self.get_settings()
109+ self.pjlink_udp = PJLinkUDP(self.projector_list)
110
111 def bootstrap_post_set_up(self):
112 """
113@@ -344,6 +344,7 @@
114 """
115 Retrieve the saved settings
116 """
117+ log.debug('Updating ProjectorManager settings')
118 settings = Settings()
119 settings.beginGroup(self.settings_section)
120 self.autostart = settings.value('connect on start')
121@@ -502,10 +503,6 @@
122 if ans == msg.Cancel:
123 return
124 try:
125- projector.link.projectorNetwork.disconnect(self.update_status)
126- except (AttributeError, TypeError):
127- pass
128- try:
129 projector.link.changeStatus.disconnect(self.update_status)
130 except (AttributeError, TypeError):
131 pass
132
133=== modified file 'openlp/core/projectors/pjlink.py'
134--- openlp/core/projectors/pjlink.py 2018-01-13 05:41:42 +0000
135+++ openlp/core/projectors/pjlink.py 2018-02-12 01:05:51 +0000
136@@ -64,7 +64,7 @@
137 log = logging.getLogger(__name__)
138 log.debug('pjlink loaded')
139
140-__all__ = ['PJLink']
141+__all__ = ['PJLink', 'PJLinkUDP']
142
143 # Shortcuts
144 SocketError = QtNetwork.QAbstractSocket.SocketError
145@@ -79,22 +79,145 @@
146 """
147 Socket service for PJLink UDP socket.
148 """
149- # New commands available in PJLink Class 2
150- pjlink_udp_commands = [
151- 'ACKN', # Class 2 (cmd is SRCH)
152- 'ERST', # Class 1/2
153- 'INPT', # Class 1/2
154- 'LKUP', # Class 2 (reply only - no cmd)
155- 'POWR', # Class 1/2
156- 'SRCH' # Class 2 (reply is ACKN)
157- ]
158-
159 def __init__(self, projector_list, port=PJLINK_PORT):
160 """
161- Initialize socket
162+ Socket services for PJLink UDP packets.
163+
164+ Since all UDP packets from any projector will come into the same
165+ port, process UDP packets here then route to the appropriate
166+ projector instance as needed.
167 """
168+ # Keep track of currently defined projectors so we can route
169+ # inbound packets to the correct instance
170+ super().__init__()
171 self.projector_list = projector_list
172 self.port = port
173+ # Local defines
174+ self.ackn_list = {} # Replies from online projetors
175+ self.search_active = False
176+ self.search_time = 30000 # 30 seconds for allowed time
177+ self.search_timer = QtCore.QTimer()
178+ # New commands available in PJLink Class 2
179+ # ACKN/SRCH is processed here since it's used to find available projectors
180+ # Other commands are processed by the individual projector instances
181+ self.pjlink_udp_functions = {
182+ 'ACKN': self.process_ackn, # Class 2, command is 'SRCH'
183+ 'ERST': None, # Class 1/2
184+ 'INPT': None, # Class 1/2
185+ 'LKUP': None, # Class 2 (reply only - no cmd)
186+ 'POWR': None, # Class 1/2
187+ 'SRCH': self.process_srch # Class 2 (reply is ACKN)
188+ }
189+
190+ self.readyRead.connect(self.get_datagram)
191+ log.debug('(UDP) PJLinkUDP() Initialized')
192+
193+ @QtCore.pyqtSlot()
194+ def get_datagram(self):
195+ """
196+ Retrieve packet and basic checks
197+ """
198+ log.debug('(UDP) get_datagram() - Receiving data')
199+ read = self.pendingDatagramSize()
200+ if read < 0:
201+ log.warn('(UDP) No data (-1)')
202+ return
203+ if read < 1:
204+ log.warn('(UDP) get_datagram() called when pending data size is 0')
205+ return
206+ data, peer_address, peer_port = self.readDatagram(self.pendingDatagramSize())
207+ log.debug('(UDP) {size} bytes received from {adx} on port {port}'.format(size=len(data),
208+ adx=peer_address,
209+ port=peer_port))
210+ log.debug('(UDP) packet "{data}"'.format(data=data))
211+ if len(data) < 0:
212+ log.warn('(UDP) No data (-1)')
213+ return
214+ elif len(data) < 8:
215+ # Minimum packet is '%2CCCC='
216+ log.warn('(UDP) Invalid packet - not enough data')
217+ return
218+ elif data is None:
219+ log.warn('(UDP) No data (None)')
220+ return
221+ elif len(data) > PJLINK_MAX_PACKET:
222+ log.warn('(UDP) Invalid packet - length too long')
223+ return
224+ elif not data.startswith(PJLINK_PREFIX):
225+ log.warn('(UDP) Invalid packet - does not start with PJLINK_PREFIX')
226+ return
227+ elif data[1] != '2':
228+ log.warn('(UDP) Invalid packet - missing/invalid PJLink class version')
229+ return
230+ elif data[6] != '=':
231+ log.warn('(UDP) Invalid packet - separator missing')
232+ return
233+ # First two characters are header information we don't need at this time
234+ cmd, data = data[2:].split('=')
235+ if cmd not in self.pjlink_udp_functions:
236+ log.warn('(UDP) Invalid packet - not a valid PJLink UDP reply')
237+ return
238+ if self.pjlink_udp_functions[cmd] is not None:
239+ log.debug('(UDP) Processing {cmd} with "{data}"'.format(cmd=cmd, data=data))
240+ return self.pjlink_udp_functions[cmd](data=data, host=peer_address, port=peer_port)
241+ else:
242+ log.debug('(UDP) Checking projector list for ip {host} to process'.format(host=peer_address))
243+ for projector in self.projector_list:
244+ if peer_address == projector.ip:
245+ if cmd not in projector.pjlink_functions:
246+ log.error('(UDP) Could not find method to process '
247+ '"{cmd}" in {host}'.format(cmd=cmd, host=projector.ip))
248+ return
249+ log.debug('(UDP) Calling "{cmd}" in {host}'.format(cmd=cmd, host=projector.ip))
250+ return projector.pjlink_functions[cmd](data=data)
251+ log.warn('(UDP) Could not find projector with ip {ip} to process packet'.format(ip=peer_address))
252+ return
253+
254+ def process_ackn(self, data, host, port):
255+ """
256+ Process the ACKN command.
257+
258+ :param data: Data in packet
259+ :param host: IP address of sending host
260+ :param port: Port received on
261+ """
262+ log.debug('(UDP) Processing ACKN packet')
263+ if host not in self.ackn_list:
264+ log.debug('(UDP) Adding {host} to ACKN list'.format(host=host))
265+ self.ackn_list[host] = {'data': data,
266+ 'port': port}
267+ else:
268+ log.warn('(UDP) Host {host} already replied - ignoring'.format(host=host))
269+
270+ def process_srch(self, data, host, port):
271+ """
272+ Process the SRCH command.
273+
274+ SRCH is processed by terminals so we ignore any packet.
275+
276+ :param data: Data in packet
277+ :param host: IP address of sending host
278+ :param port: Port received on
279+ """
280+ log.debug('(UDP) SRCH packet received - ignoring')
281+ return
282+
283+ def search_start(self):
284+ """
285+ Start search for projectors on local network
286+ """
287+ self.search_active = True
288+ self.ackn_list = {}
289+ # TODO: Send SRCH packet here
290+ self.search_timer.singleShot(self.search_time, self.search_stop)
291+
292+ @QtCore.pyqtSlot()
293+ def search_stop(self):
294+ """
295+ Stop search
296+ """
297+ self.search_active = False
298+ self.search_timer.stop()
299
300
301 class PJLinkCommands(object):
302@@ -257,8 +380,9 @@
303 else:
304 clss = data
305 self.pjlink_class = clss
306- log.debug('({ip}) Setting pjlink_class for this projector to "{data}"'.format(ip=self.entry.name,
307- data=self.pjlink_class))
308+ log.debug('({ip}) Setting pjlink_class for this projector '
309+ 'to "{data}"'.format(ip=self.entry.name,
310+ data=self.pjlink_class))
311 # Since we call this one on first connect, setup polling from here
312 if not self.no_poll:
313 log.debug('({ip}) process_pjlink(): Starting timer'.format(ip=self.entry.name))
314@@ -276,9 +400,10 @@
315 """
316 if len(data) != PJLINK_ERST_DATA['DATA_LENGTH']:
317 count = PJLINK_ERST_DATA['DATA_LENGTH']
318- log.warning('({ip}) Invalid error status response "{data}": length != {count}'.format(ip=self.entry.name,
319- data=data,
320- count=count))
321+ log.warning('({ip}) Invalid error status response "{data}": '
322+ 'length != {count}'.format(ip=self.entry.name,
323+ data=data,
324+ count=count))
325 return
326 try:
327 datacheck = int(data)
328@@ -557,7 +682,7 @@
329
330 class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
331 """
332- Socket service for PJLink TCP socket.
333+ Socket services for PJLink TCP packets.
334 """
335 # Signals sent by this module
336 changeStatus = QtCore.pyqtSignal(str, int, str)
337
338=== modified file 'tests/functional/openlp_core/api/test_tab.py'
339--- tests/functional/openlp_core/api/test_tab.py 2017-12-29 09:15:48 +0000
340+++ tests/functional/openlp_core/api/test_tab.py 2018-02-12 01:05:51 +0000
341@@ -29,6 +29,7 @@
342 from PyQt5 import QtWidgets
343
344 from openlp.core.api.tab import ApiTab
345+from openlp.core.common import get_local_ip4
346 from openlp.core.common.registry import Registry
347 from openlp.core.common.settings import Settings
348 from tests.helpers.testmixin import TestMixin
349@@ -63,6 +64,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
355 def tearDown(self):
356 """
357@@ -76,11 +78,18 @@
358 """
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+
366 # WHEN: the default ip address is given
367 ip_address = self.form.get_ip_address(ZERO_URL)
368+
369 # THEN: the default ip address will be returned
370 assert re.match('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', ip_address), \
371 'The return value should be a valid ip address'
372+ assert ip_address in ip4_list, 'The return address should be in the list of local IP addresses'
373
374 def test_get_ip_address_with_ip(self):
375 """
376@@ -88,8 +97,10 @@
377 """
378 # GIVEN: An ip address
379 given_ip = '192.168.1.1'
380+
381 # WHEN: the default ip address is given
382 ip_address = self.form.get_ip_address(given_ip)
383+
384 # THEN: the default ip address will be returned
385 assert ip_address == given_ip, 'The return value should be %s' % given_ip
386
387
388=== added directory 'tests/openlp_core'
389=== added file 'tests/openlp_core/__init__.py'
390--- tests/openlp_core/__init__.py 1970-01-01 00:00:00 +0000
391+++ tests/openlp_core/__init__.py 2018-02-12 01:05:51 +0000
392@@ -0,0 +1,26 @@
393+# -*- coding: utf-8 -*-
394+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
395+
396+###############################################################################
397+# OpenLP - Open Source Lyrics Projection #
398+# --------------------------------------------------------------------------- #
399+# Copyright (c) 2008-2018 OpenLP Developers #
400+# --------------------------------------------------------------------------- #
401+# This program is free software; you can redistribute it and/or modify it #
402+# under the terms of the GNU General Public License as published by the Free #
403+# Software Foundation; version 2 of the License. #
404+# #
405+# This program is distributed in the hope that it will be useful, but WITHOUT #
406+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
407+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
408+# more details. #
409+# #
410+# You should have received a copy of the GNU General Public License along #
411+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
412+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
413+###############################################################################
414+"""
415+:mod: `tests.openlp_core` module
416+
417+Tests modules/files for module openlp.core
418+"""
419
420=== renamed directory 'tests/functional/openlp_core/projectors' => 'tests/openlp_core/projectors'
421=== modified file 'tests/openlp_core/projectors/__init__.py'
422--- tests/functional/openlp_core/projectors/__init__.py 2017-11-10 11:59:38 +0000
423+++ tests/openlp_core/projectors/__init__.py 2018-02-12 01:05:51 +0000
424@@ -20,5 +20,5 @@
425 # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
426 ###############################################################################
427 """
428-Module-level functions for the functional test suite
429+Module-level functions for the projector test suite
430 """
431
432=== added file 'tests/openlp_core/projectors/test_projector_pjlink_udp.py'
433--- tests/openlp_core/projectors/test_projector_pjlink_udp.py 1970-01-01 00:00:00 +0000
434+++ tests/openlp_core/projectors/test_projector_pjlink_udp.py 2018-02-12 01:05:51 +0000
435@@ -0,0 +1,360 @@
436+
437+# -*- coding: utf-8 -*-
438+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
439+
440+###############################################################################
441+# OpenLP - Open Source Lyrics Projection #
442+# --------------------------------------------------------------------------- #
443+# Copyright (c) 2008-2018 OpenLP Developers #
444+# --------------------------------------------------------------------------- #
445+# This program is free software; you can redistribute it and/or modify it #
446+# under the terms of the GNU General Public License as published by the Free #
447+# Software Foundation; version 2 of the License. #
448+# #
449+# This program is distributed in the hope that it will be useful, but WITHOUT #
450+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
451+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
452+# more details. #
453+# #
454+# You should have received a copy of the GNU General Public License along #
455+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
456+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
457+###############################################################################
458+"""
459+Package to test the PJLink UDP functions
460+"""
461+
462+from unittest import TestCase
463+from unittest.mock import call, patch
464+
465+import openlp.core.projectors.pjlink
466+from openlp.core.projectors.constants import PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_PREFIX
467+
468+from openlp.core.projectors.db import Projector
469+from openlp.core.projectors.pjlink import PJLinkUDP
470+from tests.resources.projector.data import TEST1_DATA, TEST2_DATA
471+
472+
473+class TestPJLinkBase(TestCase):
474+ """
475+ Tests for the PJLinkUDP class
476+ """
477+ def setUp(self):
478+ """
479+ Setup generic test conditions
480+ """
481+ self.test_list = [Projector(**TEST1_DATA), Projector(**TEST2_DATA)]
482+
483+ def tearDown(self):
484+ """
485+ Close generic test condidtions
486+ """
487+ self.test_list = None
488+
489+ @patch.object(openlp.core.projectors.pjlink, 'log')
490+ def test_get_datagram_data_invalid_class(self, mock_log):
491+ """
492+ Test get_datagram with invalid class number
493+ """
494+ # GIVEN: Test setup
495+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
496+ log_warn_calls = [call('(UDP) Invalid packet - missing/invalid PJLink class version')]
497+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
498+ call('(UDP) get_datagram() - Receiving data'),
499+ call('(UDP) 24 bytes received from 111.111.111.111 on port 4352'),
500+ call('(UDP) packet "%1ACKN=11:11:11:11:11:11"')]
501+ with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
502+ patch.object(pjlink_udp, 'readDatagram') as mock_read:
503+ mock_datagram.return_value = 24
504+ mock_read.return_value = ('{prefix}1ACKN={mac}'.format(prefix=PJLINK_PREFIX, mac=TEST1_DATA['mac_adx']),
505+ TEST1_DATA['ip'], PJLINK_PORT)
506+
507+ # WHEN: get_datagram called with 0 bytes ready
508+ pjlink_udp.get_datagram()
509+
510+ # THEN: Log entries should be made and method returns
511+ mock_log.debug.assert_has_calls(log_debug_calls)
512+ mock_log.warn.assert_has_calls(log_warn_calls)
513+
514+ @patch.object(openlp.core.projectors.pjlink, 'log')
515+ def test_get_datagram_data_invalid_command(self, mock_log):
516+ """
517+ Test get_datagram with invalid PJLink UDP command
518+ """
519+ # GIVEN: Test setup
520+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
521+ log_warn_calls = [call('(UDP) Invalid packet - not a valid PJLink UDP reply')]
522+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
523+ call('(UDP) get_datagram() - Receiving data'),
524+ call('(UDP) 24 bytes received from 111.111.111.111 on port 4352'),
525+ call('(UDP) packet "%2DUMB=11:11:11:11:11:11"')]
526+ with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
527+ patch.object(pjlink_udp, 'readDatagram') as mock_read:
528+ mock_datagram.return_value = 24
529+ mock_read.return_value = ('{prefix}2DUMB={mac}'.format(prefix=PJLINK_PREFIX, mac=TEST1_DATA['mac_adx']),
530+ TEST1_DATA['ip'], PJLINK_PORT)
531+
532+ # WHEN: get_datagram called with 0 bytes ready
533+ pjlink_udp.get_datagram()
534+
535+ # THEN: Log entries should be made and method returns
536+ mock_log.debug.assert_has_calls(log_debug_calls)
537+ mock_log.warn.assert_has_calls(log_warn_calls)
538+
539+ @patch.object(openlp.core.projectors.pjlink, 'log')
540+ def test_get_datagram_data_invalid_prefix(self, mock_log):
541+ """
542+ Test get_datagram when prefix != PJLINK_PREFIX
543+ """
544+ # GIVEN: Test setup
545+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
546+ log_warn_calls = [call('(UDP) Invalid packet - does not start with PJLINK_PREFIX')]
547+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
548+ call('(UDP) get_datagram() - Receiving data'),
549+ call('(UDP) 24 bytes received from 111.111.111.111 on port 4352'),
550+ call('(UDP) packet "$2ACKN=11:11:11:11:11:11"')]
551+ with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
552+ patch.object(pjlink_udp, 'readDatagram') as mock_read:
553+ mock_datagram.return_value = 24
554+ mock_read.return_value = ('{prefix}2ACKN={mac}'.format(prefix='$', mac=TEST1_DATA['mac_adx']),
555+ TEST1_DATA['ip'], PJLINK_PORT)
556+
557+ # WHEN: get_datagram called with 0 bytes ready
558+ pjlink_udp.get_datagram()
559+
560+ # THEN: Log entries should be made and method returns
561+ mock_log.debug.assert_has_calls(log_debug_calls)
562+ mock_log.warn.assert_has_calls(log_warn_calls)
563+
564+ @patch.object(openlp.core.projectors.pjlink, 'log')
565+ def test_get_datagram_data_invalid_separator(self, mock_log):
566+ """
567+ Test get_datagram when separator not equal to =
568+ """
569+ # GIVEN: Test setup
570+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
571+ log_warn_calls = [call('(UDP) Invalid packet - separator missing')]
572+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
573+ call('(UDP) get_datagram() - Receiving data'),
574+ call('(UDP) 24 bytes received from 111.111.111.111 on port 4352'),
575+ call('(UDP) packet "%2ACKN 11:11:11:11:11:11"')]
576+ with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
577+ patch.object(pjlink_udp, 'readDatagram') as mock_read:
578+ mock_datagram.return_value = 24
579+ mock_read.return_value = ('{prefix}2ACKN {mac}'.format(prefix=PJLINK_PREFIX, mac=TEST1_DATA['mac_adx']),
580+ TEST1_DATA['ip'], PJLINK_PORT)
581+
582+ # WHEN: get_datagram called with 0 bytes ready
583+ pjlink_udp.get_datagram()
584+
585+ # THEN: Log entries should be made and method returns
586+ mock_log.debug.assert_has_calls(log_debug_calls)
587+ mock_log.warn.assert_has_calls(log_warn_calls)
588+
589+ @patch.object(openlp.core.projectors.pjlink, 'log')
590+ def test_get_datagram_data_long(self, mock_log):
591+ """
592+ Test get_datagram when datagram > PJLINK_MAX_PACKET
593+ """
594+ # GIVEN: Test setup
595+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
596+ log_warn_calls = [call('(UDP) Invalid packet - length too long')]
597+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
598+ call('(UDP) get_datagram() - Receiving data'),
599+ call('(UDP) 143 bytes received from 111.111.111.111 on port 4352'),
600+ call('(UDP) packet "%2ACKN={long}"'.format(long='X' * PJLINK_MAX_PACKET))]
601+ with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
602+ patch.object(pjlink_udp, 'readDatagram') as mock_read:
603+ mock_datagram.return_value = PJLINK_MAX_PACKET + 7
604+ mock_read.return_value = ('{prefix}2ACKN={long}'.format(prefix=PJLINK_PREFIX,
605+ long='X' * PJLINK_MAX_PACKET),
606+ TEST1_DATA['ip'], PJLINK_PORT)
607+
608+ # WHEN: get_datagram called with 0 bytes ready
609+ pjlink_udp.get_datagram()
610+
611+ # THEN: Log entries should be made and method returns
612+ mock_log.debug.assert_has_calls(log_debug_calls)
613+ mock_log.warn.assert_has_calls(log_warn_calls)
614+
615+ @patch.object(openlp.core.projectors.pjlink, 'log')
616+ def test_get_datagram_data_negative_zero_length(self, mock_log):
617+ """
618+ Test get_datagram when pendingDatagramSize = 0
619+ """
620+ # GIVEN: Test setup
621+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
622+ log_warn_calls = [call('(UDP) No data (-1)')]
623+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
624+ call('(UDP) get_datagram() - Receiving data')]
625+ with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
626+ patch.object(pjlink_udp, 'readDatagram') as mock_read:
627+ mock_datagram.return_value = -1
628+ mock_read.return_value = ('', TEST1_DATA['ip'], PJLINK_PORT)
629+
630+ # WHEN: get_datagram called with 0 bytes ready
631+ pjlink_udp.get_datagram()
632+
633+ # THEN: Log entries should be made and method returns
634+ mock_log.warn.assert_has_calls(log_warn_calls)
635+ mock_log.debug.assert_has_calls(log_debug_calls)
636+
637+ @patch.object(openlp.core.projectors.pjlink, 'log')
638+ def test_get_datagram_data_no_data(self, mock_log):
639+ """
640+ Test get_datagram when data length = 0
641+ """
642+ # GIVEN: Test setup
643+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
644+ log_warn_calls = [call('(UDP) Invalid packet - not enough data')]
645+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
646+ call('(UDP) get_datagram() - Receiving data')]
647+ with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
648+ patch.object(pjlink_udp, 'readDatagram') as mock_read:
649+ mock_datagram.return_value = 1
650+ mock_read.return_value = ('', TEST1_DATA['ip'], PJLINK_PORT)
651+
652+ # WHEN: get_datagram called with 0 bytes ready
653+ pjlink_udp.get_datagram()
654+
655+ # THEN: Log entries should be made and method returns
656+ mock_log.warn.assert_has_calls(log_warn_calls)
657+ mock_log.debug.assert_has_calls(log_debug_calls)
658+
659+ @patch.object(openlp.core.projectors.pjlink, 'log')
660+ def test_get_datagram_data_short(self, mock_log):
661+ """
662+ Test get_datagram when data length < 8
663+ """
664+ # GIVEN: Test setup
665+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
666+ log_warn_calls = [call('(UDP) Invalid packet - not enough data')]
667+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
668+ call('(UDP) get_datagram() - Receiving data')]
669+ with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
670+ patch.object(pjlink_udp, 'readDatagram') as mock_read:
671+ mock_datagram.return_value = 6
672+ mock_read.return_value = ('{prefix}2ACKN'.format(prefix=PJLINK_PREFIX), TEST1_DATA['ip'], PJLINK_PORT)
673+
674+ # WHEN: get_datagram called with 0 bytes ready
675+ pjlink_udp.get_datagram()
676+
677+ # THEN: Log entries should be made and method returns
678+ mock_log.warn.assert_has_calls(log_warn_calls)
679+ mock_log.debug.assert_has_calls(log_debug_calls)
680+
681+ @patch.object(openlp.core.projectors.pjlink, 'log')
682+ def test_get_datagram_pending_zero_length(self, mock_log):
683+ """
684+ Test get_datagram when pendingDatagramSize = 0
685+ """
686+ # GIVEN: Test setup
687+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
688+ log_warn_calls = [call('(UDP) get_datagram() called when pending data size is 0')]
689+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
690+ call('(UDP) get_datagram() - Receiving data')]
691+ with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram:
692+ mock_datagram.return_value = 0
693+
694+ # WHEN: get_datagram called with 0 bytes ready
695+ pjlink_udp.get_datagram()
696+
697+ # THEN: Log entries should be made and method returns
698+ mock_log.warn.assert_has_calls(log_warn_calls)
699+ mock_log.debug.assert_has_calls(log_debug_calls)
700+
701+ @patch.object(openlp.core.projectors.pjlink, 'log')
702+ def test_process_ackn_duplicate(self, mock_log):
703+ """
704+ Test process_ackn method with multiple calls with same data
705+ """
706+ # GIVEN: Test setup
707+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
708+ check_list = {TEST1_DATA['ip']: {'data': TEST1_DATA['mac_adx'], 'port': PJLINK_PORT}}
709+ log_warn_calls = [call('(UDP) Host {host} already replied - ignoring'.format(host=TEST1_DATA['ip']))]
710+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
711+ call('(UDP) Processing ACKN packet'),
712+ call('(UDP) Adding {host} to ACKN list'.format(host=TEST1_DATA['ip'])),
713+ call('(UDP) Processing ACKN packet')]
714+
715+ # WHEN: process_ackn called twice with same data
716+ pjlink_udp.process_ackn(data=TEST1_DATA['mac_adx'], host=TEST1_DATA['ip'], port=PJLINK_PORT)
717+ pjlink_udp.process_ackn(data=TEST1_DATA['mac_adx'], host=TEST1_DATA['ip'], port=PJLINK_PORT)
718+
719+ # THEN: pjlink_udp.ack_list should equal test_list
720+ # NOTE: This assert only returns AssertionError - does not list differences. Maybe add a compare function?
721+ if pjlink_udp.ackn_list != check_list:
722+ # Check this way so we can print differences to stdout
723+ print('\nackn_list: ', pjlink_udp.ackn_list)
724+ print('test_list: ', check_list)
725+ assert pjlink_udp.ackn_list == check_list
726+ mock_log.debug.assert_has_calls(log_debug_calls)
727+ mock_log.warn.assert_has_calls(log_warn_calls)
728+
729+ @patch.object(openlp.core.projectors.pjlink, 'log')
730+ def test_process_ackn_multiple(self, mock_log):
731+ """
732+ Test process_ackn method with multiple calls
733+ """
734+ # GIVEN: Test setup
735+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
736+ check_list = {TEST1_DATA['ip']: {'data': TEST1_DATA['mac_adx'], 'port': PJLINK_PORT},
737+ TEST2_DATA['ip']: {'data': TEST2_DATA['mac_adx'], 'port': PJLINK_PORT}}
738+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
739+ call('(UDP) Processing ACKN packet'),
740+ call('(UDP) Adding {host} to ACKN list'.format(host=TEST1_DATA['ip'])),
741+ call('(UDP) Processing ACKN packet'),
742+ call('(UDP) Adding {host} to ACKN list'.format(host=TEST2_DATA['ip']))]
743+
744+ # WHEN: process_ackn called twice with different data
745+ pjlink_udp.process_ackn(data=TEST1_DATA['mac_adx'], host=TEST1_DATA['ip'], port=PJLINK_PORT)
746+ pjlink_udp.process_ackn(data=TEST2_DATA['mac_adx'], host=TEST2_DATA['ip'], port=PJLINK_PORT)
747+
748+ # THEN: pjlink_udp.ack_list should equal test_list
749+ # NOTE: This assert only returns AssertionError - does not list differences. Maybe add a compare function?
750+ if pjlink_udp.ackn_list != check_list:
751+ # Check this way so we can print differences to stdout
752+ print('\nackn_list: ', pjlink_udp.ackn_list)
753+ print('test_list: ', check_list)
754+ assert pjlink_udp.ackn_list == check_list
755+ mock_log.debug.assert_has_calls(log_debug_calls)
756+
757+ @patch.object(openlp.core.projectors.pjlink, 'log')
758+ def test_process_ackn_single(self, mock_log):
759+ """
760+ Test process_ackn method with single call
761+ """
762+ # GIVEN: Test setup
763+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
764+ check_list = {TEST1_DATA['ip']: {'data': TEST1_DATA['mac_adx'], 'port': PJLINK_PORT}}
765+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
766+ call('(UDP) Processing ACKN packet'),
767+ call('(UDP) Adding {host} to ACKN list'.format(host=TEST1_DATA['ip']))]
768+
769+ # WHEN: process_ackn called twice with different data
770+ pjlink_udp.process_ackn(data=TEST1_DATA['mac_adx'], host=TEST1_DATA['ip'], port=PJLINK_PORT)
771+
772+ # THEN: pjlink_udp.ack_list should equal test_list
773+ # NOTE: This assert only returns AssertionError - does not list differences. Maybe add a compare function?
774+ if pjlink_udp.ackn_list != check_list:
775+ # Check this way so we can print differences to stdout
776+ print('\nackn_list: ', pjlink_udp.ackn_list)
777+ print('test_list: ', check_list)
778+ assert pjlink_udp.ackn_list == check_list
779+ mock_log.debug.assert_has_calls(log_debug_calls)
780+
781+ @patch.object(openlp.core.projectors.pjlink, 'log')
782+ def test_process_srch(self, mock_log):
783+ """
784+ Test process_srch method
785+ """
786+ # GIVEN: Test setup
787+ pjlink_udp = PJLinkUDP(projector_list=self.test_list)
788+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
789+ call('(UDP) SRCH packet received - ignoring')]
790+
791+ # WHEN: process_srch called
792+ pjlink_udp.process_srch(data=None, host=None, port=None)
793+
794+ # THEN: debug log entry should be entered
795+ mock_log.debug.assert_has_calls(log_debug_calls)
796
797=== renamed file 'tests/functional/openlp_core/common/test_projector_utilities.py' => 'tests/openlp_core/projectors/test_projector_utilities.py'
798=== renamed file 'tests/interfaces/openlp_core/ui/test_projectoreditform.py' => 'tests/openlp_core/projectors/test_projectoreditform.py'
799=== renamed file 'tests/interfaces/openlp_core/ui/test_projectormanager.py' => 'tests/openlp_core/projectors/test_projectormanager.py'
800=== renamed file 'tests/interfaces/openlp_core/ui/test_projectorsourceform.py' => 'tests/openlp_core/projectors/test_projectorsourceform.py'
801--- tests/interfaces/openlp_core/ui/test_projectorsourceform.py 2017-12-29 09:15:48 +0000
802+++ tests/openlp_core/projectors/test_projectorsourceform.py 2018-02-12 01:05:51 +0000
803@@ -125,7 +125,6 @@
804 select_form = SourceSelectSingle(parent=None, projectordb=self.projectordb)
805 select_form.edit = True
806 select_form.exec(projector=self.projector)
807- projector = select_form.projector
808
809 # THEN: Verify all 4 buttons are available
810 assert len(select_form.button_box.buttons()) == 4, \
811@@ -144,7 +143,6 @@
812 select_form = SourceSelectSingle(parent=None, projectordb=self.projectordb)
813 select_form.edit = False
814 select_form.exec(projector=self.projector)
815- projector = select_form.projector
816
817 # THEN: Verify only 2 buttons are available
818 assert len(select_form.button_box.buttons()) == 2, \
819
820=== modified file 'tests/resources/projector/data.py'
821--- tests/resources/projector/data.py 2017-12-29 09:15:48 +0000
822+++ tests/resources/projector/data.py 2018-02-12 01:05:51 +0000
823@@ -45,7 +45,8 @@
824 serial_no='Serial Number 1',
825 sw_version='Version 1',
826 model_filter='Filter type 1',
827- model_lamp='Lamp type 1')
828+ model_lamp='Lamp type 1',
829+ mac_adx='11:11:11:11:11:11')
830
831 TEST2_DATA = dict(ip='222.222.222.222',
832 port='2222',
833@@ -56,7 +57,8 @@
834 serial_no='Serial Number 2',
835 sw_version='Version 2',
836 model_filter='Filter type 2',
837- model_lamp='Lamp type 2')
838+ model_lamp='Lamp type 2',
839+ mac_adx='22:22:22:22:22:22')
840
841 TEST3_DATA = dict(ip='333.333.333.333',
842 port='3333',
843@@ -67,7 +69,8 @@
844 serial_no='Serial Number 3',
845 sw_version='Version 3',
846 model_filter='Filter type 3',
847- model_lamp='Lamp type 3')
848+ model_lamp='Lamp type 3',
849+ mac_adx='33:33:33:33:33:33')
850
851 TEST_VIDEO_CODES = {
852 '11': 'RGB 1',