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_broadcast(faker): for _ in range(1000): i = faker.pyint(max_value=0xfffe) tally = Tally(i) assert not tally.is_broadcast assert not Display.from_tally(tally).is_broadcast tally1 = Tally(0xffff) tally2 = Tally.broadcast() assert tally1.is_broadcast assert tally2.is_broadcast assert Display.from_tally(tally1).is_broadcast assert Display.from_tally(tally2).is_broadcast
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 send_broadcast_tally(self, screen_index: int, **kwargs): """Send a :attr:`broadcast <.Display.is_broadcast>` update to all listening displays Arguments: screen_index: The screen :attr:`~.Screen.index` **kwargs: The keyword arguments to pass to the :class:`~.Tally` constructor .. versionadded:: 0.0.2 .. versionchanged:: 0.0.3 Added the screen_index parameter """ screen = self.get_or_create_screen(screen_index) tally = screen.broadcast_tally(**kwargs) if tally.text == '' or tally.control != b'': msg_type = MessageType.control else: msg_type = MessageType.display msg = self._build_message(screen=screen_index) disp = Display.from_tally(tally, msg_type=msg_type) msg.displays.append(disp) async with self._tx_lock: await self.send_message(msg) screen.unbind(self) for oth_tally in screen: oth_tally.update_from_display(disp) self._bind_screen(screen)
async def send_screen_update(self, screen: Screen): if screen.is_broadcast: return msg = self._build_message(screen=screen.index) for tally in screen: disp = Display.from_tally(tally) msg.displays.append(disp) await self.send_message(msg)
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
async def on_tally_control(self, tally: Tally, data: bytes, **kwargs): if self.running: async with self._tx_lock: disp = Display.from_tally(tally, msg_type=MessageType.control) msg = self._build_message( screen=tally.screen.index, displays=[disp], ) await self.send_message(msg)
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
async def test_control_event(faker): loop = asyncio.get_event_loop() listener = Listener() disp = Display(index=0) tally = Tally.from_display(disp) tally.bind_async(loop, on_control=listener.callback) for _ in range(100): data_len = faker.pyint(min_value=1, max_value=1024) control_data = faker.binary(length=data_len) disp = Display(index=0, control=control_data) tally.update_from_display(disp) _, rx_data = await listener.get() assert rx_data == tally.control == disp.control == control_data assert disp == tally == Tally.from_display(disp)
async def test_control_event_with_text(faker): loop = asyncio.get_event_loop() text_listener = Listener() ctrl_listener = Listener() tally_text = 'foo' disp = Display(index=0, text=tally_text) tally = Tally.from_display(disp) assert disp == tally tally.bind_async(loop, on_update=text_listener.callback, on_control=ctrl_listener.callback, ) for _ in range(100): for word in faker.words(3): data_len = faker.pyint(min_value=1, max_value=1024) control_data = faker.binary(length=data_len) disp = Display(index=0, control=control_data) tally.update_from_display(disp) _, rx_data = await ctrl_listener.get() assert rx_data == tally.control == disp.control == control_data assert tally.text == tally_text _, props_changed = await text_listener.get() assert set(props_changed) == set(['control']) tally_text=word disp = Display(index=0, text=tally_text) tally.update_from_display(disp) _, props_changed = await text_listener.get() assert set(props_changed) == set(['text']) assert tally.text == tally_text assert tally.control == control_data
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_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 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_tally_display_conversion(faker): # i = 0 # if True: for _ in range(100): i = faker.pyint(max_value=0xfffe) disp = Display(index=i) tally = Tally(i) for tally_type, color in iter_tally_types_and_colors(): brightness = faker.pyint(max_value=3) # print(f'{i=}, {tally_type=}, {color=}') disp.brightness = brightness tally.brightness = brightness assert 0 <= tally.normalized_brightness <= 1 assert tally.normalized_brightness == brightness / 3 setattr(tally, tally_type.name, color) setattr(disp, tally_type.name, color) for word in faker.words(3): # print(f'{word=}') tally.text = word disp.text = word assert disp == Tally.from_display(disp) == tally assert disp == Display.from_tally(tally) == tally assert Tally.from_display(disp).normalized_brightness == tally.normalized_brightness
async def tx_loop(self): async def get_queue_item(timeout): try: item = await asyncio.wait_for(self.update_queue.get(), timeout) if item[1] is False: return False except asyncio.TimeoutError: item = None return item await self.connected_evt.wait() while self.running: item = await get_queue_item(self.tx_interval) if item is False: self.update_queue.task_done() break elif item is None and not self._tx_lock.locked(): await self.send_full_update() else: screen_index, _ = item ids = set([item]) self.update_queue.task_done() while not self.update_queue.empty(): try: item = self.update_queue.get_nowait() except asyncio.QueueEmpty: break if item is False: self.update_queue.task_done() return _screen_index, _ = item if _screen_index == screen_index: ids.add(item) self.update_queue.task_done() else: await self.update_queue.put(item) break msg = self._build_message(screen=screen_index) tallies = {i:self.tallies[i] for i in ids} async with self._tx_lock: for key in sorted(tallies.keys()): tally = tallies[key] msg.displays.append(Display.from_tally(tally)) await self.send_message(msg)
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 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)
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)
async def test_update_event(faker): loop = asyncio.get_event_loop() listener = Listener() tally = Tally(0) tally.bind_async(loop, on_update=listener.callback) tally.text = 'foo' _, props_changed = await listener.get() assert set(props_changed) == set(['text']) d = dict(rh_tally=TallyColor.RED, txt_tally=TallyColor.GREEN, lh_tally=TallyColor.AMBER) tally.update(**d) _, props_changed = await listener.get() assert set(props_changed) == set(d.keys()) disp = Display(index=0, text=tally.text) tally.update_from_display(disp) assert disp == tally _, props_changed = await listener.get() assert set(props_changed) == set(['rh_tally', 'txt_tally', 'lh_tally']) for tally_type, color in iter_tally_types_and_colors(): attr = tally_type.name should_change = getattr(tally, attr) != color # print(f'{tally_type=}, {color=}, {should_change=}') setattr(tally, attr, color) if should_change: _, props_changed = await listener.get() assert set(props_changed) == set([attr]) else: await asyncio.sleep(.01) assert listener.results.empty()