async def _update_aliases(self, aliases): """Update the set of aliases to match a new specification.""" async with self._aliases_lock: # Do nothing if not changed if self._aliases_json == aliases: return # Check for dependency cycles cycle = has_cycle( {a["alias"]: a["target"] for a in aliases.values()}) if cycle: # Revert if cycle is found await self._error("cyclic alias dependency: {}".format( " -> ".join(cycle))) await self._client.set_property(self._aliases_path, self._aliases_json) return old_aliases = set(self._aliases) new_aliases = set(aliases) added = new_aliases - old_aliases removed = old_aliases - new_aliases changed = set() # Changed aliases are treated as removed-then-added for path in old_aliases & new_aliases: if self._aliases[path].json != aliases[path]: changed.add(path) logging.info( "Updating aliases: Added: %s. Changed: %s. Removed: %s.", ", ".join(added), ", ".join(changed), ", ".join(removed)) todo = [] # Update the set of aliases for path in removed | changed: todo.append(self._aliases.pop(path).delete()) for path in added | changed: alias = Alias(self, **aliases[path]) todo.append(alias.async_init()) self._aliases[path] = alias # Update the property todo.append( self._client.set_property(self._aliases_path, self._aliases_json)) if todo: await asyncio.wait(todo, loop=self._loop) # Save aliases to file try: with open(self._cache_file, "w") as f: json.dump(self._aliases_json, f) except Exception as e: logging.exception(e)
async def test_transform_inverse(mock_alias_server): a = Alias(mock_alias_server, "foo/target", "foo/alias", "value / 63.0", "int(value * 63)") assert a._transform(0) == 0.0 assert a._transform(63) == 1.0 assert a._inverse(0.0) == 0 assert a._inverse(1.0) == 63
async def test_multiple_registration_changes(mock_alias_server, mock_client, event_loop): a = Alias(mock_alias_server, "foo/target", "foo/alias", "value / 63.0", "int(value * 63)", "Alias description.") await a.async_init() # If many registration changes occur, this should only turn into a minimum # number of calls which should finally end with a consistent state. # Make the 'watch_property' call block to simulate calls taking time watch_property_event = asyncio.Event(loop=event_loop) async def watch_property_side_effect(*_, **__): await watch_property_event.wait() mock_client.watch_property = Mock(side_effect=watch_property_side_effect) # Send a series of conflicting registrations todo = [] for _ in range(10): for behaviour in ["PROPERTY-1:N", "EVENT-N:1"]: todo.append( a._on_target_registration_changed( "foo/target", [{ "behaviour": behaviour, "description": "Target description", }])) # Make sure they all get started... await asyncio.sleep(0.1, loop=event_loop) # let them all proceed watch_property_event.set() await asyncio.wait(todo, loop=event_loop) # Make sure the final registration settles on one value assert a._watching_property != a._watching_event # Make sure not every call resulted in a change assert mock_client.register.call_count < 20 assert mock_client.unregister.call_count == 0 # And that the watches we're left with are consistent if a._watching_property: assert mock_client.watch_property.call_count > \ mock_client.unwatch_property.call_count assert mock_client.watch_event.call_count == \ mock_client.unwatch_event.call_count else: assert mock_client.watch_event.call_count > \ mock_client.unwatch_event.call_count assert mock_client.watch_property.call_count == \ mock_client.unwatch_property.call_count
async def test_ambiguous_registration(mock_alias_server, mock_client): a = Alias(mock_alias_server, "foo/target", "foo/alias", "value / 63.0", "int(value * 63)", "Alias description.") await a.async_init() # Check to see if when sent an ambiguous registration (e.g. a path which is # both a directory and a non-directory) we choose the first non-directory. await a._on_target_registration_changed("foo/target", [ { "behaviour": "DIRECTORY" }, { "behaviour": "PROPERTY-N:1", "description": "A property, really.", }, { "behaviour": "EVENT-N:1", "description": "An event.", }, ]) assert a._target_registration == { "behaviour": "PROPERTY-N:1", "description": "A property, really.", }
async def test_eval_transform(mock_alias_server): a = Alias(mock_alias_server, "foo/target", "foo/alias") assert a._eval_transform(None, 123) == 123 assert a._eval_transform(None, qth.Empty) is qth.Empty assert mock_alias_server._error_sync.call_count == 0 assert a._eval_transform("False", qth.Empty) is qth.Empty assert a._eval_transform("False", 123) is False assert mock_alias_server._error_sync.call_count == 0 assert a._eval_transform("math.floor(value)", qth.Empty) is qth.Empty assert a._eval_transform("math.floor(value)", 1.23) == 1 assert mock_alias_server._error_sync.call_count == 0 assert a._eval_transform("math.floor(value)", "bad") == "bad" assert mock_alias_server._error_sync.call_count == 1
async def test_json(mock_alias_server, mock_ls): a = Alias(mock_alias_server, "foo/target", "foo/alias", "value / 63.0", "int(value * 63)", "A test alias.") await a.async_init() assert a.json == { "target": "foo/target", "alias": "foo/alias", "transform": "value / 63.0", "inverse": "int(value * 63)", "description": "A test alias.", }
async def test_delete(mock_alias_server, mock_client, is_property, on_unregister, delete_on_unregister): a = Alias(mock_alias_server, "foo/target", "foo/alias") await a.async_init() reg = { "behaviour": "PROPERTY-N:1" if is_property else "EVENT-N:1", "description": "A property.", } if on_unregister is not None: reg["on_unregister"] = on_unregister if delete_on_unregister is not None: reg["delete_on_unregister"] = delete_on_unregister await a._on_target_registration_changed("foo/target", [reg]) await a.delete() # Unregistered mock_client.unregister.assert_called_with("foo/alias") # Unwatched if is_property: mock_client.unwatch_property.assert_any_call("foo/target", a._on_target_set) mock_client.unwatch_property.assert_any_call("foo/alias", a._on_alias_set) else: mock_client.unwatch_event.assert_any_call("foo/target", a._on_target_sent) mock_client.unwatch_event.assert_any_call("foo/alias", a._on_alias_sent) # on_unregister sent if on_unregister is not None: if is_property: mock_client.set_property.assert_called_once_with("foo/alias", 123) assert mock_client.send_event.call_count == 0 else: mock_client.send_event.assert_called_once_with("foo/alias", 123) assert mock_client.set_property.call_count == 0 else: assert mock_client.set_property.call_count == 0 assert mock_client.send_event.call_count == 0 # delete_on_unregister if delete_on_unregister is True: mock_client.delete_property.assert_called_once_with("foo/alias") else: assert mock_client.delete_property.call_count == 0
async def test_init(mock_alias_server, mock_ls): a = Alias(mock_alias_server, "foo/target", "foo/alias") await a.async_init() mock_ls.watch_path.assert_called_once_with( "foo/target", a._on_target_registration_changed)
async def test_on_target_registration_changed(mock_alias_server, mock_client): a = Alias(mock_alias_server, "foo/target", "foo/alias", "value / 63.0", "int(value * 63)", "Alias description.") await a.async_init() # Initially should have no registrations assert not a._watching_property assert not a._watching_event assert a._target_registration is None assert a._alias_registration is None assert mock_client.watch_property.call_count == 0 assert mock_client.watch_event.call_count == 0 assert mock_client.unwatch_property.call_count == 0 assert mock_client.unwatch_event.call_count == 0 assert mock_client.register.call_count == 0 assert mock_client.unregister.call_count == 0 # Repeat of no-registration should do nothing await a._on_target_registration_changed("foo/target", None) assert not a._watching_property assert not a._watching_event assert a._target_registration is None assert a._alias_registration is None assert mock_client.watch_property.call_count == 0 assert mock_client.watch_event.call_count == 0 assert mock_client.unwatch_property.call_count == 0 assert mock_client.unwatch_event.call_count == 0 assert mock_client.register.call_count == 0 assert mock_client.unregister.call_count == 0 # Test singular registration with on_unregister requiring conversion. await a._on_target_registration_changed("foo/target", [{ "behaviour": "EVENT-1:N", "description": "Underlying description", "on_unregister": 63, }]) assert not a._watching_property assert a._watching_event assert a._target_registration == { "behaviour": "EVENT-1:N", "description": "Underlying description", "on_unregister": 63, } assert a._alias_registration == { "behaviour": "EVENT-1:N", "description": "Alias description.", "on_unregister": 1.0, } assert mock_client.watch_property.call_count == 0 assert mock_client.watch_event.call_count == 2 assert mock_client.unwatch_property.call_count == 0 assert mock_client.unwatch_event.call_count == 0 mock_client.watch_event.assert_any_call("foo/target", a._on_target_sent) mock_client.watch_event.assert_any_call("foo/alias", a._on_alias_sent) mock_client.register.assert_called_once_with( "foo/alias", behaviour="EVENT-1:N", description="Alias description.", on_unregister=1.0) assert mock_client.unregister.call_count == 0 # Test repeated registration: no change await a._on_target_registration_changed("foo/target", [{ "behaviour": "EVENT-1:N", "description": "Underlying description", "on_unregister": 63, }]) assert not a._watching_property assert a._watching_event assert a._target_registration == { "behaviour": "EVENT-1:N", "description": "Underlying description", "on_unregister": 63, } assert a._alias_registration == { "behaviour": "EVENT-1:N", "description": "Alias description.", "on_unregister": 1.0, } assert mock_client.watch_property.call_count == 0 assert mock_client.watch_event.call_count == 2 assert mock_client.unwatch_property.call_count == 0 assert mock_client.unwatch_event.call_count == 0 assert mock_client.register.call_count == 1 assert mock_client.unregister.call_count == 0 # Test same type registration: only changes registration await a._on_target_registration_changed("foo/target", [{ "behaviour": "EVENT-N:1", "description": "Underlying description changed.", "on_unregister": 0, }]) assert not a._watching_property assert a._watching_event assert a._target_registration == { "behaviour": "EVENT-N:1", "description": "Underlying description changed.", "on_unregister": 0, } assert a._alias_registration == { "behaviour": "EVENT-N:1", "description": "Alias description.", "on_unregister": 0.0, } assert mock_client.watch_property.call_count == 0 assert mock_client.watch_event.call_count == 2 assert mock_client.unwatch_property.call_count == 0 assert mock_client.unwatch_event.call_count == 0 assert mock_client.register.call_count == 2 mock_client.register.assert_called_with("foo/alias", behaviour="EVENT-N:1", description="Alias description.", on_unregister=0.0) assert mock_client.unregister.call_count == 0 # Change type should result in registration changes await a._on_target_registration_changed( "foo/target", [{ "behaviour": "PROPERTY-N:1", "description": "Underlying description changed.", "delete_on_unregister": True, }]) assert a._watching_property assert not a._watching_event assert a._target_registration == { "behaviour": "PROPERTY-N:1", "description": "Underlying description changed.", "delete_on_unregister": True, } assert a._alias_registration == { "behaviour": "PROPERTY-N:1", "description": "Alias description.", "delete_on_unregister": True, } assert mock_client.watch_property.call_count == 2 assert mock_client.watch_event.call_count == 2 assert mock_client.unwatch_property.call_count == 0 assert mock_client.unwatch_event.call_count == 2 mock_client.watch_property.assert_any_call("foo/alias", a._on_alias_set) mock_client.watch_property.assert_any_call("foo/target", a._on_target_set) mock_client.unwatch_event.assert_any_call("foo/alias", a._on_alias_sent) mock_client.unwatch_event.assert_any_call("foo/target", a._on_target_sent) assert mock_client.register.call_count == 3 mock_client.register.assert_called_with("foo/alias", behaviour="PROPERTY-N:1", description="Alias description.", delete_on_unregister=True) assert mock_client.unregister.call_count == 0 # ...and back await a._on_target_registration_changed( "foo/target", [{ "behaviour": "EVENT-N:1", "description": "Underlying description changed.", }]) assert not a._watching_property assert a._watching_event assert a._target_registration == { "behaviour": "EVENT-N:1", "description": "Underlying description changed.", } assert a._alias_registration == { "behaviour": "EVENT-N:1", "description": "Alias description.", } assert mock_client.watch_property.call_count == 2 assert mock_client.watch_event.call_count == 4 assert mock_client.unwatch_property.call_count == 2 assert mock_client.unwatch_event.call_count == 2 mock_client.watch_event.assert_any_call("foo/alias", a._on_alias_sent) mock_client.watch_event.assert_any_call("foo/target", a._on_target_sent) mock_client.unwatch_property.assert_any_call("foo/alias", a._on_alias_set) mock_client.unwatch_property.assert_any_call("foo/target", a._on_target_set) assert mock_client.register.call_count == 4 mock_client.register.assert_called_with("foo/alias", behaviour="EVENT-N:1", description="Alias description.") assert mock_client.unregister.call_count == 0 # Removal should completely unregister but retain the most recent watch await a._on_target_registration_changed("foo/target", None) assert not a._watching_property assert a._watching_event assert a._target_registration is None assert a._alias_registration is None assert mock_client.watch_property.call_count == 2 assert mock_client.watch_event.call_count == 4 assert mock_client.unwatch_property.call_count == 2 assert mock_client.unwatch_event.call_count == 2 assert mock_client.register.call_count == 4 mock_client.unregister.assert_called_once_with("foo/alias")