← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~alisonken1/openlp/pjlink2-t into lp:openlp

 

Ken Roberts has proposed merging lp:~alisonken1/openlp/pjlink2-t into lp:openlp.

Commit message:
PJLink2-T Update (UDP updates and some cleanups)

Having network issues - ci failed when running ci but passed when looked at branch webpage:
--------------------------------------------------------------------------------
lp:~alisonken1/openlp/pjlink2-t (revision 2834)
https://ci.openlp.io/job/Branch-01-Pull/2578/                          [SUCCESS]
https://ci.openlp.io/job/Branch-02a-Linux-Tests/2475/                  [WAITING]Traceback (most recent call last):
...
Exception: Build has not started yet, it may be stuck in the queue
===============

Jenkins branch webpage:
-------------------------------------------------------------------------------
Success	Branch-01-Pull (#2578)
Success	Branch-02a-Linux-Tests (#2475)
Failed	Branch-02b-macOS-Tests (#249)
Success	Branch-03a-Build-Source (#162)
Success	Branch-03b-Build-macOS (#141)
Success	Branch-04a-Code-Analysis (#1624)
Success	Branch-04b-Test-Coverage (#1437)
Success	Branch-04c-Lint-Check (#567)
Failed	Branch-05-AppVeyor-Tests (#336)
Failed	Branch-06-Build_API_Docs (#486)


Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~alisonken1/openlp/pjlink2-t/+merge/356677

- Move tests/functional/openlp_core/common/test_networkinterfaces.py to tests/openlp_core/common
- Minor cleanups in openlp.core.common.__init__.get_local_ip4()
- Add more get_local_ip4 tests
- Fix some oops in ProjectorManager when add/edit projector entry
- Update tests/openlp_core/projectors/test_projector_pjlink_udp.py with correct log entries
- Add UDP 'projector/udp broadcast listen' status in Settings() (default=False)
- Add UDP broadcast listen option in projector settings tab
- Add check_settings(), udp_start(), and udp_stop(), udp_add(), udp_delete() methods in PJLinkUDP
- Add Registry().register_function('udp_broadcast_listen') call for UDP settings update
- Add udp_listen_add(), udp_listen_delete() methods to ProjectorManager
- pep8 cleanup in tests/functional/openlp_core/common/test_i18n.py
- pep8 cleanup in openlp/core/lib/__init__.py

-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~alisonken1/openlp/pjlink2-t into lp:openlp.
=== modified file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py	2018-08-03 22:32:32 +0000
+++ openlp/core/common/__init__.py	2018-10-13 12:21:32 +0000
@@ -60,7 +60,6 @@
 
     :returns: Dict of interfaces
     """
-    # Get the local IPv4 active address(es) that are NOT localhost (lo or '127.0.0.1')
     log.debug('Getting local IPv4 interface(es) information')
     my_ip4 = {}
     for iface in QNetworkInterface.allInterfaces():
@@ -70,8 +69,6 @@
         log.debug('Checking address(es) protocol')
         for address in iface.addressEntries():
             ip = address.ip()
-            # NOTE: Next line will skip if interface is localhost - keep for now until we decide about it later
-            # if (ip.protocol() == QAbstractSocket.IPv4Protocol) and (ip != QHostAddress.LocalHost):
             log.debug('Checking for protocol == IPv4Protocol')
             if ip.protocol() == QAbstractSocket.IPv4Protocol:
                 log.debug('Getting interface information')
@@ -83,12 +80,13 @@
                                                                  ip.toIPv4Address()).toString()
                                         }
                 log.debug('Adding {iface} to active list'.format(iface=iface.name()))
+    if len(my_ip4) == 0:
+        log.warning('No active IPv4 network interfaces detected')
+        return my_ip4
     if 'localhost' in my_ip4:
         log.debug('Renaming windows localhost to lo')
         my_ip4['lo'] = my_ip4['localhost']
         my_ip4.pop('localhost')
-    if len(my_ip4) == 0:
-        log.warning('No active IPv4 network interfaces detected')
     if len(my_ip4) == 1:
         if 'lo' in my_ip4:
             # No active interfaces - so leave localhost in there

=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	2018-06-07 17:44:35 +0000
+++ openlp/core/common/settings.py	2018-10-13 12:21:32 +0000
@@ -217,7 +217,8 @@
         'projector/last directory export': None,
         'projector/poll time': 20,  # PJLink  timeout is 30 seconds
         'projector/socket timeout': 5,  # 5 second socket timeout
-        'projector/source dialog type': 0  # Source select dialog box type
+        'projector/source dialog type': 0,  # Source select dialog box type
+        'projector/udp broadcast listen': False  # Enable/disable listening for PJLink 2 UDP broadcast packets
     }
     __file_path__ = ''
     # Settings upgrades prior to 3.0

=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2018-08-25 14:08:19 +0000
+++ openlp/core/lib/__init__.py	2018-10-13 12:21:32 +0000
@@ -609,4 +609,4 @@
                                                                                    last=string_list[-1])
     else:
         list_to_string = ''
-    return list_to_string
\ No newline at end of file
+    return list_to_string

=== modified file 'openlp/core/projectors/editform.py'
--- openlp/core/projectors/editform.py	2018-04-21 19:57:51 +0000
+++ openlp/core/projectors/editform.py	2018-10-13 12:21:32 +0000
@@ -179,6 +179,7 @@
         Validate input before accepting input.
         """
         log.debug('accept_me() signal received')
+        valid = True
         if len(self.name_text.text().strip()) < 1:
             QtWidgets.QMessageBox.warning(self,
                                           translate('OpenLP.ProjectorEdit', 'Name Not Set'),

=== modified file 'openlp/core/projectors/manager.py'
--- openlp/core/projectors/manager.py	2018-06-28 20:40:54 +0000
+++ openlp/core/projectors/manager.py	2018-10-13 12:21:32 +0000
@@ -38,7 +38,7 @@
 from openlp.core.projectors import DialogSourceStyle
 from openlp.core.projectors.constants import E_AUTHENTICATION, E_ERROR, E_NETWORK, E_NOT_CONNECTED, \
     E_SOCKET_TIMEOUT, E_UNKNOWN_SOCKET_ERROR, S_CONNECTED, S_CONNECTING, S_COOLDOWN, S_INITIALIZE, \
-    S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP, PJLINK_PORT, STATUS_CODE, STATUS_MSG, QSOCKET_STATE
+    S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP, STATUS_CODE, STATUS_MSG, QSOCKET_STATE
 
 from openlp.core.projectors.db import ProjectorDB
 from openlp.core.projectors.editform import ProjectorEditForm
@@ -335,10 +335,6 @@
         """
         Post-initialize setups.
         """
-        # Default PJLink port UDP socket
-        log.debug('Creating PJLinkUDP listener for default port {port}'.format(port=PJLINK_PORT))
-        self.pjlink_udp = {PJLINK_PORT: PJLinkUDP(port=PJLINK_PORT)}
-        self.pjlink_udp[PJLINK_PORT].bind(PJLINK_PORT)
         # Set 1.5 second delay before loading all projectors
         if self.autostart:
             log.debug('Delaying 1.5 seconds before loading all projectors')
@@ -351,6 +347,36 @@
         self.projector_form.editProjector.connect(self.edit_projector_from_wizard)
         self.projector_list_widget.itemSelectionChanged.connect(self.update_icons)
 
+    def udp_listen_add(self, port):
+        """
+        Add UDP broadcast listener
+        """
+        if port in self.pjlink_udp:
+            log.warning('UDP Listener for port {port} already added - skipping'.format(port=port))
+        else:
+            log.debug('Adding UDP listener on port {port}'.format(port=port))
+            self.pjlink_udp[port] = PJLinkUDP(port=port)
+            self.pjlink_udp[port].udp_add()
+
+    def udp_listen_delete(self, port):
+        """
+        Remove a UDP broadcast listener
+        """
+        log.debug('Checking for UDP port {port} listener deletion'.format(port=port))
+        if port not in self.pjlink_udp:
+            log.warn('UDP listener for port {port} not there - skipping delete'.format(port=port))
+            return
+        keep_port = False
+        for item in self.projector_list:
+            if port == item.link.port:
+                keep_port = True
+        if keep_port:
+            log.warn('UDP listener for port {port} needed for other projectors - skipping delete'.format(port=port))
+            return
+        self.pjlink_udp[port].udp_delete()
+        del self.pjlink_udp[port]
+        log.debug('UDP listener for port {port} deleted'.format(port=port))
+
     def get_settings(self):
         """
         Retrieve the saved settings
@@ -518,25 +544,22 @@
         except (AttributeError, TypeError):
             pass
         try:
-            projector.poll_timer.stop()
-            projector.poll_timer.timeout.disconnect(projector.link.poll_loop)
-        except (AttributeError, TypeError):
-            pass
-        try:
-            projector.socket_timer.stop()
-            projector.socket_timer.timeout.disconnect(projector.link.socket_abort)
-        except (AttributeError, TypeError):
-            pass
-        # Disconnect signals from projector being deleted
-        try:
-            self.pjlink_udp[projector.link.port].data_received.disconnect(projector.link.get_buffer)
+            projector.link.poll_timer.stop()
+            projector.link.poll_timer.timeout.disconnect(projector.link.poll_loop)
+        except (AttributeError, TypeError):
+            pass
+        try:
+            projector.link.socket_timer.stop()
+            projector.link.socket_timer.timeout.disconnect(projector.link.socket_abort)
         except (AttributeError, TypeError):
             pass
 
+        old_port = projector.link.port
         # Rebuild projector list
         new_list = []
         for item in self.projector_list:
             if item.link.db_item.id == projector.link.db_item.id:
+                log.debug('Removing projector "{item}"'.format(item=item.link.name))
                 continue
             new_list.append(item)
         self.projector_list = new_list
@@ -546,6 +569,7 @@
             log.warning('Delete projector {item} failed'.format(item=projector.db_item))
         for item in self.projector_list:
             log.debug('New projector list - item: {ip} {name}'.format(ip=item.link.ip, name=item.link.name))
+        self.udp_listen_delete(old_port)
 
     def on_disconnect_projector(self, opt=None):
         """
@@ -748,15 +772,8 @@
         item.link.projectorAuthentication.connect(self.authentication_error)
         item.link.projectorNoAuthentication.connect(self.no_authentication_error)
         item.link.projectorUpdateIcons.connect(self.update_icons)
-        # Connect UDP signal to projector instances with same port
-        if item.link.port not in self.pjlink_udp:
-            log.debug('Adding new PJLinkUDP listener fo port {port}'.format(port=item.link.port))
-            self.pjlink_udp[item.link.port] = PJLinkUDP(port=item.link.port)
-            self.pjlink_udp[item.link.port].bind(item.link.port)
-        log.debug('Connecting PJLinkUDP port {port} signal to "{item}"'.format(port=item.link.port,
-                                                                               item=item.link.name))
-        self.pjlink_udp[item.link.port].data_received.connect(item.link.get_buffer)
-
+        # Add UDP listener for new projector port
+        self.udp_listen_add(item.link.port)
         self.projector_list.append(item)
         if start:
             item.link.connect_to_host()
@@ -783,13 +800,25 @@
         :param projector: Projector() instance of projector with updated information
         """
         log.debug('edit_projector_from_wizard(ip={ip})'.format(ip=projector.ip))
+        old_port = self.old_projector.link.port
+        old_ip = self.old_projector.link.ip
         self.old_projector.link.name = projector.name
         self.old_projector.link.ip = projector.ip
         self.old_projector.link.pin = None if projector.pin == '' else projector.pin
-        self.old_projector.link.port = projector.port
         self.old_projector.link.location = projector.location
         self.old_projector.link.notes = projector.notes
         self.old_projector.widget.setText(projector.name)
+        self.old_projector.link.port = int(projector.port)
+        # Update projector list items
+        for item in self.projector_list:
+            if item.link.ip == old_ip:
+                item.link.port = int(projector.port)
+                # NOTE: This assumes (!) we are using IP addresses as keys
+                break
+        # Update UDP listeners before setting old_projector.port
+        if old_port != projector.port:
+            self.udp_listen_delete(old_port)
+            self.udp_listen_add(int(projector.port))
 
     def _load_projectors(self):
         """'

=== modified file 'openlp/core/projectors/pjlink.py'
--- openlp/core/projectors/pjlink.py	2018-07-02 20:38:47 +0000
+++ openlp/core/projectors/pjlink.py	2018-10-13 12:21:32 +0000
@@ -54,6 +54,7 @@
 
 from openlp.core.common import qmd5_hash
 from openlp.core.common.i18n import translate
+from openlp.core.common.registry import Registry
 from openlp.core.common.settings import Settings
 from openlp.core.projectors.constants import CONNECTION_ERRORS, PJLINK_CLASS, PJLINK_DEFAULT_CODES, PJLINK_ERRORS, \
     PJLINK_ERST_DATA, PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PREFIX, PJLINK_PORT, PJLINK_POWR_STATUS, \
@@ -98,32 +99,65 @@
         self.search_active = False
         self.search_time = 30000  # 30 seconds for allowed time
         self.search_timer = QtCore.QTimer()
+        self.udp_broadcast_listen_setting = False
+        log.debug('(UDP:{port}) PJLinkUDP() Initialized'.format(port=self.port))
+
+    def udp_add(self):
+        """
+        Setup for new UDP listener
+        """
+        log.debug('(UDP:{port}) Adding Registry() entry'.format(port=self.port))
+        Registry().register_function('udp_broadcast_listen', self.check_settings)
+        self.udp_start()
+
+    def udp_delete(self):
+        """
+        Teardown for UDP listener
+        """
+        self.udp_stop()
+        log.debug('(UDP:{port}) Removing Registry() entry'.format(port=self.port))
+        Registry().remove_function('udp_broadcast_listen', self.check_settings)
+
+    def udp_start(self):
+        """
+        Start listening on UDP port
+        """
+        log.debug('(UDP:{port}) Start called'.format(port=self.port))
         self.readyRead.connect(self.get_datagram)
-        log.debug('(UDP) PJLinkUDP() Initialized for port {port}'.format(port=self.port))
+        self.check_settings(checked=Settings().value('projector/udp broadcast listen'))
+
+    def udp_stop(self):
+        """
+        Stop listening on UDP port
+        """
+        log.debug('(UDP:{port}) Stopping listener'.format(port=self.port))
+        self.close()
+        self.readyRead.disconnect(self.get_datagram)
 
     @QtCore.pyqtSlot()
     def get_datagram(self):
         """
         Retrieve packet and basic checks
         """
-        log.debug('(UDP) get_datagram() - Receiving data')
+        log.debug('(UDP:{port}) get_datagram() - Receiving data'.format(port=self.port))
         read_size = self.pendingDatagramSize()
         if -1 == read_size:
-            log.warning('(UDP) No data (-1)')
+            log.warning('(UDP:{port}) No data (-1)'.format(port=self.port))
             return
         elif 0 == read_size:
-            log.warning('(UDP) get_datagram() called when pending data size is 0')
+            log.warning('(UDP:{port}) get_datagram() called when pending data size is 0'.format(port=self.port))
             return
         elif read_size > PJLINK_MAX_PACKET:
-            log.warning('(UDP) UDP Packet too large ({size} bytes)- ignoring'.format(size=read_size))
+            log.warning('(UDP:{port}) UDP Packet too large ({size} bytes)- ignoring'.format(size=read_size,
+                                                                                            port=self.port))
             return
         data_in, peer_host, peer_port = self.readDatagram(read_size)
         data = data_in.decode('utf-8') if isinstance(data_in, bytes) else data_in
-        log.debug('(UDP) {size} bytes received from {adx} on port {port}'.format(size=len(data),
-                                                                                 adx=peer_host.toString(),
-                                                                                 port=self.port))
-        log.debug('(UDP) packet "{data}"'.format(data=data))
-        log.debug('(UDP) Sending data_received signal to projectors')
+        log.debug('(UDP:{port}) {size} bytes received from {adx}'.format(size=len(data),
+                                                                         adx=peer_host.toString(),
+                                                                         port=self.port))
+        log.debug('(UDP:{port}) packet "{data}"'.format(data=data, port=self.port))
+        log.debug('(UDP:{port}) Sending data_received signal to projectors'.format(port=self.port))
         self.data_received.emit(peer_host, self.localPort(), data)
         return
 
@@ -143,6 +177,24 @@
         self.search_active = False
         self.search_timer.stop()
 
+    def check_settings(self, checked):
+        """
+        Update UDP listening state based on settings change
+        """
+        if self.udp_broadcast_listen_setting == checked:
+            log.debug('(UDP:{port}) No change to status - skipping'.format(port=self.port))
+            return
+        self.udp_broadcast_listen_setting = checked
+        if self.udp_broadcast_listen_setting:
+            if self.state() == self.ListeningState:
+                log.debug('(UDP:{port}) Already listening - skipping')
+                return
+            self.bind(self.port)
+            log.debug('(UDP:{port}) Listening'.format(port=self.port))
+        else:
+            # Close socket
+            self.udp_stop()
+
 
 class PJLinkCommands(object):
     """

=== modified file 'openlp/core/projectors/tab.py'
--- openlp/core/projectors/tab.py	2018-08-25 14:08:19 +0000
+++ openlp/core/projectors/tab.py	2018-10-13 12:21:32 +0000
@@ -27,6 +27,7 @@
 from PyQt5 import QtWidgets
 
 from openlp.core.common.i18n import UiStrings, translate
+from openlp.core.common.registry import Registry
 from openlp.core.common.settings import Settings
 from openlp.core.lib.settingstab import SettingsTab
 from openlp.core.ui.icons import UiIcons
@@ -49,6 +50,8 @@
         self.icon_path = UiIcons().projector
         projector_translated = translate('OpenLP.ProjectorTab', 'Projector')
         super(ProjectorTab, self).__init__(parent, 'Projector', projector_translated)
+        # So we don't get tracebacks in the log when there are no UDP listeners registered
+        Registry().register_function('udp_broadcast_listen', self.ping)
 
     def setupUi(self):
         """
@@ -90,6 +93,10 @@
         self.connect_box_layout.addRow(self.dialog_type_label, self.dialog_type_combo_box)
         self.left_layout.addStretch()
         self.dialog_type_combo_box.activated.connect(self.on_dialog_type_combo_box_changed)
+        # Enable/disable listening on UDP ports for PJLink2 broadcasts
+        self.udp_broadcast_listen = QtWidgets.QCheckBox(self.connect_box)
+        self.udp_broadcast_listen.setObjectName('udp_broadcast_listen')
+        self.connect_box_layout.addRow(self.udp_broadcast_listen)
         # Connect on LKUP packet received (PJLink v2+ only)
         self.connect_on_linkup = QtWidgets.QCheckBox(self.connect_box)
         self.connect_on_linkup.setObjectName('connect_on_linkup')
@@ -116,6 +123,8 @@
                                                translate('OpenLP.ProjectorTab', 'Single dialog box'))
         self.connect_on_linkup.setText(
             translate('OpenLP.ProjectorTab', 'Connect to projector when LINKUP received (v2 only)'))
+        self.udp_broadcast_listen.setText(
+            translate('OpenLP.ProjectorTab', 'Enable listening for PJLink2 broadcast messages'))
 
     def load(self):
         """
@@ -125,6 +134,7 @@
         self.socket_timeout_spin_box.setValue(Settings().value('projector/socket timeout'))
         self.socket_poll_spin_box.setValue(Settings().value('projector/poll time'))
         self.dialog_type_combo_box.setCurrentIndex(Settings().value('projector/source dialog type'))
+        self.udp_broadcast_listen.setChecked(Settings().value('projector/udp broadcast listen'))
         self.connect_on_linkup.setChecked(Settings().value('projector/connect when LKUP received'))
 
     def save(self):
@@ -136,6 +146,14 @@
         Settings().setValue('projector/poll time', self.socket_poll_spin_box.value())
         Settings().setValue('projector/source dialog type', self.dialog_type_combo_box.currentIndex())
         Settings().setValue('projector/connect when LKUP received', self.connect_on_linkup.isChecked())
+        Settings().setValue('projector/udp broadcast listen', self.udp_broadcast_listen.isChecked())
+        Registry().execute('udp_broadcast_listen', self.udp_broadcast_listen.isChecked())
 
     def on_dialog_type_combo_box_changed(self):
         self.dialog_type = self.dialog_type_combo_box.currentIndex()
+
+    def ping(self, checked):
+        """
+        Placeholder function so we don't get tracebacks when there are no UDP listeners
+        """
+        log.debug('UDP setting change callback - checked={chk}'.format(chk=checked))

=== modified file 'tests/functional/openlp_core/common/test_i18n.py'
--- tests/functional/openlp_core/common/test_i18n.py	2018-08-27 14:16:26 +0000
+++ tests/functional/openlp_core/common/test_i18n.py	2018-10-13 12:21:32 +0000
@@ -162,6 +162,7 @@
 def test_get_language_from_settings():
     assert LanguageManager.get_language() == 'en'
 
+
 def test_get_language_from_settings_returns_unchanged_if_unknown_format():
     Settings().setValue('core/language', '(foobar)')
     assert LanguageManager.get_language() == '(foobar)'

=== added directory 'tests/openlp_core/common'
=== renamed file 'tests/functional/openlp_core/common/test_network_interfaces.py' => 'tests/openlp_core/common/test_network_interfaces.py'
--- tests/functional/openlp_core/common/test_network_interfaces.py	2018-08-03 22:32:32 +0000
+++ tests/openlp_core/common/test_network_interfaces.py	2018-10-13 12:21:32 +0000
@@ -23,21 +23,61 @@
 Functional tests to test calls for network interfaces.
 """
 from unittest import TestCase
-from unittest.mock import MagicMock, call, patch
+from unittest.mock import call, patch
+from PyQt5.QtCore import QObject
+from PyQt5.QtNetwork import QHostAddress, QNetworkAddressEntry, QNetworkInterface
 
 import openlp.core.common
 from openlp.core.common import get_local_ip4
 
 from tests.helpers.testmixin import TestMixin
 
-lo_address_attrs = {'isValid.return_value': True,
-                    'flags.return_value': True,
-                    'InterfaceFlags.return_value': True,
-                    'name.return_value': 'lo',
-                    'broadcast.toString.return_value': '127.0.0.255',
-                    'netmask.toString.return_value': '255.0.0.0',
-                    'prefixLength.return_value': 8,
-                    'ip.protocol.return_value': True}
+
+class FakeIP4InterfaceEntry(QObject):
+    """
+    Class to face an interface for testing purposes
+    """
+    def __init__(self, name='lo'):
+        self.my_name = name
+        if name in ['localhost', 'lo']:
+            self.my_ip = QNetworkAddressEntry()
+            self.my_ip.setBroadcast(QHostAddress('255.0.0.0'))
+            self.my_ip.setIp(QHostAddress('127.0.0.2'))
+            self.my_ip.setPrefixLength(8)
+            self.fake_data = {'lo': {'ip': '127.0.0.2',
+                                     'broadcast': '255.0.0.0',
+                                     'netmask': '255.0.0.0',
+                                     'prefix': 8,
+                                     'localnet': '127.0.0.0'}}
+        else:
+            # Define a fake real address
+            self.my_ip = QNetworkAddressEntry()
+            self.my_ip.setBroadcast(QHostAddress('255.255.255.0'))
+            self.my_ip.setIp(QHostAddress('127.254.0.2'))
+            self.my_ip.setPrefixLength(24)
+            self.fake_data = {self.my_name: {'ip': '127.254.0.2',
+                                             'broadcast': '255.255.255.0',
+                                             'netmask': '255.255.255.0',
+                                             'prefix': 24,
+                                             'localnet': '127.254.0.0'}}
+
+    def addressEntries(self):
+        """
+        Return fake IP address
+        """
+        return [self.my_ip]
+
+    def flags(self):
+        """
+        Return a QFlags enum with IsUp and IsRunning
+        """
+        return (QNetworkInterface.IsUp | QNetworkInterface.IsRunning)
+
+    def name(self):
+        return self.my_name
+
+    def isValid(self):
+        return True
 
 
 class TestInterfaces(TestCase, TestMixin):
@@ -49,9 +89,11 @@
         Create an instance and a few example actions.
         """
         self.build_settings()
-
-        self.ip4_lo_address = MagicMock()
-        self.ip4_lo_address.configure_mock(**lo_address_attrs)
+        if not hasattr(self, 'fake_lo'):
+            # Since these shouldn't change, only need to instantiate them the first time
+            self.fake_lo = FakeIP4InterfaceEntry()
+            self.fake_localhost = FakeIP4InterfaceEntry(name='localhost')
+            self.fake_address = FakeIP4InterfaceEntry(name='eth25')
 
     def tearDown(self):
         """
@@ -65,14 +107,118 @@
         Test no interfaces available
         """
         # GIVEN: Test environment
+        call_debug = [call('Getting local IPv4 interface(es) information')]
         call_warning = [call('No active IPv4 network interfaces detected')]
 
-        with patch('openlp.core.common.QNetworkInterface') as mock_newtork_interface:
-            mock_newtork_interface.allInterfaces.return_value = []
-
-            # WHEN: get_local_ip4 is called
-            ifaces = get_local_ip4()
-
-            # THEN: There should not be any interfaces detected
-            assert not ifaces, 'There should have been no active interfaces'
-            mock_log.warning.assert_has_calls(call_warning)
+        # WHEN: get_local_ip4 is called
+        with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
+            mock_network_interface.allInterfaces.return_value = []
+            ifaces = get_local_ip4()
+
+        # THEN: There should not be any interfaces detected
+        mock_log.debug.assert_has_calls(call_debug)
+        mock_log.warning.assert_has_calls(call_warning)
+        assert not ifaces, 'There should have been no active interfaces listed'
+
+    @patch.object(openlp.core.common, 'log')
+    def test_ip4_lo(self, mock_log):
+        """
+        Test get_local_ip4 returns proper dictionary with 'lo'
+        """
+        # GIVEN: Test environment
+        call_debug = [call('Getting local IPv4 interface(es) information'),
+                      call('Checking for isValid and flags == IsUP | IsRunning'),
+                      call('Checking address(es) protocol'),
+                      call('Checking for protocol == IPv4Protocol'),
+                      call('Getting interface information'),
+                      call('Adding lo to active list')]
+        call_warning = [call('No active IPv4 interfaces found except localhost')]
+
+        # WHEN: get_local_ip4 is called
+        with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
+            mock_network_interface.allInterfaces.return_value = [self.fake_lo]
+            ifaces = get_local_ip4()
+
+        # THEN: There should be a fake 'lo' interface
+        mock_log.debug.assert_has_calls(call_debug)
+        mock_log.warning.assert_has_calls(call_warning)
+        assert ifaces == self.fake_lo.fake_data, "There should have been an 'lo' interface listed"
+
+    @patch.object(openlp.core.common, 'log')
+    def test_ip4_localhost(self, mock_log):
+        """
+        Test get_local_ip4 returns proper dictionary with 'lo' if interface is 'localhost'
+        """
+        # GIVEN: Test environment
+        call_debug = [call('Getting local IPv4 interface(es) information'),
+                      call('Checking for isValid and flags == IsUP | IsRunning'),
+                      call('Checking address(es) protocol'),
+                      call('Checking for protocol == IPv4Protocol'),
+                      call('Getting interface information'),
+                      call('Adding localhost to active list'),
+                      call('Renaming windows localhost to lo')]
+        call_warning = [call('No active IPv4 interfaces found except localhost')]
+
+        # WHEN: get_local_ip4 is called
+        with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
+            mock_network_interface.allInterfaces.return_value = [self.fake_localhost]
+            ifaces = get_local_ip4()
+
+        # THEN: There should be a fake 'lo' interface
+        mock_log.debug.assert_has_calls(call_debug)
+        mock_log.warning.assert_has_calls(call_warning)
+        assert ifaces == self.fake_lo.fake_data, "There should have been an 'lo' interface listed"
+
+    @patch.object(openlp.core.common, 'log')
+    def test_ip4_eth25(self, mock_log):
+        """
+        Test get_local_ip4 returns proper dictionary with 'eth25'
+        """
+        # GIVEN: Test environment
+        call_debug = [call('Getting local IPv4 interface(es) information'),
+                      call('Checking for isValid and flags == IsUP | IsRunning'),
+                      call('Checking address(es) protocol'),
+                      call('Checking for protocol == IPv4Protocol'),
+                      call('Getting interface information'),
+                      call('Adding eth25 to active list')]
+        call_warning = []
+
+        # WHEN: get_local_ip4 is called
+        with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
+            mock_network_interface.allInterfaces.return_value = [self.fake_address]
+            ifaces = get_local_ip4()
+
+        # THEN: There should be a fake 'eth25' interface
+        mock_log.debug.assert_has_calls(call_debug)
+        mock_log.warning.assert_has_calls(call_warning)
+        assert ifaces == self.fake_address.fake_data
+
+    @patch.object(openlp.core.common, 'log')
+    def test_ip4_lo_eth25(self, mock_log):
+        """
+        Test get_local_ip4 returns proper dictionary with 'eth25'
+        """
+        # GIVEN: Test environment
+        call_debug = [call('Getting local IPv4 interface(es) information'),
+                      call('Checking for isValid and flags == IsUP | IsRunning'),
+                      call('Checking address(es) protocol'),
+                      call('Checking for protocol == IPv4Protocol'),
+                      call('Getting interface information'),
+                      call('Adding lo to active list'),
+                      call('Checking for isValid and flags == IsUP | IsRunning'),
+                      call('Checking address(es) protocol'),
+                      call('Checking for protocol == IPv4Protocol'),
+                      call('Getting interface information'),
+                      call('Adding eth25 to active list'),
+                      call('Found at least one IPv4 interface, removing localhost')]
+        call_warning = []
+
+        # WHEN: get_local_ip4 is called
+        with patch('openlp.core.common.QNetworkInterface') as mock_network_interface:
+            mock_network_interface.allInterfaces.return_value = [self.fake_lo, self.fake_address]
+            ifaces = get_local_ip4()
+
+        # THEN: There should be a fake 'eth25' interface
+        mock_log.debug.assert_has_calls(call_debug)
+        mock_log.warning.assert_has_calls(call_warning)
+        assert ifaces == self.fake_address.fake_data, "There should have been only 'eth25' interface listed"

=== modified file 'tests/openlp_core/projectors/__init__.py'
--- tests/openlp_core/projectors/__init__.py	2018-02-11 11:42:13 +0000
+++ tests/openlp_core/projectors/__init__.py	2018-10-13 12:21:32 +0000
@@ -4,7 +4,7 @@
 ###############################################################################
 # OpenLP - Open Source Lyrics Projection                                      #
 # --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2017 OpenLP Developers                                   #
+# Copyright (c) 2008-2018 OpenLP Developers                                   #
 # --------------------------------------------------------------------------- #
 # This program is free software; you can redistribute it and/or modify it     #
 # under the terms of the GNU General Public License as published by the Free  #
@@ -20,5 +20,5 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 """
-Module-level functions for the projector test suite
+:mod tests/openlp_core/projectors: Tests for projector code
 """

=== modified file 'tests/openlp_core/projectors/test_projector_pjlink_udp.py'
--- tests/openlp_core/projectors/test_projector_pjlink_udp.py	2018-05-03 14:58:50 +0000
+++ tests/openlp_core/projectors/test_projector_pjlink_udp.py	2018-10-13 12:21:32 +0000
@@ -45,9 +45,9 @@
         """
         # GIVEN: Test setup
         pjlink_udp = PJLinkUDP()
-        log_warning_calls = [call('(UDP) No data (-1)')]
-        log_debug_calls = [call('(UDP) PJLinkUDP() Initialized for port 4352'),
-                           call('(UDP) get_datagram() - Receiving data')]
+        log_warning_calls = [call('(UDP:4352) No data (-1)')]
+        log_debug_calls = [call('(UDP:4352) PJLinkUDP() Initialized'),
+                           call('(UDP:4352) get_datagram() - Receiving data')]
         with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
                 patch.object(pjlink_udp, 'readDatagram') as mock_read:
             mock_datagram.return_value = -1
@@ -67,9 +67,9 @@
         """
         # GIVEN: Test setup
         pjlink_udp = PJLinkUDP()
-        log_warning_calls = [call('(UDP) get_datagram() called when pending data size is 0')]
-        log_debug_calls = [call('(UDP) PJLinkUDP() Initialized for port 4352'),
-                           call('(UDP) get_datagram() - Receiving data')]
+        log_warning_calls = [call('(UDP:4352) get_datagram() called when pending data size is 0')]
+        log_debug_calls = [call('(UDP:4352) PJLinkUDP() Initialized'),
+                           call('(UDP:4352) get_datagram() - Receiving data')]
         with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
                 patch.object(pjlink_udp, 'readDatagram') as mock_read:
             mock_datagram.return_value = 0
@@ -89,9 +89,9 @@
         """
         # GIVEN: Test setup
         pjlink_udp = PJLinkUDP()
-        log_warning_calls = [call('(UDP) get_datagram() called when pending data size is 0')]
-        log_debug_calls = [call('(UDP) PJLinkUDP() Initialized for port 4352'),
-                           call('(UDP) get_datagram() - Receiving data')]
+        log_warning_calls = [call('(UDP:4352) get_datagram() called when pending data size is 0')]
+        log_debug_calls = [call('(UDP:4352) PJLinkUDP() Initialized'),
+                           call('(UDP:4352) get_datagram() - Receiving data')]
         with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram:
             mock_datagram.return_value = 0
 


Follow ups