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

Proposed by Ken Roberts
Status: Merged
Merged at revision: 2795
Proposed branch: lp:~alisonken1/openlp/pjlink2-m
Merge into: lp:openlp
Diff against target: 1323 lines (+502/-243)
9 files modified
openlp/core/projectors/__init__.py (+0/-2)
openlp/core/projectors/constants.py (+18/-0)
openlp/core/projectors/db.py (+1/-1)
openlp/core/projectors/pjlink.py (+195/-117)
tests/functional/openlp_core/projectors/test_projector_bugfixes_01.py (+8/-35)
tests/functional/openlp_core/projectors/test_projector_pjlink_base.py (+34/-48)
tests/functional/openlp_core/projectors/test_projector_pjlink_cmd_routing.py (+35/-27)
tests/functional/openlp_core/projectors/test_projector_pjlink_commands_01.py (+13/-13)
tests/functional/openlp_core/projectors/test_projector_pjlink_commands_02.py (+198/-0)
To merge this branch: bzr merge lp:~alisonken1/openlp/pjlink2-m
Reviewer Review Type Date Requested Status
Tim Bentley Approve
Phill Approve
Review via email: mp+335001@code.launchpad.net

This proposal supersedes a proposal from 2017-12-05.

Commit message

PJLink2 update M

Description of the change

- Added pjlink.process_pjlink
- Split pjlink.check_login() to use process_pjlink()
- Added QAbstractSocket connect enum to constants
- Minor code cleanups for connection and command processing
- Updated packet queueing
- Fix get_object_filtered()
- Fix tests in test_projector_pjlink_base
- Fix tests in test_projector_pjlink_cmd_routing
- Added tests for process_pjlink method
- Updated test_projector_bugfixes_01
- Some OLP style cleanups

--------------------------------------------------------------------------------
lp:~alisonken1/openlp/pjlink2-m (revision 2796)
https://ci.openlp.io/job/Branch-01-Pull/2339/ [SUCCESS]
https://ci.openlp.io/job/Branch-02-Functional-Tests/2240/ [SUCCESS]
https://ci.openlp.io/job/Branch-03-Interface-Tests/2110/ [SUCCESS]
https://ci.openlp.io/job/Branch-04a-Code_Analysis/1436/ [SUCCESS]
https://ci.openlp.io/job/Branch-04b-Test_Coverage/1255/ [SUCCESS]
https://ci.openlp.io/job/Branch-04c-Code_Analysis2/385/ [SUCCESS]
https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/214/ [FAILURE]

To post a comment you must log in.
Revision history for this message
Phill (phill-ridout) wrote : Posted in a previous version of this proposal

Just some style fixes please.

There's a couple inline comments.

Also you've got a right mismatch between single and double quoted strings. Can you change these so that strings use single quotes. I'm not sure there is a standard for quotes within the string, but double quotes are tidier as they do not need escaping. I'm sure I've seen this somewhere in the wiki, but I cannot find it on the coding standards page.

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

See inline

review: Needs Fixing
Revision history for this message
Phill (phill-ridout) wrote :

Great. Thanks for those changes :-)

review: Approve
Revision history for this message
Tim Bentley (trb143) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/projectors/__init__.py'
2--- openlp/core/projectors/__init__.py 2017-11-16 23:53:53 +0000
3+++ openlp/core/projectors/__init__.py 2017-12-09 11:19:42 +0000
4@@ -25,8 +25,6 @@
5 Initialization for the openlp.core.projectors modules.
6 """
7
8-from openlp.core.projectors.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING
9-
10
11 class DialogSourceStyle(object):
12 """
13
14=== modified file 'openlp/core/projectors/constants.py'
15--- openlp/core/projectors/constants.py 2017-11-10 11:59:38 +0000
16+++ openlp/core/projectors/constants.py 2017-12-09 11:19:42 +0000
17@@ -144,6 +144,24 @@
18 }
19 }
20
21+# QAbstractSocketState enums converted to string
22+S_QSOCKET_STATE = {
23+ 0: 'QSocketState - UnconnectedState',
24+ 1: 'QSocketState - HostLookupState',
25+ 2: 'QSocketState - ConnectingState',
26+ 3: 'QSocketState - ConnectedState',
27+ 4: 'QSocketState - BoundState',
28+ 5: 'QSocketState - ListeningState (internal use only)',
29+ 6: 'QSocketState - ClosingState',
30+ 'UnconnectedState': 0,
31+ 'HostLookupState': 1,
32+ 'ConnectingState': 2,
33+ 'ConnectedState': 3,
34+ 'BoundState': 4,
35+ 'ListeningState': 5,
36+ 'ClosingState': 6
37+}
38+
39 # Error and status codes
40 S_OK = E_OK = 0 # E_OK included since I sometimes forget
41 # Error codes. Start at 200 so we don't duplicate system error codes.
42
43=== modified file 'openlp/core/projectors/db.py'
44--- openlp/core/projectors/db.py 2017-11-10 11:59:38 +0000
45+++ openlp/core/projectors/db.py 2017-12-09 11:19:42 +0000
46@@ -415,7 +415,7 @@
47 for key in projector.source_available:
48 item = self.get_object_filtered(ProjectorSource,
49 and_(ProjectorSource.code == key,
50- ProjectorSource.projector_id == projector.dbid))
51+ ProjectorSource.projector_id == projector.id))
52 if item is None:
53 source_dict[key] = PJLINK_DEFAULT_CODES[key]
54 else:
55
56=== modified file 'openlp/core/projectors/pjlink.py'
57--- openlp/core/projectors/pjlink.py 2017-11-24 19:08:23 +0000
58+++ openlp/core/projectors/pjlink.py 2017-12-09 11:19:42 +0000
59@@ -58,8 +58,7 @@
60 E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, E_OK, \
61 E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, PJLINK_ERST_DATA, \
62 PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \
63- STATUS_STRING, S_CONNECTED, S_CONNECTING, S_INFO, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \
64- S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS
65+ STATUS_STRING, S_CONNECTED, S_CONNECTING, S_INFO, S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_QSOCKET_STATE, S_STATUS
66
67 log = logging.getLogger(__name__)
68 log.debug('pjlink loaded')
69@@ -111,7 +110,7 @@
70 """
71 log.debug('PJlinkCommands(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs))
72 super().__init__()
73- # Map command to function
74+ # Map PJLink command to method
75 self.pjlink_functions = {
76 'AVMT': self.process_avmt,
77 'CLSS': self.process_clss,
78@@ -123,7 +122,9 @@
79 'INST': self.process_inst,
80 'LAMP': self.process_lamp,
81 'NAME': self.process_name,
82- 'PJLINK': self.check_login,
83+ 'PJLINK': self.process_pjlink,
84+ # TODO: Part of check_login refactor - remove when done
85+ # 'PJLINK': self.check_login,
86 'POWR': self.process_powr,
87 'SNUM': self.process_snum,
88 'SVER': self.process_sver,
89@@ -135,7 +136,8 @@
90 """
91 Initialize instance variables. Also used to reset projector-specific information to default.
92 """
93- log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip, state=self.state()))
94+ log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip,
95+ state=S_QSOCKET_STATE[self.state()]))
96 self.fan = None # ERST
97 self.filter_time = None # FILT
98 self.lamp = None # LAMP
99@@ -165,6 +167,7 @@
100 self.socket_timer.stop()
101 self.send_busy = False
102 self.send_queue = []
103+ self.priority_queue = []
104
105 def process_command(self, cmd, data):
106 """
107@@ -176,18 +179,19 @@
108 log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.ip,
109 cmd=cmd,
110 data=data))
111+ # cmd should already be in uppercase, but data may be in mixed-case.
112+ # Due to some replies should stay as mixed-case, validate using separate uppercase check
113+ _data = data.upper()
114 # Check if we have a future command not available yet
115- _cmd = cmd.upper()
116- _data = data.upper()
117- if _cmd not in PJLINK_VALID_CMD:
118- log.error("({ip}) Ignoring command='{cmd}' (Invalid/Unknown)".format(ip=self.ip, cmd=cmd))
119+ if cmd not in PJLINK_VALID_CMD:
120+ log.error('({ip}) Ignoring command="{cmd}" (Invalid/Unknown)'.format(ip=self.ip, cmd=cmd))
121 return
122 elif _data == 'OK':
123 log.debug('({ip}) Command "{cmd}" returned OK'.format(ip=self.ip, cmd=cmd))
124- # A command returned successfully, no further processing needed
125- return
126- elif _cmd not in self.pjlink_functions:
127- log.warning("({ip}) Unable to process command='{cmd}' (Future option)".format(ip=self.ip, cmd=cmd))
128+ # A command returned successfully, so do a query on command to verify status
129+ return self.send_command(cmd=cmd)
130+ elif cmd not in self.pjlink_functions:
131+ log.warning('({ip}) Unable to process command="{cmd}" (Future option?)'.format(ip=self.ip, cmd=cmd))
132 return
133 elif _data in PJLINK_ERRORS:
134 # Oops - projector error
135@@ -211,12 +215,10 @@
136 elif _data == PJLINK_ERRORS[E_PROJECTOR]:
137 # Projector/display error
138 self.change_status(E_PROJECTOR)
139- self.receive_data_signal()
140 return
141 # Command checks already passed
142 log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
143- self.receive_data_signal()
144- self.pjlink_functions[_cmd](data)
145+ self.pjlink_functions[cmd](data)
146
147 def process_avmt(self, data):
148 """
149@@ -259,19 +261,19 @@
150 # : Received: '%1CLSS=Class 1' (Optoma)
151 # : Received: '%1CLSS=Version1' (BenQ)
152 if len(data) > 1:
153- log.warning("({ip}) Non-standard CLSS reply: '{data}'".format(ip=self.ip, data=data))
154+ log.warning('({ip}) Non-standard CLSS reply: "{data}"'.format(ip=self.ip, data=data))
155 # Due to stupid projectors not following standards (Optoma, BenQ comes to mind),
156 # AND the different responses that can be received, the semi-permanent way to
157 # fix the class reply is to just remove all non-digit characters.
158 try:
159 clss = re.findall('\d', data)[0] # Should only be the first match
160 except IndexError:
161- log.error("({ip}) No numbers found in class version reply '{data}' - "
162- "defaulting to class '1'".format(ip=self.ip, data=data))
163+ log.error('({ip}) No numbers found in class version reply "{data}" - '
164+ 'defaulting to class "1"'.format(ip=self.ip, data=data))
165 clss = '1'
166 elif not data.isdigit():
167- log.error("({ip}) NAN clss version reply '{data}' - "
168- "defaulting to class '1'".format(ip=self.ip, data=data))
169+ log.error('({ip}) NAN CLSS version reply "{data}" - '
170+ 'defaulting to class "1"'.format(ip=self.ip, data=data))
171 clss = '1'
172 else:
173 clss = data
174@@ -289,7 +291,7 @@
175 """
176 if len(data) != PJLINK_ERST_DATA['DATA_LENGTH']:
177 count = PJLINK_ERST_DATA['DATA_LENGTH']
178- log.warning("{ip}) Invalid error status response '{data}': length != {count}".format(ip=self.ip,
179+ log.warning('{ip}) Invalid error status response "{data}": length != {count}'.format(ip=self.ip,
180 data=data,
181 count=count))
182 return
183@@ -297,7 +299,7 @@
184 datacheck = int(data)
185 except ValueError:
186 # Bad data - ignore
187- log.warning("({ip}) Invalid error status response '{data}'".format(ip=self.ip, data=data))
188+ log.warning('({ip}) Invalid error status response "{data}"'.format(ip=self.ip, data=data))
189 return
190 if datacheck == 0:
191 self.projector_errors = None
192@@ -430,6 +432,51 @@
193 log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.ip, data=self.pjlink_name))
194 return
195
196+ def process_pjlink(self, data):
197+ """
198+ Process initial socket connection to terminal.
199+
200+ :param data: Initial packet with authentication scheme
201+ """
202+ log.debug('({ip}) Processing PJLINK command'.format(ip=self.ip))
203+ chk = data.split(' ')
204+ if len(chk[0]) != 1:
205+ # Invalid - after splitting, first field should be 1 character, either '0' or '1' only
206+ log.error('({ip}) Invalid initial authentication scheme - aborting'.format(ip=self.ip))
207+ return self.disconnect_from_host()
208+ elif chk[0] == '0':
209+ # Normal connection no authentication
210+ if len(chk) > 1:
211+ # Invalid data - there should be nothing after a normal authentication scheme
212+ log.error('({ip}) Normal connection with extra information - aborting'.format(ip=self.ip))
213+ return self.disconnect_from_host()
214+ elif self.pin:
215+ log.error('({ip}) Normal connection but PIN set - aborting'.format(ip=self.ip))
216+ return self.disconnect_from_host()
217+ else:
218+ data_hash = None
219+ elif chk[0] == '1':
220+ if len(chk) < 2:
221+ # Not enough information for authenticated connection
222+ log.error('({ip}) Authenticated connection but not enough info - aborting'.format(ip=self.ip))
223+ return self.disconnect_from_host()
224+ elif not self.pin:
225+ log.error('({ip}) Authenticate connection but no PIN - aborting'.format(ip=self.ip))
226+ return self.disconnect_from_host()
227+ else:
228+ data_hash = str(qmd5_hash(salt=chk[1].encode('utf-8'), data=self.pin.encode('utf-8')),
229+ encoding='ascii')
230+ # Passed basic checks, so start connection
231+ self.readyRead.connect(self.get_socket)
232+ if not self.no_poll:
233+ log.debug('({ip}) process_pjlink(): Starting timer'.format(ip=self.ip))
234+ self.timer.setInterval(2000) # Set 2 seconds for initial information
235+ self.timer.start()
236+ self.change_status(S_CONNECTED)
237+ log.debug('({ip}) process_pjlink(): Sending "CLSS" initial command'.format(ip=self.ip))
238+ # Since this is an initial connection, make it a priority just in case
239+ return self.send_command(cmd="CLSS", salt=data_hash, priority=True)
240+
241 def process_powr(self, data):
242 """
243 Power status. See PJLink specification for format.
244@@ -450,7 +497,7 @@
245 self.send_command('INST')
246 else:
247 # Log unknown status response
248- log.warning('({ip}) Unknown power response: {data}'.format(ip=self.ip, data=data))
249+ log.warning('({ip}) Unknown power response: "{data}"'.format(ip=self.ip, data=data))
250 return
251
252 def process_rfil(self, data):
253@@ -460,9 +507,9 @@
254 if self.model_filter is None:
255 self.model_filter = data
256 else:
257- log.warning("({ip}) Filter model already set".format(ip=self.ip))
258- log.warning("({ip}) Saved model: '{old}'".format(ip=self.ip, old=self.model_filter))
259- log.warning("({ip}) New model: '{new}'".format(ip=self.ip, new=data))
260+ log.warning('({ip}) Filter model already set'.format(ip=self.ip))
261+ log.warning('({ip}) Saved model: "{old}"'.format(ip=self.ip, old=self.model_filter))
262+ log.warning('({ip}) New model: "{new}"'.format(ip=self.ip, new=data))
263
264 def process_rlmp(self, data):
265 """
266@@ -471,9 +518,9 @@
267 if self.model_lamp is None:
268 self.model_lamp = data
269 else:
270- log.warning("({ip}) Lamp model already set".format(ip=self.ip))
271- log.warning("({ip}) Saved lamp: '{old}'".format(ip=self.ip, old=self.model_lamp))
272- log.warning("({ip}) New lamp: '{new}'".format(ip=self.ip, new=data))
273+ log.warning('({ip}) Lamp model already set'.format(ip=self.ip))
274+ log.warning('({ip}) Saved lamp: "{old}"'.format(ip=self.ip, old=self.model_lamp))
275+ log.warning('({ip}) New lamp: "{new}"'.format(ip=self.ip, new=data))
276
277 def process_snum(self, data):
278 """
279@@ -482,16 +529,16 @@
280 :param data: Serial number from projector.
281 """
282 if self.serial_no is None:
283- log.debug("({ip}) Setting projector serial number to '{data}'".format(ip=self.ip, data=data))
284+ log.debug('({ip}) Setting projector serial number to "{data}"'.format(ip=self.ip, data=data))
285 self.serial_no = data
286 self.db_update = False
287 else:
288 # Compare serial numbers and see if we got the same projector
289 if self.serial_no != data:
290- log.warning("({ip}) Projector serial number does not match saved serial number".format(ip=self.ip))
291- log.warning("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.serial_no))
292- log.warning("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
293- log.warning("({ip}) NOT saving serial number".format(ip=self.ip))
294+ log.warning('({ip}) Projector serial number does not match saved serial number'.format(ip=self.ip))
295+ log.warning('({ip}) Saved: "{old}"'.format(ip=self.ip, old=self.serial_no))
296+ log.warning('({ip}) Received: "{new}"'.format(ip=self.ip, new=data))
297+ log.warning('({ip}) NOT saving serial number'.format(ip=self.ip))
298 self.serial_no_received = data
299
300 def process_sver(self, data):
301@@ -500,20 +547,20 @@
302 """
303 if len(data) > 32:
304 # Defined in specs max version is 32 characters
305- log.warning("Invalid software version - too long")
306+ log.warning('Invalid software version - too long')
307 return
308 elif self.sw_version is None:
309- log.debug("({ip}) Setting projector software version to '{data}'".format(ip=self.ip, data=data))
310+ log.debug('({ip}) Setting projector software version to "{data}"'.format(ip=self.ip, data=data))
311 self.sw_version = data
312 self.db_update = True
313 else:
314 # Compare software version and see if we got the same projector
315 if self.serial_no != data:
316- log.warning("({ip}) Projector software version does not match saved "
317- "software version".format(ip=self.ip))
318- log.warning("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.sw_version))
319- log.warning("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
320- log.warning("({ip}) Saving new serial number as sw_version_received".format(ip=self.ip))
321+ log.warning('({ip}) Projector software version does not match saved '
322+ 'software version'.format(ip=self.ip))
323+ log.warning('({ip}) Saved: "{old}"'.format(ip=self.ip, old=self.sw_version))
324+ log.warning('({ip}) Received: "{new}"'.format(ip=self.ip, new=data))
325+ log.warning('({ip}) Saving new serial number as sw_version_received'.format(ip=self.ip))
326 self.sw_version_received = data
327
328
329@@ -540,9 +587,9 @@
330 :param poll_time: Time (in seconds) to poll connected projector
331 :param socket_timeout: Time (in seconds) to abort the connection if no response
332 """
333- log.debug('PJlink(projector={projector}, args={args} kwargs={kwargs})'.format(projector=projector,
334- args=args,
335- kwargs=kwargs))
336+ log.debug('PJlink(projector="{projector}", args="{args}" kwargs="{kwargs}")'.format(projector=projector,
337+ args=args,
338+ kwargs=kwargs))
339 super().__init__()
340 self.entry = projector
341 self.ip = self.entry.ip
342@@ -573,6 +620,7 @@
343 self.widget = None # QListBox entry
344 self.timer = None # Timer that calls the poll_loop
345 self.send_queue = []
346+ self.priority_queue = []
347 self.send_busy = False
348 # Socket timer for some possible brain-dead projectors or network cable pulled
349 self.socket_timer = None
350@@ -586,6 +634,7 @@
351 self.connected.connect(self.check_login)
352 self.disconnected.connect(self.disconnect_from_host)
353 self.error.connect(self.get_error)
354+ self.projectorReceivedData.connect(self._send_command)
355
356 def thread_stopped(self):
357 """
358@@ -608,6 +657,10 @@
359 self.projectorReceivedData.disconnect(self._send_command)
360 except TypeError:
361 pass
362+ try:
363+ self.readyRead.disconnect(self.get_socket) # Set in process_pjlink
364+ except TypeError:
365+ pass
366 self.disconnect_from_host()
367 self.deleteLater()
368 self.i_am_running = False
369@@ -625,10 +678,10 @@
370 Retrieve information from projector that changes.
371 Normally called by timer().
372 """
373- if self.state() != self.ConnectedState:
374- log.warning("({ip}) poll_loop(): Not connected - returning".format(ip=self.ip))
375+ if self.state() != S_QSOCKET_STATE['ConnectedState']:
376+ log.warning('({ip}) poll_loop(): Not connected - returning'.format(ip=self.ip))
377 return
378- log.debug('({ip}) Updating projector status'.format(ip=self.ip))
379+ log.debug('({ip}) poll_loop(): Updating projector status'.format(ip=self.ip))
380 # Reset timer in case we were called from a set command
381 if self.timer.interval() < self.poll_time:
382 # Reset timer to 5 seconds
383@@ -640,28 +693,28 @@
384 if self.pjlink_class == '2':
385 check_list.extend(['FILT', 'FREZ'])
386 for command in check_list:
387- self.send_command(command, queue=True)
388+ self.send_command(command)
389 # The following commands do not change, so only check them once
390 if self.power == S_ON and self.source_available is None:
391- self.send_command('INST', queue=True)
392+ self.send_command('INST')
393 if self.other_info is None:
394- self.send_command('INFO', queue=True)
395+ self.send_command('INFO')
396 if self.manufacturer is None:
397- self.send_command('INF1', queue=True)
398+ self.send_command('INF1')
399 if self.model is None:
400- self.send_command('INF2', queue=True)
401+ self.send_command('INF2')
402 if self.pjlink_name is None:
403- self.send_command('NAME', queue=True)
404+ self.send_command('NAME')
405 if self.pjlink_class == '2':
406 # Class 2 specific checks
407 if self.serial_no is None:
408- self.send_command('SNUM', queue=True)
409+ self.send_command('SNUM')
410 if self.sw_version is None:
411- self.send_command('SVER', queue=True)
412+ self.send_command('SVER')
413 if self.model_filter is None:
414- self.send_command('RFIL', queue=True)
415+ self.send_command('RFIL')
416 if self.model_lamp is None:
417- self.send_command('RLMP', queue=True)
418+ self.send_command('RLMP')
419
420 def _get_status(self, status):
421 """
422@@ -713,14 +766,12 @@
423 code=status_code,
424 message=status_message if msg is None else msg))
425 self.changeStatus.emit(self.ip, status, message)
426+ self.projectorUpdateIcons.emit()
427
428 @QtCore.pyqtSlot()
429 def check_login(self, data=None):
430 """
431- Processes the initial connection and authentication (if needed).
432- Starts poll timer if connection is established.
433-
434- NOTE: Qt md5 hash function doesn't work with projector authentication. Use the python md5 hash function.
435+ Processes the initial connection and convert to a PJLink packet if valid initial connection
436
437 :param data: Optional data if called from another routine
438 """
439@@ -733,12 +784,12 @@
440 self.change_status(E_SOCKET_TIMEOUT)
441 return
442 read = self.readLine(self.max_size)
443- self.readLine(self.max_size) # Clean out the trailing \r\n
444+ self.readLine(self.max_size) # Clean out any trailing whitespace
445 if read is None:
446 log.warning('({ip}) read is None - socket error?'.format(ip=self.ip))
447 return
448 elif len(read) < 8:
449- log.warning('({ip}) Not enough data read)'.format(ip=self.ip))
450+ log.warning('({ip}) Not enough data read - skipping'.format(ip=self.ip))
451 return
452 data = decode(read, 'utf-8')
453 # Possibility of extraneous data on input when reading.
454@@ -750,9 +801,16 @@
455 # PJLink initial login will be:
456 # 'PJLink 0' - Unauthenticated login - no extra steps required.
457 # 'PJLink 1 XXXXXX' Authenticated login - extra processing required.
458- if not data.upper().startswith('PJLINK'):
459- # Invalid response
460+ if not data.startswith('PJLINK'):
461+ # Invalid initial packet - close socket
462+ log.error('({ip}) Invalid initial packet received - closing socket'.format(ip=self.ip))
463 return self.disconnect_from_host()
464+ log.debug('({ip}) check_login(): Formatting initial connection prompt to PJLink packet'.format(ip=self.ip))
465+ return self.get_data('{start}{clss}{data}'.format(start=PJLINK_PREFIX,
466+ clss='1',
467+ data=data.replace(' ', '=', 1)).encode('utf-8'))
468+ # TODO: The below is replaced by process_pjlink() - remove when working properly
469+ """
470 if '=' in data:
471 # Processing a login reply
472 data_check = data.strip().split('=')
473@@ -801,18 +859,19 @@
474 log.debug('({ip}) Starting timer'.format(ip=self.ip))
475 self.timer.setInterval(2000) # Set 2 seconds for initial information
476 self.timer.start()
477+ """
478
479 def _trash_buffer(self, msg=None):
480 """
481 Clean out extraneous stuff in the buffer.
482 """
483- log.warning("({ip}) {message}".format(ip=self.ip, message='Invalid packet' if msg is None else msg))
484+ log.warning('({ip}) {message}'.format(ip=self.ip, message='Invalid packet' if msg is None else msg))
485 self.send_busy = False
486 trash_count = 0
487 while self.bytesAvailable() > 0:
488 trash = self.read(self.max_size)
489 trash_count += len(trash)
490- log.debug("({ip}) Finished cleaning buffer - {count} bytes dropped".format(ip=self.ip,
491+ log.debug('({ip}) Finished cleaning buffer - {count} bytes dropped'.format(ip=self.ip,
492 count=trash_count))
493 return
494
495@@ -824,7 +883,7 @@
496 :param data: Data to process. buffer must be formatted as a proper PJLink packet.
497 :param ip: Destination IP for buffer.
498 """
499- log.debug("({ip}) get_buffer(data='{buff}' ip='{ip_in}'".format(ip=self.ip, buff=data, ip_in=ip))
500+ log.debug('({ip}) get_buffer(data="{buff}" ip="{ip_in}"'.format(ip=self.ip, buff=data, ip_in=ip))
501 if ip is None:
502 log.debug("({ip}) get_buffer() Don't know who data is for - exiting".format(ip=self.ip))
503 return
504@@ -842,38 +901,52 @@
505 return
506 # Although we have a packet length limit, go ahead and use a larger buffer
507 read = self.readLine(1024)
508- log.debug("({ip}) get_socket(): '{buff}'".format(ip=self.ip, buff=read))
509+ log.debug('({ip}) get_socket(): "{buff}"'.format(ip=self.ip, buff=read))
510 if read == -1:
511 # No data available
512 log.debug('({ip}) get_socket(): No data available (-1)'.format(ip=self.ip))
513 return self.receive_data_signal()
514 self.socket_timer.stop()
515- return self.get_data(buff=read, ip=self.ip)
516+ self.get_data(buff=read, ip=self.ip)
517+ return self.receive_data_signal()
518
519- def get_data(self, buff, ip):
520+ def get_data(self, buff, ip=None):
521 """
522 Process received data
523
524 :param buff: Data to process.
525 :param ip: (optional) Destination IP.
526 """
527- log.debug("({ip}) get_data(ip='{ip_in}' buffer='{buff}'".format(ip=self.ip, ip_in=ip, buff=buff))
528+ # Since "self" is not available to options and the "ip" keyword is a "maybe I'll use in the future",
529+ # set to default here
530+ if ip is None:
531+ ip = self.ip
532+ log.debug('({ip}) get_data(ip="{ip_in}" buffer="{buff}"'.format(ip=self.ip, ip_in=ip, buff=buff))
533 # NOTE: Class2 has changed to some values being UTF-8
534 data_in = decode(buff, 'utf-8')
535 data = data_in.strip()
536- if (len(data) < 7) or (not data.startswith(PJLINK_PREFIX)):
537- return self._trash_buffer(msg='get_data(): Invalid packet - length or prefix')
538+ # Initial packet checks
539+ if (len(data) < 7):
540+ return self._trash_buffer(msg='get_data(): Invalid packet - length')
541 elif len(data) > self.max_size:
542 return self._trash_buffer(msg='get_data(): Invalid packet - too long')
543+ elif not data.startswith(PJLINK_PREFIX):
544+ return self._trash_buffer(msg='get_data(): Invalid packet - PJLink prefix missing')
545 elif '=' not in data:
546- return self._trash_buffer(msg='get_data(): Invalid packet does not have equal')
547+ return self._trash_buffer(msg='get_data(): Invalid reply - Does not have "="')
548 log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data))
549 header, data = data.split('=')
550+ # At this point, the header should contain:
551+ # "PVCCCC"
552+ # Where:
553+ # P = PJLINK_PREFIX
554+ # V = PJLink class or version
555+ # C = PJLink command
556 try:
557- version, cmd = header[1], header[2:]
558+ version, cmd = header[1], header[2:].upper()
559 except ValueError as e:
560 self.change_status(E_INVALID_DATA)
561- log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip()))
562+ log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in))
563 return self._trash_buffer('get_data(): Expected header + command + data')
564 if cmd not in PJLINK_VALID_CMD:
565 log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
566@@ -881,6 +954,7 @@
567 if int(self.pjlink_class) < int(version):
568 log.warning('({ip}) get_data(): Projector returned class reply higher '
569 'than projector stated class'.format(ip=self.ip))
570+ self.send_busy = False
571 return self.process_command(cmd, data)
572
573 @QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
574@@ -910,19 +984,18 @@
575 self.reset_information()
576 return
577
578- def send_command(self, cmd, opts='?', salt=None, queue=False):
579+ def send_command(self, cmd, opts='?', salt=None, priority=False):
580 """
581 Add command to output queue if not already in queue.
582
583 :param cmd: Command to send
584 :param opts: Command option (if any) - defaults to '?' (get information)
585 :param salt: Optional salt for md5 hash initial authentication
586- :param queue: Option to force add to queue rather than sending directly
587+ :param priority: Option to send packet now rather than queue it up
588 """
589 if self.state() != self.ConnectedState:
590 log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip))
591- self.send_queue = []
592- return
593+ return self.reset_information()
594 if cmd not in PJLINK_VALID_CMD:
595 log.error('({ip}) send_command(): Invalid command requested - ignoring.'.format(ip=self.ip))
596 return
597@@ -939,28 +1012,26 @@
598 header = PJLINK_HEADER.format(linkclass=cmd_ver[0])
599 else:
600 # NOTE: Once we get to version 3 then think about looping
601- log.error('({ip}): send_command(): PJLink class check issue? aborting'.format(ip=self.ip))
602+ log.error('({ip}): send_command(): PJLink class check issue? Aborting'.format(ip=self.ip))
603 return
604 out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
605 header=header,
606 command=cmd,
607 options=opts,
608 suffix=CR)
609- if out in self.send_queue:
610- # Already there, so don't add
611- log.debug('({ip}) send_command(out="{data}") Already in queue - skipping'.format(ip=self.ip,
612- data=out.strip()))
613- elif not queue and len(self.send_queue) == 0:
614- # Nothing waiting to send, so just send it
615- log.debug('({ip}) send_command(out="{data}") Sending data'.format(ip=self.ip, data=out.strip()))
616- return self._send_command(data=out)
617+ if out in self.priority_queue:
618+ log.debug('({ip}) send_command(): Already in priority queue - skipping'.format(ip=self.ip))
619+ elif out in self.send_queue:
620+ log.debug('({ip}) send_command(): Already in normal queue - skipping'.format(ip=self.ip))
621 else:
622- log.debug('({ip}) send_command(out="{data}") adding to queue'.format(ip=self.ip, data=out.strip()))
623- self.send_queue.append(out)
624- self.projectorReceivedData.emit()
625- log.debug('({ip}) send_command(): send_busy is {data}'.format(ip=self.ip, data=self.send_busy))
626- if not self.send_busy:
627- log.debug('({ip}) send_command() calling _send_string()'.format(ip=self.ip))
628+ if priority:
629+ log.debug('({ip}) send_command(): Adding to priority queue'.format(ip=self.ip))
630+ self.priority_queue.append(out)
631+ else:
632+ log.debug('({ip}) send_command(): Adding to normal queue'.format(ip=self.ip))
633+ self.send_queue.append(out)
634+ if self.priority_queue or self.send_queue:
635+ # May be some initial connection setup so make sure we send data
636 self._send_command()
637
638 @QtCore.pyqtSlot()
639@@ -971,43 +1042,53 @@
640 :param data: Immediate data to send
641 :param utf8: Send as UTF-8 string otherwise send as ASCII string
642 """
643- log.debug('({ip}) _send_string()'.format(ip=self.ip))
644- log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state()))
645+ # Funny looking data check, but it's a quick check for data=None
646+ log.debug('({ip}) _send_command(data="{data}")'.format(ip=self.ip, data=data.strip() if data else data))
647+ log.debug('({ip}) _send_command(): Connection status: {data}'.format(ip=self.ip,
648+ data=S_QSOCKET_STATE[self.state()]))
649 if self.state() != self.ConnectedState:
650- log.debug('({ip}) _send_string() Not connected - abort'.format(ip=self.ip))
651- self.send_queue = []
652+ log.debug('({ip}) _send_command() Not connected - abort'.format(ip=self.ip))
653 self.send_busy = False
654- return
655+ return self.disconnect_from_host()
656+ if data and data not in self.priority_queue:
657+ log.debug('({ip}) _send_command(): Priority packet - adding to priority queue'.format(ip=self.ip))
658+ self.priority_queue.append(data)
659+
660 if self.send_busy:
661 # Still waiting for response from last command sent
662+ log.debug('({ip}) _send_command(): Still busy, returning'.format(ip=self.ip))
663+ log.debug('({ip}) _send_command(): Priority queue = {data}'.format(ip=self.ip, data=self.priority_queue))
664+ log.debug('({ip}) _send_command(): Normal queue = {data}'.format(ip=self.ip, data=self.send_queue))
665 return
666- if data is not None:
667- out = data
668- log.debug('({ip}) _send_string(data="{data}")'.format(ip=self.ip, data=out.strip()))
669+
670+ if len(self.priority_queue) != 0:
671+ out = self.priority_queue.pop(0)
672+ log.debug('({ip}) _send_command(): Getting priority queued packet'.format(ip=self.ip))
673 elif len(self.send_queue) != 0:
674 out = self.send_queue.pop(0)
675- log.debug('({ip}) _send_string(queued data="{data}"%s)'.format(ip=self.ip, data=out.strip()))
676+ log.debug('({ip}) _send_command(): Getting normal queued packet'.format(ip=self.ip))
677 else:
678 # No data to send
679- log.debug('({ip}) _send_string(): No data to send'.format(ip=self.ip))
680+ log.debug('({ip}) _send_command(): No data to send'.format(ip=self.ip))
681 self.send_busy = False
682 return
683 self.send_busy = True
684- log.debug('({ip}) _send_string(): Sending "{data}"'.format(ip=self.ip, data=out.strip()))
685- log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue))
686+ log.debug('({ip}) _send_command(): Sending "{data}"'.format(ip=self.ip, data=out.strip()))
687 self.socket_timer.start()
688 sent = self.write(out.encode('{string_encoding}'.format(string_encoding='utf-8' if utf8 else 'ascii')))
689 self.waitForBytesWritten(2000) # 2 seconds should be enough
690 if sent == -1:
691 # Network error?
692- log.warning("({ip}) _send_command(): -1 received".format(ip=self.ip))
693+ log.warning('({ip}) _send_command(): -1 received - disconnecting from host'.format(ip=self.ip))
694 self.change_status(E_NETWORK,
695 translate('OpenLP.PJLink', 'Error while sending data to projector'))
696+ self.disconnect_from_host()
697
698 def connect_to_host(self):
699 """
700 Initiate connection to projector.
701 """
702+ log.debug('{ip}) connect_to_host(): Starting connection'.format(ip=self.ip))
703 if self.state() == self.ConnectedState:
704 log.warning('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.ip))
705 return
706@@ -1023,22 +1104,19 @@
707 if abort:
708 log.warning('({ip}) disconnect_from_host(): Aborting connection'.format(ip=self.ip))
709 else:
710- log.warning('({ip}) disconnect_from_host(): Not connected - returning'.format(ip=self.ip))
711- self.reset_information()
712+ log.warning('({ip}) disconnect_from_host(): Not connected'.format(ip=self.ip))
713 self.disconnectFromHost()
714 try:
715 self.readyRead.disconnect(self.get_socket)
716 except TypeError:
717 pass
718+ log.debug('({ip}) disconnect_from_host() '
719+ 'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0]))
720 if abort:
721 self.change_status(E_NOT_CONNECTED)
722 else:
723- log.debug('({ip}) disconnect_from_host() '
724- 'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0]))
725- if self.status_connect != E_NOT_CONNECTED:
726- self.change_status(S_NOT_CONNECTED)
727+ self.change_status(S_NOT_CONNECTED)
728 self.reset_information()
729- self.projectorUpdateIcons.emit()
730
731 def get_av_mute_status(self):
732 """
733
734=== modified file 'tests/functional/openlp_core/projectors/test_projector_bugfixes_01.py'
735--- tests/functional/openlp_core/projectors/test_projector_bugfixes_01.py 2017-11-24 19:08:23 +0000
736+++ tests/functional/openlp_core/projectors/test_projector_bugfixes_01.py 2017-12-09 11:19:42 +0000
737@@ -23,12 +23,11 @@
738 Package to test the openlp.core.projectors.pjlink base package.
739 """
740 from unittest import TestCase
741-from unittest.mock import patch
742
743 from openlp.core.projectors.db import Projector
744 from openlp.core.projectors.pjlink import PJLink
745
746-from tests.resources.projector.data import TEST_PIN, TEST_CONNECT_AUTHENTICATE, TEST_HASH, TEST1_DATA
747+from tests.resources.projector.data import TEST1_DATA
748
749
750 class TestPJLinkBugs(TestCase):
751@@ -80,43 +79,17 @@
752 """
753 Test bug 1593882 no pin and authenticated request exception
754 """
755- # GIVEN: Test object and mocks
756- mock_socket_timer = patch.object(self.pjlink_test, 'socket_timer').start()
757- mock_timer = patch.object(self.pjlink_test, 'timer').start()
758- mock_authentication = patch.object(self.pjlink_test, 'projectorAuthentication').start()
759- mock_ready_read = patch.object(self.pjlink_test, 'waitForReadyRead').start()
760- mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
761- pjlink = self.pjlink_test
762- pjlink.pin = None
763- mock_ready_read.return_value = True
764-
765- # WHEN: call with authentication request and pin not set
766- pjlink.check_login(data=TEST_CONNECT_AUTHENTICATE)
767-
768- # THEN: 'No Authentication' signal should have been sent
769- mock_authentication.emit.assert_called_with(pjlink.ip)
770+ # Test now part of test_projector_pjlink_commands_02
771+ # Keeping here for bug reference
772+ pass
773
774 def test_bug_1593883_pjlink_authentication(self):
775 """
776- Test bugfix 1593883 pjlink authentication
777+ Test bugfix 1593883 pjlink authentication and ticket 92187
778 """
779- # GIVEN: Test object and data
780- mock_socket_timer = patch.object(self.pjlink_test, 'socket_timer').start()
781- mock_timer = patch.object(self.pjlink_test, 'timer').start()
782- mock_send_command = patch.object(self.pjlink_test, 'write').start()
783- mock_state = patch.object(self.pjlink_test, 'state').start()
784- mock_waitForReadyRead = patch.object(self.pjlink_test, 'waitForReadyRead').start()
785- pjlink = self.pjlink_test
786- pjlink.pin = TEST_PIN
787- mock_state.return_value = pjlink.ConnectedState
788- mock_waitForReadyRead.return_value = True
789-
790- # WHEN: Athenticated connection is called
791- pjlink.check_login(data=TEST_CONNECT_AUTHENTICATE)
792-
793- # THEN: send_command should have the proper authentication
794- self.assertEqual("{test}".format(test=mock_send_command.call_args),
795- "call(b'{hash}%1CLSS ?\\r')".format(hash=TEST_HASH))
796+ # Test now part of test_projector_pjlink_commands_02
797+ # Keeping here for bug reference
798+ pass
799
800 def test_bug_1734275_process_lamp_nonstandard_reply(self):
801 """
802
803=== modified file 'tests/functional/openlp_core/projectors/test_projector_pjlink_base.py'
804--- tests/functional/openlp_core/projectors/test_projector_pjlink_base.py 2017-11-24 08:30:37 +0000
805+++ tests/functional/openlp_core/projectors/test_projector_pjlink_base.py 2017-12-09 11:19:42 +0000
806@@ -25,11 +25,11 @@
807 from unittest import TestCase
808 from unittest.mock import call, patch, MagicMock
809
810-from openlp.core.projectors.constants import E_PARAMETER, ERROR_STRING, S_ON, S_CONNECTED
811+from openlp.core.projectors.constants import E_PARAMETER, ERROR_STRING, S_ON, S_CONNECTED, S_QSOCKET_STATE
812 from openlp.core.projectors.db import Projector
813 from openlp.core.projectors.pjlink import PJLink
814
815-from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST1_DATA
816+from tests.resources.projector.data import TEST1_DATA
817
818 pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True)
819
820@@ -38,29 +38,17 @@
821 """
822 Tests for the PJLink module
823 """
824- @patch.object(pjlink_test, 'readyRead')
825- @patch.object(pjlink_test, 'send_command')
826- @patch.object(pjlink_test, 'waitForReadyRead')
827- @patch('openlp.core.common.qmd5_hash')
828- def test_authenticated_connection_call(self,
829- mock_qmd5_hash,
830- mock_waitForReadyRead,
831- mock_send_command,
832- mock_readyRead):
833- """
834- Ticket 92187: Fix for projector connect with PJLink authentication exception.
835- """
836- # GIVEN: Test object
837- pjlink = pjlink_test
838-
839- # WHEN: Calling check_login with authentication request:
840- pjlink.check_login(data=TEST_CONNECT_AUTHENTICATE)
841-
842- # THEN: Should have called qmd5_hash
843- self.assertTrue(mock_qmd5_hash.called_with(TEST_SALT,
844- "Connection request should have been called with TEST_SALT"))
845- self.assertTrue(mock_qmd5_hash.called_with(TEST_PIN,
846- "Connection request should have been called with TEST_PIN"))
847+ def setUp(self):
848+ '''
849+ TestPJLinkCommands part 2 initialization
850+ '''
851+ self.pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True)
852+
853+ def tearDown(self):
854+ '''
855+ TestPJLinkCommands part 2 cleanups
856+ '''
857+ self.pjlink_test = None
858
859 @patch.object(pjlink_test, 'change_status')
860 def test_status_change(self, mock_change_status):
861@@ -110,18 +98,18 @@
862 # THEN: poll_loop should exit without calling any other method
863 self.assertFalse(pjlink.timer.called, 'Should have returned without calling any other method')
864
865- @patch.object(pjlink_test, 'send_command')
866- def test_poll_loop_start(self, mock_send_command):
867+ def test_poll_loop_start(self):
868 """
869 Test PJLink.poll_loop makes correct calls
870 """
871- # GIVEN: test object and test data
872- pjlink = pjlink_test
873- pjlink.state = MagicMock()
874- pjlink.timer = MagicMock()
875- pjlink.timer.interval = MagicMock()
876- pjlink.timer.setInterval = MagicMock()
877- pjlink.timer.start = MagicMock()
878+ # GIVEN: Mocks and test data
879+ mock_state = patch.object(self.pjlink_test, 'state').start()
880+ mock_state.return_value = S_QSOCKET_STATE['ConnectedState']
881+ mock_timer = patch.object(self.pjlink_test, 'timer').start()
882+ mock_timer.interval.return_value = 10
883+ mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
884+
885+ pjlink = self.pjlink_test
886 pjlink.poll_time = 20
887 pjlink.power = S_ON
888 pjlink.source_available = None
889@@ -130,19 +118,17 @@
890 pjlink.model = None
891 pjlink.pjlink_name = None
892 pjlink.ConnectedState = S_CONNECTED
893- pjlink.timer.interval.return_value = 10
894- pjlink.state.return_value = S_CONNECTED
895 call_list = [
896- call('POWR', queue=True),
897- call('ERST', queue=True),
898- call('LAMP', queue=True),
899- call('AVMT', queue=True),
900- call('INPT', queue=True),
901- call('INST', queue=True),
902- call('INFO', queue=True),
903- call('INF1', queue=True),
904- call('INF2', queue=True),
905- call('NAME', queue=True),
906+ call('POWR'),
907+ call('ERST'),
908+ call('LAMP'),
909+ call('AVMT'),
910+ call('INPT'),
911+ call('INST'),
912+ call('INFO'),
913+ call('INF1'),
914+ call('INF2'),
915+ call('NAME'),
916 ]
917
918 # WHEN: PJLink.poll_loop is called
919@@ -150,8 +136,8 @@
920
921 # THEN: proper calls were made to retrieve projector data
922 # First, call to update the timer with the next interval
923- self.assertTrue(pjlink.timer.setInterval.called, 'Should have updated the timer')
924+ self.assertTrue(mock_timer.setInterval.called)
925 # Next, should have called the timer to start
926- self.assertTrue(pjlink.timer.start.called, 'Should have started the timer')
927+ self.assertTrue(mock_timer.start.called, 'Should have started the timer')
928 # Finally, should have called send_command with a list of projetctor status checks
929 mock_send_command.assert_has_calls(call_list, 'Should have queued projector queries')
930
931=== modified file 'tests/functional/openlp_core/projectors/test_projector_pjlink_cmd_routing.py'
932--- tests/functional/openlp_core/projectors/test_projector_pjlink_cmd_routing.py 2017-11-16 23:53:53 +0000
933+++ tests/functional/openlp_core/projectors/test_projector_pjlink_cmd_routing.py 2017-12-09 11:19:42 +0000
934@@ -46,6 +46,18 @@
935 """
936 Tests for the PJLink module command routing
937 """
938+ def setUp(self):
939+ '''
940+ TestPJLinkCommands part 2 initialization
941+ '''
942+ self.pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True)
943+
944+ def tearDown(self):
945+ '''
946+ TestPJLinkCommands part 2 cleanups
947+ '''
948+ self.pjlink_test = None
949+
950 @patch.object(openlp.core.projectors.pjlink, 'log')
951 def test_process_command_call_clss(self, mock_log):
952 """
953@@ -163,21 +175,20 @@
954 mock_change_status.assert_called_once_with(E_AUTHENTICATION)
955 mock_log.error.assert_called_with(log_text)
956
957- @patch.object(openlp.core.projectors.pjlink, 'log')
958- def test_process_command_future(self, mock_log):
959+ def test_process_command_future(self):
960 """
961 Test command valid but no method to process yet
962 """
963- # GIVEN: Test object
964- pjlink = pjlink_test
965- log_text = "(127.0.0.1) Unable to process command='CLSS' (Future option)"
966- mock_log.reset_mock()
967- # Remove a valid command so we can test valid command but not available yet
968- pjlink.pjlink_functions.pop('CLSS')
969+ # GIVEN: Initial mocks and data
970+ mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start()
971+ mock_functions = patch.object(self.pjlink_test, 'pjlink_functions').start()
972+ mock_functions.return_value = []
973+
974+ pjlink = self.pjlink_test
975+ log_text = '(111.111.111.111) Unable to process command="CLSS" (Future option?)'
976
977 # WHEN: process_command called with an unknown command
978- with patch.object(pjlink, 'pjlink_functions') as mock_functions:
979- pjlink.process_command(cmd='CLSS', data='DONT CARE')
980+ pjlink.process_command(cmd='CLSS', data='DONT CARE')
981
982 # THEN: Error should be logged and no command called
983 self.assertFalse(mock_functions.called, 'Should not have gotten to the end of the method')
984@@ -196,29 +207,26 @@
985
986 # WHEN: process_command called with an unknown command
987 pjlink.process_command(cmd='Unknown', data='Dont Care')
988- log_text = "(127.0.0.1) Ignoring command='Unknown' (Invalid/Unknown)"
989+ log_text = '(127.0.0.1) Ignoring command="Unknown" (Invalid/Unknown)'
990
991 # THEN: Error should be logged and no command called
992 self.assertFalse(mock_functions.called, 'Should not have gotten to the end of the method')
993 mock_log.error.assert_called_once_with(log_text)
994
995- @patch.object(pjlink_test, 'pjlink_functions')
996- @patch.object(openlp.core.projectors.pjlink, 'log')
997- def test_process_command_ok(self, mock_log, mock_functions):
998+ def test_process_command_ok(self):
999 """
1000 Test command returned success
1001 """
1002- # GIVEN: Test object
1003- pjlink = pjlink_test
1004- mock_functions.reset_mock()
1005- mock_log.reset_mock()
1006-
1007- # WHEN: process_command called with an unknown command
1008- pjlink.process_command(cmd='CLSS', data='OK')
1009- log_text = '(127.0.0.1) Command "CLSS" returned OK'
1010-
1011- # THEN: Error should be logged and no command called
1012- self.assertFalse(mock_functions.called, 'Should not have gotten to the end of the method')
1013- self.assertEqual(mock_log.debug.call_count, 2, 'log.debug() should have been called twice')
1014- # Although we called it twice, only the last log entry is saved
1015+ # GIVEN: Initial mocks and data
1016+ mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start()
1017+ mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
1018+
1019+ pjlink = self.pjlink_test
1020+ log_text = '(111.111.111.111) Command "POWR" returned OK'
1021+
1022+ # WHEN: process_command called with a command that returns OK
1023+ pjlink.process_command(cmd='POWR', data='OK')
1024+
1025+ # THEN: Appropriate calls should have been made
1026 mock_log.debug.assert_called_with(log_text)
1027+ mock_send_command.assert_called_once_with(cmd='POWR')
1028
1029=== renamed file 'tests/functional/openlp_core/projectors/test_projector_pjlink_commands.py' => 'tests/functional/openlp_core/projectors/test_projector_pjlink_commands_01.py'
1030--- tests/functional/openlp_core/projectors/test_projector_pjlink_commands.py 2017-11-24 08:30:37 +0000
1031+++ tests/functional/openlp_core/projectors/test_projector_pjlink_commands_01.py 2017-12-09 11:19:42 +0000
1032@@ -47,7 +47,7 @@
1033
1034 class TestPJLinkCommands(TestCase):
1035 """
1036- Tests for the PJLink module
1037+ Tests for the PJLinkCommands class part 1
1038 """
1039 @patch.object(pjlink_test, 'changeStatus')
1040 @patch.object(openlp.core.projectors.pjlink, 'log')
1041@@ -580,7 +580,7 @@
1042
1043 # WHEN: Process invalid reply
1044 pjlink.process_clss('Z')
1045- log_text = "(127.0.0.1) NAN clss version reply 'Z' - defaulting to class '1'"
1046+ log_text = '(127.0.0.1) NAN CLSS version reply "Z" - defaulting to class "1"'
1047
1048 # THEN: Projector class should be set with default value
1049 self.assertEqual(pjlink.pjlink_class, '1',
1050@@ -597,7 +597,7 @@
1051
1052 # WHEN: Process invalid reply
1053 pjlink.process_clss('Invalid')
1054- log_text = "(127.0.0.1) No numbers found in class version reply 'Invalid' - defaulting to class '1'"
1055+ log_text = '(127.0.0.1) No numbers found in class version reply "Invalid" - defaulting to class "1"'
1056
1057 # THEN: Projector class should be set with default value
1058 self.assertEqual(pjlink.pjlink_class, '1',
1059@@ -627,7 +627,7 @@
1060 # GIVEN: Test object
1061 pjlink = pjlink_test
1062 pjlink.projector_errors = None
1063- log_text = "127.0.0.1) Invalid error status response '11111111': length != 6"
1064+ log_text = '127.0.0.1) Invalid error status response "11111111": length != 6'
1065
1066 # WHEN: process_erst called with invalid data (too many values
1067 pjlink.process_erst('11111111')
1068@@ -645,7 +645,7 @@
1069 # GIVEN: Test object
1070 pjlink = pjlink_test
1071 pjlink.projector_errors = None
1072- log_text = "(127.0.0.1) Invalid error status response '1111Z1'"
1073+ log_text = '(127.0.0.1) Invalid error status response "1111Z1"'
1074
1075 # WHEN: process_erst called with invalid data (too many values
1076 pjlink.process_erst('1111Z1')
1077@@ -671,8 +671,8 @@
1078 # THEN: PJLink instance errors should match chk_value
1079 for chk in pjlink.projector_errors:
1080 self.assertEqual(pjlink.projector_errors[chk], chk_string,
1081- "projector_errors['{chk}'] should have been set to {err}".format(chk=chk,
1082- err=chk_string))
1083+ 'projector_errors["{chk}"] should have been set to "{err}"'.format(chk=chk,
1084+ err=chk_string))
1085
1086 def test_projector_process_erst_all_error(self):
1087 """
1088@@ -690,8 +690,8 @@
1089 # THEN: PJLink instance errors should match chk_value
1090 for chk in pjlink.projector_errors:
1091 self.assertEqual(pjlink.projector_errors[chk], chk_string,
1092- "projector_errors['{chk}'] should have been set to {err}".format(chk=chk,
1093- err=chk_string))
1094+ 'projector_errors["{chk}"] should have been set to "{err}"'.format(chk=chk,
1095+ err=chk_string))
1096
1097 def test_projector_process_erst_warn_cover_only(self):
1098 """
1099@@ -744,9 +744,9 @@
1100 pjlink = pjlink_test
1101 pjlink.source_available = []
1102 test_data = '21 10 30 31 11 20'
1103- test_saved = ['10', '11', '20', '21', '30', '31']
1104- log_data = '(127.0.0.1) Setting projector sources_available to ' \
1105- '"[\'10\', \'11\', \'20\', \'21\', \'30\', \'31\']"'
1106+ test_saved = ["10", "11", "20", "21", "30", "31"]
1107+ log_data = "(127.0.0.1) Setting projector sources_available to " \
1108+ "\"['10', '11', '20', '21', '30', '31']\""
1109 mock_UpdateIcons.reset_mock()
1110 mock_log.reset_mock()
1111
1112@@ -1021,7 +1021,7 @@
1113 pjlink.sw_version = None
1114 pjlink.sw_version_received = None
1115 test_data = 'Test 1 Subtest 1'
1116- test_log = "(127.0.0.1) Setting projector software version to 'Test 1 Subtest 1'"
1117+ test_log = '(127.0.0.1) Setting projector software version to "Test 1 Subtest 1"'
1118 mock_log.reset_mock()
1119
1120 # WHEN: process_sver called with invalid data
1121
1122=== added file 'tests/functional/openlp_core/projectors/test_projector_pjlink_commands_02.py'
1123--- tests/functional/openlp_core/projectors/test_projector_pjlink_commands_02.py 1970-01-01 00:00:00 +0000
1124+++ tests/functional/openlp_core/projectors/test_projector_pjlink_commands_02.py 2017-12-09 11:19:42 +0000
1125@@ -0,0 +1,198 @@
1126+# -*- coding: utf-8 -*-
1127+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1128+
1129+###############################################################################
1130+# OpenLP - Open Source Lyrics Projection #
1131+# --------------------------------------------------------------------------- #
1132+# Copyright (c) 2008-2015 OpenLP Developers #
1133+# --------------------------------------------------------------------------- #
1134+# This program is free software; you can redistribute it and/or modify it #
1135+# under the terms of the GNU General Public License as published by the Free #
1136+# Software Foundation; version 2 of the License. #
1137+# #
1138+# This program is distributed in the hope that it will be useful, but WITHOUT #
1139+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1140+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1141+# more details. #
1142+# #
1143+# You should have received a copy of the GNU General Public License along #
1144+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1145+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1146+###############################################################################
1147+"""
1148+Package to test the openlp.core.projectors.pjlink commands package.
1149+"""
1150+from unittest import TestCase
1151+from unittest.mock import patch, call
1152+
1153+import openlp.core.projectors.pjlink
1154+from openlp.core.projectors.constants import S_CONNECTED
1155+from openlp.core.projectors.db import Projector
1156+from openlp.core.projectors.pjlink import PJLink
1157+
1158+from tests.resources.projector.data import TEST_HASH, TEST_PIN, TEST_SALT, TEST1_DATA
1159+
1160+
1161+class TestPJLinkCommands(TestCase):
1162+ """
1163+ Tests for the PJLinkCommands class part 2
1164+ """
1165+ def setUp(self):
1166+ '''
1167+ TestPJLinkCommands part 2 initialization
1168+ '''
1169+ self.pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True)
1170+
1171+ def tearDown(self):
1172+ '''
1173+ TestPJLinkCommands part 2 cleanups
1174+ '''
1175+ self.pjlink_test = None
1176+
1177+ def test_process_pjlink_normal(self):
1178+ """
1179+ Test initial connection prompt with no authentication
1180+ """
1181+ # GIVEN: Initial mocks and data
1182+ mock_log = patch.object(openlp.core.projectors.pjlink, "log").start()
1183+ mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start()
1184+ mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
1185+ mock_readyRead = patch.object(self.pjlink_test, 'readyRead').start()
1186+ mock_change_status = patch.object(self.pjlink_test, 'change_status').start()
1187+ pjlink = self.pjlink_test
1188+ pjlink.pin = None
1189+ log_check = [call("({111.111.111.111}) process_pjlink(): Sending 'CLSS' initial command'"), ]
1190+
1191+ # WHEN: process_pjlink called with no authentication required
1192+ pjlink.process_pjlink(data="0")
1193+
1194+ # THEN: proper processing should have occured
1195+ mock_log.debug.has_calls(log_check)
1196+ mock_disconnect_from_host.assert_not_called()
1197+ self.assertEqual(mock_readyRead.connect.call_count, 1, 'Should have only been called once')
1198+ mock_change_status.assert_called_once_with(S_CONNECTED)
1199+ mock_send_command.assert_called_with(cmd='CLSS', priority=True, salt=None)
1200+
1201+ def test_process_pjlink_authenticate(self):
1202+ """
1203+ Test initial connection prompt with authentication
1204+ """
1205+ # GIVEN: Initial mocks and data
1206+ mock_log = patch.object(openlp.core.projectors.pjlink, "log").start()
1207+ mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start()
1208+ mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
1209+ mock_readyRead = patch.object(self.pjlink_test, 'readyRead').start()
1210+ mock_change_status = patch.object(self.pjlink_test, 'change_status').start()
1211+ pjlink = self.pjlink_test
1212+ pjlink.pin = TEST_PIN
1213+ log_check = [call("({111.111.111.111}) process_pjlink(): Sending 'CLSS' initial command'"), ]
1214+
1215+ # WHEN: process_pjlink called with no authentication required
1216+ pjlink.process_pjlink(data='1 {salt}'.format(salt=TEST_SALT))
1217+
1218+ # THEN: proper processing should have occured
1219+ mock_log.debug.has_calls(log_check)
1220+ mock_disconnect_from_host.assert_not_called()
1221+ self.assertEqual(mock_readyRead.connect.call_count, 1, 'Should have only been called once')
1222+ mock_change_status.assert_called_once_with(S_CONNECTED)
1223+ mock_send_command.assert_called_with(cmd='CLSS', priority=True, salt=TEST_HASH)
1224+
1225+ def test_process_pjlink_normal_pin_set_error(self):
1226+ """
1227+ Test process_pjlinnk called with no authentication but pin is set
1228+ """
1229+ # GIVEN: Initial mocks and data
1230+ # GIVEN: Initial mocks and data
1231+ mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start()
1232+ mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start()
1233+ mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
1234+ pjlink = self.pjlink_test
1235+ pjlink.pin = TEST_PIN
1236+ log_check = [call('(111.111.111.111) Normal connection but PIN set - aborting'), ]
1237+
1238+ # WHEN: process_pjlink called with invalid authentication scheme
1239+ pjlink.process_pjlink(data='0')
1240+
1241+ # THEN: Proper calls should be made
1242+ mock_log.error.assert_has_calls(log_check)
1243+ self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once')
1244+ mock_send_command.assert_not_called()
1245+
1246+ def test_process_pjlink_normal_with_salt_error(self):
1247+ """
1248+ Test process_pjlinnk called with no authentication but pin is set
1249+ """
1250+ # GIVEN: Initial mocks and data
1251+ # GIVEN: Initial mocks and data
1252+ mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start()
1253+ mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start()
1254+ mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
1255+ pjlink = self.pjlink_test
1256+ pjlink.pin = TEST_PIN
1257+ log_check = [call('(111.111.111.111) Normal connection with extra information - aborting'), ]
1258+
1259+ # WHEN: process_pjlink called with invalid authentication scheme
1260+ pjlink.process_pjlink(data='0 {salt}'.format(salt=TEST_SALT))
1261+
1262+ # THEN: Proper calls should be made
1263+ mock_log.error.assert_has_calls(log_check)
1264+ self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once')
1265+ mock_send_command.assert_not_called()
1266+
1267+ def test_process_pjlink_invalid_authentication_scheme_length_error(self):
1268+ """
1269+ Test initial connection prompt with authentication scheme longer than 1 character
1270+ """
1271+ # GIVEN: Initial mocks and data
1272+ mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start()
1273+ mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start()
1274+ mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
1275+ pjlink = self.pjlink_test
1276+ log_check = [call('(111.111.111.111) Invalid initial authentication scheme - aborting'), ]
1277+
1278+ # WHEN: process_pjlink called with invalid authentication scheme
1279+ pjlink.process_pjlink(data='01')
1280+
1281+ # THEN: socket should be closed and invalid data logged
1282+ mock_log.error.assert_has_calls(log_check)
1283+ self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once')
1284+ mock_send_command.assert_not_called()
1285+
1286+ def test_process_pjlink_invalid_authentication_data_length_error(self):
1287+ """
1288+ Test initial connection prompt with authentication no salt
1289+ """
1290+ # GIVEN: Initial mocks and data
1291+ mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start()
1292+ mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start()
1293+ mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
1294+ log_check = [call('(111.111.111.111) Authenticated connection but not enough info - aborting'), ]
1295+ pjlink = self.pjlink_test
1296+
1297+ # WHEN: process_pjlink called with no salt
1298+ pjlink.process_pjlink(data='1')
1299+
1300+ # THEN: socket should be closed and invalid data logged
1301+ mock_log.error.assert_has_calls(log_check)
1302+ self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once')
1303+ mock_send_command.assert_not_called()
1304+
1305+ def test_process_pjlink_authenticate_pin_not_set_error(self):
1306+ """
1307+ Test process_pjlink authentication but pin not set
1308+ """
1309+ # GIVEN: Initial mocks and data
1310+ mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start()
1311+ mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start()
1312+ mock_send_command = patch.object(self.pjlink_test, 'send_command').start()
1313+ log_check = [call('(111.111.111.111) Authenticate connection but no PIN - aborting'), ]
1314+ pjlink = self.pjlink_test
1315+ pjlink.pin = None
1316+
1317+ # WHEN: process_pjlink called with no salt
1318+ pjlink.process_pjlink(data='1 {salt}'.format(salt=TEST_SALT))
1319+
1320+ # THEN: socket should be closed and invalid data logged
1321+ mock_log.error.assert_has_calls(log_check)
1322+ self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once')
1323+ mock_send_command.assert_not_called()