def test_broadcast_message(faker): for i in range(1000): # Create Messages with random `screen` value in the non-broadcast range # and ensure the `is_broadcast` field is correct in both the # instance and its parsed version screen = faker.pyint(max_value=0xfffe) msgobj = Message(screen=screen) msgobj.displays.append(Display(index=i)) assert not msgobj.is_broadcast parsed, remaining = Message.parse(msgobj.build_message()) assert not msgobj.is_broadcast # Create broadcast Messages using both methods and check the `is_broadcast` # field on the instances and their parsed versions msgobj1 = Message(screen=0xffff) msgobj1.displays.append(Display(index=1)) assert msgobj1.is_broadcast parsed1, remaining = Message.parse(msgobj1.build_message()) assert parsed1.is_broadcast msgobj2 = Message.broadcast(displays=[Display(index=1)]) assert msgobj2.is_broadcast parsed2, remaining = Message.parse(msgobj2.build_message()) assert parsed2.is_broadcast assert msgobj1 == msgobj2 == parsed1 == parsed2
def test_invalid_dmsg(uhs500_msg_bytes, faker): # Clip the dmsg header fields bad_bytes = bytearray(uhs500_msg_bytes[:8]) # Insert the correct value for `PBC` field so it gets past initial checks bad_byte_count = struct.pack('<H', len(bad_bytes) - 2) bad_bytes[:2] = bad_byte_count bad_bytes = bytes(bad_bytes) with pytest.raises(DmsgParseError) as excinfo: r = Message.parse(bad_bytes) assert 'dmsg length' in str(excinfo.value).lower() # Clip the display text length field to the wrong size bad_bytes = bytearray(uhs500_msg_bytes[:10]) # Insert the correct value for `PBC` field so it gets past initial checks bad_byte_count = struct.pack('<H', len(bad_bytes) - 2) bad_bytes[:2] = bad_byte_count bad_bytes = bytes(bad_bytes) with pytest.raises(DmsgParseError) as excinfo: r = Message.parse(bad_bytes) assert 'text length' in str(excinfo.value).lower() # Insert an incorrect value for the text length field bad_bytes = bytearray(uhs500_msg_bytes) txt_len_bytes = struct.pack('<H', len(uhs500_msg_bytes) + 10) bad_bytes[10:12] = txt_len_bytes bad_bytes = bytes(bad_bytes) with pytest.raises(DmsgParseError) as excinfo: r = Message.parse(bad_bytes) assert 'invalid text bytes' in str(excinfo.value).lower()
async def test_broadcast_display(uhs500_msg_bytes, uhs500_msg_parsed, udp_endpoint, udp_port, faker): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) displays_by_index = { disp.index: disp for disp in uhs500_msg_parsed.displays } async with receiver: # Populate the receiver's tallies and wait for them to be added transport.sendto(uhs500_msg_bytes, ('127.0.0.1', udp_port)) _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() assert len(receiver.tallies) == len(uhs500_msg_parsed.displays) receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) # Send a broadcast display message for each TallyColor with a random brightness # The control field is set so the Tally text field is unchanged for color in TallyColor: brightness = faker.pyint(max_value=3) bc_disp = Display.broadcast( control=b'foo', rh_tally=color, lh_tally=color, txt_tally=color, brightness=brightness, ) msg = Message(displays=[bc_disp]) transport.sendto(msg.build_message(), ('127.0.0.1', udp_port)) _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() # Check each of the receiver's tallies against the bc_disp values # and make sure the text didn't change for tally in receiver.tallies.values(): assert tally.index < 0xffff assert not tally.is_broadcast assert tally.rh_tally == color assert tally.txt_tally == color assert tally.lh_tally == color assert tally.control == bc_disp.control assert tally.brightness == brightness assert tally.text == displays_by_index[tally.index].text
def test_invalid_message(uhs500_msg_bytes, faker): bad_bytes = faker.binary(length=5) with pytest.raises(MessageParseError) as excinfo: r = Message.parse(bad_bytes) assert 'header' in str(excinfo.value).lower() bad_bytes = bytearray(uhs500_msg_bytes) bad_byte_count = struct.pack('<H', len(uhs500_msg_bytes) + 10) bad_bytes[:2] = bad_byte_count bad_bytes = bytes(bad_bytes) with pytest.raises(MessageParseError) as excinfo: r = Message.parse(bad_bytes) assert 'byte count' in str(excinfo.value).lower()
def test_dmsg_control(uhs500_msg_parsed, faker): tested_zero = False for _ in range(10): msgobj = Message(version=1, screen=5) for orig_disp in uhs500_msg_parsed.displays: if not tested_zero: data_len = 0 tested_zero = True else: data_len = faker.pyint(min_value=0, max_value=1024) control_data = faker.binary(length=data_len) kw = orig_disp.to_dict() del kw['text'] kw['control'] = control_data if not len(control_data): kw['type'] = MessageType.control disp = Display(**kw) assert disp.type == MessageType.control disp_bytes = disp.to_dmsg(msgobj.flags) parsed_disp, remaining = Display.from_dmsg(msgobj.flags, disp_bytes) assert not len(remaining) assert parsed_disp.control == control_data assert parsed_disp == disp msgobj.displays.append(disp) parsed = None for msg_bytes in msgobj.build_messages(): _parsed, remaining = Message.parse(msg_bytes) assert not len(remaining) if parsed is None: parsed = _parsed else: parsed.displays.extend(_parsed.displays) assert parsed == msgobj with pytest.raises(ValueError) as excinfo: disp = Display(index=1, control=b'foo', text='foo') excstr = str(excinfo.value).lower() assert 'control' in excstr and 'text' in excstr with pytest.raises(ValueError) as excinfo: disp = Display(index=1, text='foo', type=MessageType.control) excstr = str(excinfo.value).lower() assert 'control' in excstr and 'text' in excstr
def test_broadcast_display(uhs500_msg_parsed, faker): disp_attrs = ('rh_tally', 'txt_tally', 'lh_tally', 'text', 'brightness') msgobj = Message() for uhs_disp in uhs500_msg_parsed.displays: assert not uhs_disp.is_broadcast # General kwargs excluding the `index` kw = {attr: getattr(uhs_disp, attr) for attr in disp_attrs} # Create random Displays within non-broadcast range and check the # `is_broadcast` field of the instance and its parsed version for _ in range(1000): ix = faker.pyint(max_value=0xfffe) disp = Display(index=ix, **kw) assert not disp.is_broadcast parsed, remaining = Display.from_dmsg(msgobj.flags, disp.to_dmsg(msgobj.flags)) assert not parsed.is_broadcast assert parsed == disp # Create broadcast Displays using both methods and check the # `is_broadcast` field on the instances and their parsed versions bc_disp1 = Display.broadcast(**kw) bc_disp2 = Display(index=0xffff, **kw) assert bc_disp1.is_broadcast assert bc_disp2.is_broadcast parsed1, remaining = Display.from_dmsg(msgobj.flags, bc_disp1.to_dmsg(msgobj.flags)) assert parsed1.is_broadcast parsed2, remaining = Display.from_dmsg(msgobj.flags, bc_disp2.to_dmsg(msgobj.flags)) assert parsed2.is_broadcast assert bc_disp1 == bc_disp2 == parsed1 == parsed2 # Add the broadcast Display to the Message at the top msgobj.displays.append(bc_disp1) # Check the `is_broadcast` field in the displays after Message building / parsing parsed, remaining = Message.parse(msgobj.build_message()) for parsed_disp, bc_disp in zip(parsed.displays, msgobj.displays): assert parsed_disp.is_broadcast assert parsed_disp == bc_disp
async def test_parse_errors(faker, udp_endpoint, udp_port): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) msgobj = Message() disp = Display(index=0, text='foo') msgobj.displays.append(disp) async with receiver: transport.sendto(msgobj.build_message(), ('127.0.0.1', udp_port)) _ = await evt_listener.get() receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) screen = receiver.screens[msgobj.screen] rx_disp = screen[disp.index] assert rx_disp is receiver.tallies[rx_disp.id] assert rx_disp == disp for i in range(100): num_bytes = faker.pyint(min_value=1, max_value=1024) bad_bytes = faker.binary(length=num_bytes) with pytest.raises(ParseError): receiver.parse_incoming(bad_bytes, ('127.0.0.1', endpoint_port)) transport.sendto(bad_bytes, ('127.0.0.1', udp_port)) disp.text = f'foo_{i}' transport.sendto(msgobj.build_message(), ('127.0.0.1', udp_port)) _ = await evt_listener.get() assert rx_disp == disp
def message_with_lots_of_displays(): msgobj = Message() # Message header byte length: 6 # Dmsg header byte length: 4 # Text length: 2(length bytes) + 9(chars) = 11 # Each Dmsg total: 4 + 11 = 15 # Dmsg's: 4096 * 15 = 61440 # 4096 Dmsg's with Message header: 4096 * 15 + 6 = 61446 bytes for i in range(4096): msgobj.displays.append(Display(index=i, text=f'Foo {i:05d}')) return msgobj
def test_messages(): msgobj = Message(version=1, screen=5) rh_tallies = [ getattr(TallyColor, attr) for attr in ['RED', 'OFF', 'GREEN', 'AMBER'] ] lh_tallies = [ getattr(TallyColor, attr) for attr in ['GREEN', 'RED', 'OFF', 'RED'] ] txt_tallies = [ getattr(TallyColor, attr) for attr in ['OFF', 'GREEN', 'AMBER', 'GREEN'] ] txts = ['foo', 'bar', 'baz', 'blah'] indices = [4, 3, 7, 1] for i in range(4): disp = Display( index=indices[i], rh_tally=rh_tallies[i], lh_tally=lh_tallies[i], txt_tally=txt_tallies[i], text=txts[i], brightness=i, ) msgobj.displays.append(disp) parsed, remaining = Message.parse(msgobj.build_message()) assert not len(remaining) for i in range(len(rh_tallies)): disp1, disp2 = msgobj.displays[i], parsed.displays[i] assert disp1.rh_tally == disp2.rh_tally == rh_tallies[i] assert disp1.lh_tally == disp2.lh_tally == lh_tallies[i] assert disp1.txt_tally == disp2.txt_tally == txt_tallies[i] assert disp1.text == disp2.text == txts[i] assert disp1.index == disp2.index == indices[i] assert disp1 == disp2 for attr in ['version', 'flags', 'screen', 'scontrol']: assert getattr(msgobj, attr) == getattr(parsed, attr) assert msgobj == parsed
def uhs500_msg_parsed() -> Message: """Expected :class:`~tslumd.messages.Message` object matching data from :func:`uhs500_msg_bytes` """ data = json.loads(MESSAGE_JSON.read_text()) data['scontrol'] = b'' displays = [] for disp in data['displays']: for key in ['rh_tally', 'txt_tally', 'lh_tally']: disp[key] = getattr(TallyColor, disp[key]) displays.append(Display(**disp)) data['displays'] = displays return Message(**data)
def test_scontrol(faker): for _ in range(100): data_len = faker.pyint(min_value=1, max_value=1024) control_data = faker.binary(length=data_len) msgobj = Message(scontrol=control_data) assert msgobj.type == MessageType.control assert Flags.SCONTROL in msgobj.flags msg_bytes = msgobj.build_message() parsed, remaining = Message.parse(msg_bytes) assert not len(remaining) assert parsed.type == MessageType.control assert parsed.scontrol == control_data assert parsed == msgobj disp = Display(index=1) with pytest.raises(ValueError) as excinfo: disp_msg = Message(displays=[disp], scontrol=control_data) assert 'SCONTROL' in str(excinfo.value)
def test_invalid_dmsg_control(uhs500_msg_bytes, faker): msg = Message() disp = Display(index=1, control=b'foo\x00') msg.displays.append(disp) msg_bytes = msg.build_message() # Clip the length field to the wrong size bad_bytes = bytearray(msg_bytes) bad_bytes = bad_bytes[:-5] bad_byte_count = struct.pack('<H', len(bad_bytes) - 2) bad_bytes[:2] = bad_byte_count bad_bytes = bytes(bad_bytes) with pytest.raises(DmsgControlParseError): r = Message.parse(bad_bytes) # Clip the control bytes to the wrong length bad_bytes = bytearray(msg_bytes) bad_bytes = bad_bytes[:-2] bad_byte_count = struct.pack('<H', len(bad_bytes) - 2) bad_bytes[:2] = bad_byte_count bad_bytes = bytes(bad_bytes) with pytest.raises(DmsgControlParseError): r = Message.parse(bad_bytes)
async def test_scontrol(faker, udp_endpoint, udp_port): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_scontrol=evt_listener.callback) async with receiver: for i in range(100): data_len = faker.pyint(min_value=1, max_value=1024) control_data = faker.binary(length=data_len) msgobj = Message(screen=i, scontrol=control_data) transport.sendto(msgobj.build_message(), ('127.0.0.1', udp_port)) args, kwargs = await evt_listener.get() screen, rx_data = args assert screen.index == i assert rx_data == control_data
async def test_queued_updates_are_separate_messages(udp_endpoint, udp_port): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() sender = UmdSender(clients=[('127.0.0.1', endpoint_port)]) screens = {} async with sender: # Create 10 screens with 10 tallies each # and trigger an update by `set_tally_text`. # # Don't await within the loop so the sender.update_queue gets filled up for screen_index in range(10): screen = Screen(screen_index) screens[screen_index] = screen for tally_index in range(10): t_id = (screen_index, tally_index) txt = f'Tally-{t_id}' sender.set_tally_text(t_id, txt) # Now give the `sender.tx_loop` a chance to process the queue await asyncio.sleep(.1) # Check each message to make sure they only have a single screen's data. assert not protocol.queue.empty() while not protocol.queue.empty(): data, addr = await protocol.queue.get() protocol.queue.task_done() parsed, _ = Message.parse(data) print(f'screen {parsed.screen} disp_len={len(parsed.displays)}') # print(parsed) screen = screens[parsed.screen] screen.update_from_message(parsed) # Ensure nothing got packed incorrectly by the unique tally.id in the text field for screen in screens.values(): for tally in screen: assert tally.text == f'Tally-{tally.id}' assert len(screen.tallies) == 10
def parse_incoming(self, data: bytes, addr: Tuple[str, int]): """Parse data received by the server """ while True: message, remaining = Message.parse(data) if message.screen not in self.screens: screen = Screen(message.screen) self.screens[screen.index] = screen self._bind_screen(screen) self.emit('on_screen_added', screen) logger.debug(f'new screen: {screen.index}') else: screen = self.screens[message.screen] if message.is_broadcast: for screen in self.screens.values(): screen.update_from_message(message) else: screen.update_from_message(message) if not len(remaining): break
def test_packet_length(faker, message_with_lots_of_displays): msgobj = message_with_lots_of_displays # Make sure the 2048 byte limit is respected with pytest.raises(MessageLengthError): _ = msgobj.build_message() # Ensure that the limit can be bypassed msg_bytes = msgobj.build_message(ignore_packet_length=True) parsed, remaining = Message.parse(msg_bytes) assert parsed == msgobj # Iterate over individual message packets and make sure we get all displays all_parsed_displays = [] for msg_bytes in msgobj.build_messages(): assert len(msg_bytes) <= 2048 parsed, remaining = Message.parse(msg_bytes) assert not len(remaining) all_parsed_displays.extend(parsed.displays) assert len(all_parsed_displays) == len(msgobj.displays) for disp, parsed_disp in zip(msgobj.displays, all_parsed_displays): assert disp.index == parsed_disp.index assert disp.text == parsed_disp.text # Create an SCONTROL that exceeds the limit msgobj = Message(scontrol=faker.binary(length=2048)) with pytest.raises(MessageLengthError): it = msgobj.build_messages() _ = next(it) # Create a Dmsg control that exceeds the limit msgobj = Message( displays=[Display(index=0, control=faker.binary(length=2048))]) with pytest.raises(MessageLengthError): it = msgobj.build_messages() _ = next(it)
def _build_message(self, **kwargs) -> Message: return Message(**kwargs)
async def send_message(self, msg: Message): for data in msg.build_messages(): for client in self.clients: self.transport.sendto(data, client)
async def test_broadcast_screen_updates(udp_endpoint, udp_port): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() sender = UmdSender(clients=[('127.0.0.1', endpoint_port)]) screens = {} bc_screen = Screen.broadcast() async with sender: # Create 10 screens with 10 tallies each and set their initial values to # `text='Tally-{tally.id}', rh_tally=TallyColor.RED` for screen_index in range(10): screen = Screen(screen_index) screens[screen_index] = screen for tally_index in range(10): t_id = (screen_index, tally_index) txt = f'Tally-{t_id}' sender.set_tally_text(t_id, txt) tx_tally = sender.tallies[t_id] # Wait for data from the sender and parse it manually into the screen data, addr = await protocol.queue.get() protocol.queue.task_done() parsed, _ = Message.parse(data) assert parsed.screen == screen_index screen.update_from_message(parsed) assert tally_index in screen tally = screen[tally_index] assert tally.text == txt # For each screen, send a broadcast tally setting `rh_tally` to `RED` for screen_index, screen in screens.items(): await sender.send_broadcast_tally(screen_index, rh_tally=TallyColor.RED) # Wait for data and parse it again into the screen data, addr = await protocol.queue.get() protocol.queue.task_done() parsed, _ = Message.parse(data) assert parsed.screen == screen_index screen.update_from_message(parsed) for tally in screen: assert tally.rh_tally == TallyColor.RED # For each tally, send a screen-broadcast (not tally-broadcast) setting # `text='Broadcast-{tally.index}', rh_tally=TallyColor.GREEN` for tally_index in range(10): t_id = (0xffff, tally_index) txt = f'Broadcast-{tally_index}' tally = sender.broadcast_screen.add_tally( tally_index, text=txt) #, rh_tally=TallyColor.GREEN) tally.rh_tally = TallyColor.GREEN # Wait for data and parse it into a separate broadcast screen data, addr = await protocol.queue.get() protocol.queue.task_done() parsed, _ = Message.parse(data) assert parsed.screen == 0xffff bc_screen.update_from_message(parsed) assert tally_index in bc_screen bc_tally = bc_screen[tally_index] assert bc_tally.id == t_id assert bc_tally.text == txt assert bc_tally.rh_tally == TallyColor.GREEN # Parse the same screen-broadcast message into each of the 10 normal screens # This **should** change the tally values as well # (unless I'm mis-interpreting the specification) for screen in screens.values(): screen.update_from_message(parsed) sc_tally = screen[tally_index] assert sc_tally.text == txt assert sc_tally.rh_tally == TallyColor.GREEN # Wait for the periodic refresh from the sender which **should** send # the original tally states and not the broadcast ones (?) # # That's unclear, but we definitely don't want the broadcast screen # sending constant updates, so let's check that doesn't happen. # (even though that isn't specifically stated either) assert protocol.queue.empty() await asyncio.sleep(sender.tx_interval * 2) assert not protocol.queue.empty() while not protocol.queue.empty(): data, addr = await protocol.queue.get() protocol.queue.task_done() parsed, _ = Message.parse(data) assert parsed.screen in screens assert parsed.screen != 0xffff screen = screens[parsed.screen] screen.update_from_message(parsed) for tally in screen: assert tally.text == f'Tally-{tally.id}' assert tally.rh_tally == TallyColor.RED
def test_uhs_message(uhs500_msg_bytes, uhs500_msg_parsed): parsed, remaining = Message.parse(uhs500_msg_bytes) assert not len(remaining) assert parsed == uhs500_msg_parsed