def test_creation(): a_table = AssociativeTable() a_table = AssociativeTable('foo', 'bar', 'baz') assert hasattr(a_table, 'foo') assert hasattr(a_table, 'bar') assert hasattr(a_table, 'baz') assert a_table._column['foo'] is a_table.foo assert a_table._column['bar'] is a_table.bar assert a_table._column['baz'] is a_table.baz
def test_delete_complex(): element_a = 'a' element_b = 'b' a_table = AssociativeTable('foo', 'bar') a_table.foo.add(element_a) a_table.bar.add(element_b) a_table._assoc(element_a, element_b) del a_table[element_b] assert a_table[element_a] == set()
def test_path(): elem_a = 'a' elem_b = 'b' elem_c = 'c' a_table = AssociativeTable('foo', 'bar', 'baz') a_table.foo.add(elem_a) a_table.bar.add(elem_b) a_table.baz.add(elem_c) a_table._assoc(elem_a, elem_b) a_table._assoc(elem_b, elem_c) assert a_table.get(elem_a, path=(a_table.bar, a_table.baz)) == set(elem_c)
def test_get(): element = 'foo' a_table = AssociativeTable('foo') with pytest.raises(KeyError): a_table[element] with pytest.raises(KeyError): a_table.get(element) with pytest.raises(KeyError): a_table.foo[element] with pytest.raises(KeyError): a_table.foo.get(element) a_table.foo.add(element) assert a_table[element] == set() assert a_table.foo[element] == set()
def test_association(): elem_aa = 'aa' elem_ab = 'ab' elem_ba = 'ba' elem_bb = 'bb' a_table = AssociativeTable('foo', 'bar') a_table.foo.add(elem_aa) a_table.foo.add(elem_ab) a_table.bar.add(elem_ba) a_table.bar.add(elem_bb) a_table._assoc(elem_aa, elem_ba) a_table._assoc(elem_ab, elem_bb) assert a_table[elem_aa] == set([elem_ba]) assert a_table[elem_bb] == set([elem_ab]) assert a_table.get(elem_aa, elem_ab) == set([elem_ba, elem_bb])
def test_presence(): element = 'a' a_table = AssociativeTable('foo') assert element not in a_table a_table.foo.add(element) assert element in a_table assert element in a_table.foo
def test_delete_simple(): element = 'a' a_table = AssociativeTable('foo') with pytest.raises(KeyError): del a_table[element] a_table.foo.add(element) del a_table[element] assert element not in a_table
def test_dissociation(): elem_a = 'a' elem_b = 'b' a_table = AssociativeTable('foo', 'bar') a_table.foo.add(elem_a) a_table.foo.add(elem_b) a_table._assoc(elem_a, elem_b) a_table._dissoc(elem_a, elem_b) assert a_table[elem_a] == set() assert a_table[elem_b] == set()
def test_association_with_filter(): elem_a = 'a' elem_b = 'b' elem_c = 'c' a_table = AssociativeTable('foo', 'bar', 'baz') a_table.foo.add(elem_a) a_table.bar.add(elem_b) a_table.baz.add(elem_c) a_table._assoc(elem_a, elem_b) a_table._assoc(elem_b, elem_c) assert a_table[elem_b] == set([elem_a, elem_c]) assert a_table.get(elem_b, tables=(a_table.foo, )) == set(elem_a) assert a_table.get(elem_b, tables=(a_table.baz, )) == set(elem_c) assert a_table.get(elem_b, tables=(a_table.foo, a_table.baz)) == \ set([elem_a, elem_c])
def __init__(self, dclasses): self.dclasses = [dclasses[dclass_name] for dclass_name in sorted(dclasses)] for dclass_id, dclass in enumerate(self.dclasses): dclass.dclass_id = dclass_id self.state = AssociativeTable('recipients', 'zones', 'dobjects') # FIXME: Recipients and zones should be BijectiveMaps as well. self.recipients = AssociativeTable('r_id', 'r_object') self.zones = AssociativeTable('z_id', 'z_object') self.dobjects = BijectiveMap() # TODO: For the moment, we'll use a single lock to protect the whole of # the state; dobject existence, presence in zones, and interest. This is # a topic ripe for optimization, if you can do it without creating # deadlocks. Maybe do optimistic concurrency? Have a nifty planner # that'll schedule event processing into isolated parallelity? self.state_lock = Lock() self.emission_queue = Queue()
def test_unique_elements(): element = 'a' a_table = AssociativeTable('foo', 'bar') a_table.foo.add(element) with pytest.raises(ValueError): a_table.bar.add(element)
class SimpleStateKeeper(BaseStateKeeper): def __init__(self, dclasses): self.dclasses = [dclasses[dclass_name] for dclass_name in sorted(dclasses)] for dclass_id, dclass in enumerate(self.dclasses): dclass.dclass_id = dclass_id self.state = AssociativeTable('recipients', 'zones', 'dobjects') # FIXME: Recipients and zones should be BijectiveMaps as well. self.recipients = AssociativeTable('r_id', 'r_object') self.zones = AssociativeTable('z_id', 'z_object') self.dobjects = BijectiveMap() # TODO: For the moment, we'll use a single lock to protect the whole of # the state; dobject existence, presence in zones, and interest. This is # a topic ripe for optimization, if you can do it without creating # deadlocks. Maybe do optimistic concurrency? Have a nifty planner # that'll schedule event processing into isolated parallelity? self.state_lock = Lock() self.emission_queue = Queue() def get_dobject_fields(self, dobject_id): with self.state_lock: dobject = self.dobjects[dobject_id].dfields return dobject def _queue_message(self, *message): # FIXME: Make sure that all data is copies. self.emission_queue.put(message) def _work_emission_queue(self): working = True while working: try: message = self.emission_queue.get(block=False) self.message_director.create_message(*message) except Empty: working = False def emit_create_dobject_view(self, recipients, dobject_maps): for recipient in recipients: for dobject_id, dobject in dobject_maps: self._queue_message( self.individual_channel, recipient, msgtypes.CREATE_DOBJECT_VIEW, dobject_id, dobject.dclass_id, dobject.storage, ) def emit_destroy_dobject_view(self, recipients, dobject_ids): for recipient in recipients: for dobject_id in dobject_ids: self._queue_message( self.individual_channel, recipient, msgtypes.DESTROY_DOBJECT_VIEW, dobject_id, ) def create_recipient(self, recipient_id): with self.state_lock: recipient = Recipient(recipient_id) self.recipients.r_id.add(recipient_id) self.recipients.r_object.add(recipient) self.recipients._assoc(recipient_id, recipient) self.state.recipients.add(recipient) def create_dobject(self, dobject_id, dclass_id, fields): with self.state_lock: dclass = self.dclasses[dclass_id] dobject = dclass( dobject_id, fields, ) self.dobjects[dobject_id] = dobject self.state.dobjects.add(dobject) def create_zone(self, zone_id): with self.state_lock: zone = Zone(zone_id) self.zones.z_id.add(zone_id) self.zones.z_object.add(zone) self.zones._assoc(zone_id, zone) self.state.zones.add(zone) def _dobjects_seen(self, recipient): return self.state.get( recipient, path=(self.state.zones, self.state.dobjects), ) def _dobject_seen_by(self, dobject): return self.state.get( dobject, path=(self.state.zones, self.state.recipients), ) def _dobject_to_emittable(self, dobject): dobject_id = self.dobjects.getreverse(dobject) return (dobject_id, dobject) def _dobject_id_to_emittable(self, dobject_id): dobject = self.dobjects[dobject_id] return (dobject_id, dobject) def set_interest(self, recipient_id, zone_id): if recipient_id not in self.recipients: self.create_recipient(recipient_id) if zone_id not in self.zones: self.create_zone(zone_id) with self.state_lock: (recipient, ) = self.recipients[recipient_id] (zone, ) = self.zones[zone_id] dobjects_before = self._dobjects_seen(recipient) self.state._assoc(recipient, zone) dobjects_after = self._dobjects_seen(recipient) new_dobjects = dobjects_after - dobjects_before emittables = [self._dobject_to_emittable(dobject) for dobject in new_dobjects] self.emit_create_dobject_view([recipient_id], emittables) self._work_emission_queue() return [new_dobject_id for (new_dobject_id, _) in emittables] def unset_interest(self, recipient_id, zone_id): with self.state_lock: (recipient, ) = self.recipients[recipient_id] (zone, ) = self.zones[zone_id] dobjects_before = self._dobjects_seen(recipient) self.state._dissoc(recipient, zone) dobjects_after = self._dobjects_seen(recipient) lost_dobjects = dobjects_before - dobjects_after self.emit_destroy_dobject_view([recipient_id], lost_dobjects) self._work_emission_queue() return lost_dobject_ids def add_presence(self, dobject_id, zone_id): if zone_id not in self.zones: self.create_zone(zone_id) with self.state_lock: dobject = self.dobjects[dobject_id] (zone, ) = self.zones[zone_id] recipients_before = self._dobject_seen_by(dobject) self.state._assoc(dobject, zone) recipients_after = self._dobject_seen_by(dobject) new_recipients = recipients_after - recipients_before new_recipient_ids = {next(iter(self.recipients[nr])) for nr in new_recipients} emittable = [self._dobject_id_to_emittable(dobject_id)] self.emit_create_dobject_view(new_recipient_ids, emittable) self._work_emission_queue() return new_recipient_ids def remove_presence(self, dobject_id, zone_id): with self.state_lock: dobject = self.dobjects[dobject_id] (zone, ) = self.zones[zone_id] recipients_before = self._dobject_seen_by(dobject) self.state._dissoc(dobject, zone) recipients_after = self._dobject_seen_by(dobject) lost_recipients = recipients_before - recipients_after lost_recipient_ids = {next(iter(self.recipients[nr])) for nr in lost_recipients} self.emit_destroy_dobject_view(lost_recipient_ids, [dobject_id]) self._work_emission_queue() return lost_recipient_ids def set_ai(self, ai_channel, dobject_id): with self.state_lock: self.dobjects[dobject_id].set_ai(ai_channel) # self.message_director.create_message( # self.all_connections, # FIXME: This individual StateServer's ID # ai_channel, # msgtypes.CREATE_AI_VIEW, # dobject_id, # FIXME: Either we also need to add all state # # information, or, better (because later updates) # # assure that the dobject is already in the AI's # # interest. # ) def set_owner(self, owner_channel, dobject_id): with self.state_lock: # TODO: Destroy owner view if another owner was set. # TODO: Check whether dobject is even visible to client self.dobjects[dobject_id].set_owner(owner_channel) self._queue_message( self.all_connections, # FIXME: This individual StateServer's ID owner_channel, msgtypes.BECOME_OWNER, dobject_id, ) self._work_emission_queue() def set_field(self, source, dobject_id, field_id, value): with self.state_lock: dobject = self.dobjects[dobject_id] # FIXME: Jam this into DClass somehow? _, field_type, policy = dobject._dfields[field_id] # Is the source even allowed to set this field? if (policy & fp.CLIENT_SEND) or \ ((policy & fp.OWNER_SEND) and source == dobject.owner) or \ ((policy & fp.AI_SEND) and source == dobject.ai): # If it's a storage field, set its value if policy & (fp.RAM | fp.PERSIST): pass # FIXME # Emit if policy & fp.CLIENT_RECEIVE: for recipient in self._dobject_seen_by(dobject): self._queue_message( source, recipient, msgtypes.FIELD_UPDATE, dobject_id, field_id, value ) elif policy & fp.OWNER_RECEIVE: self._queue_message( source, dobject.owner, msgtypes.FIELD_UPDATE, dobject_id, field_id, value ) elif policy & fp.AI_RECEIVE: self._queue_message( source, dobject.ai, msgtypes.FIELD_UPDATE, dobject_id, field_id, value ) # NOTE: else? I mean, there MUST be a sending policy? else: raise Exception("{} tried to set {} field {} to {}. " "".format(source, dobject, field_id, value)) self._work_emission_queue()