[sugar] review: gadget buddy search code
Guillaume Desmottes
guillaume.desmottes at collabora.co.uk
Thu May 15 09:04:39 EDT 2008
Le mercredi 14 mai 2008 à 22:52 +0100, Dafydd Harries a écrit :
> > +# FIXME: add more types
> > +type_to_str = {str: 'str'}
> > +str_to_type = {'str': str}
> > +
>
> We should check which types the buddy/activity interfaces support for
> properties, and test each of those types.
>
Gabble supports the following types: str, bytes, int, uint and bool.
I started to write tests for parse_properties and properties_to_xml but
I encountered a problem.
Currently we store properties in a dict like {'key': (type, 'value')} as
{'color': (str, 'red')} for example.
But how should I store an uint as Python doesn't have an uint type?
Using dbus type? Or we could store the string representation of the type
instead of a python type ('str' instead of str).
And how should we store the value? Using the type-converted value (so
the numerical value for int/uint, True/False for bool, etc) or the
string version?
> > @@ -46,10 +50,25 @@ def parse_properties(elem):
> > if type is None or name is None or value is None:
> > continue
> >
> > - properties[name] = (type, value)
> > + try:
> > + properties[name] = (str_to_type[type], value)
> > + except KeyError:
> > + continue
>
> We shouldn't just ignore failed conversions. We should throw an error, and
> make the caller return an error to the client. Perhaps the exception class
> would have some convenience code for generating the error stanza.
>
Good idea. Implemented.
> > +def properties_to_xml(properties, elem):
> > + for key, (type, value) in properties.iteritems():
> > + try:
> > + str_type = type_to_str[type]
> > + except KeyError:
> > + continue
> > +
> > + property = elem.addElement((None, 'property'))
> > + property['type'] = str_type
> > + property['name'] = key
> > + property.addContent(value)
> > +
> > class Room(object):
> > def __init__(self, own_nick):
> > self.own_nick = own_nick
>
> Again, we shouldn't silently ignore errors. This lookup should never fail,
> because it would mean that we've stored invalid property data in the model.
>
Right, fixed.
> > @@ -118,19 +137,100 @@ class GadgetService(component.Service):
> >
> > def iq_buddy_query(self, stanza):
> > result = make_iq_result(stanza)
> > + _query = stanza.firstChildElement()
> > query = result.firstChildElement()
> >
> > - for buddy in self.model.buddy_search({}):
> > + results = set()
> > + if len(_query.children) == 0:
> > + # no buddy childs. We want all the buddies
> > + results.update(self.model.buddy_search({}))
>
> s/childs/children/.
>
> Please put blank lines before blocks, for consistency.
>
> Can we say "if _query.children:" instead? It's more idiomatic.
>
fixed.
> > +
> > + for elem in _query.children:
>
> I'm not sure we should support answering multiple queries in one IQ. I'm not
> sure if it does any harm, but I haven't seen other XMPP protocols allow it.
>
Don't know, I implemented the protocol described on
http://wiki.laptop.org/go/XMPP_Component_Protocol
> > + if elem.name != 'buddy':
> > + continue
> > + found = None
> > +
> > + # jid search
> > + try:
> > + tmp = set([self.model.buddy_by_id(elem['jid'])])
> > + if found is None:
> > + found = tmp
> > + else:
> > + found.intersection_update(tmp)
> > + except KeyError:
> > + pass
>
> The "if found is None:" seems strange, becuase "found" should always be None
> at this point in the code.
>
fixed.
> There's too much code inside the try: clause. What lookup are we expecting
> might fail? If it's the elem['jid'] lookup, I'd prefer this approach:
>
buddy_by_id can raise the exception too. I made this code clearer.
> jid = elem.getAttribute('jid')
>
> if jid is None:
> continue
>
> > +
> > + # properties search
> > + for e in elem.children:
> > + if e.name == 'properties' and e.uri == ns.BUDDY_PROP:
> > + props = parse_properties(e)
> > + tmp = set(self.model.buddy_search(props))
> > + if found is None:
> > + found = tmp
> > + else:
> > + found.intersection_update(tmp)
>
> Can we rename "tmp" to "buddies" or "results"?
>
done
> > +
> > + if found is not None:
> > + results.update(found)
> > +
> > + for buddy in results:
> > buddy_elem = query.addElement((ns.BUDDY, 'buddy'))
> > buddy_elem['jid'] = buddy.jid
> > + properties = buddy_elem.addElement((ns.BUDDY_PROP, 'properties'))
> > + properties_to_xml(buddy.properties, properties)
> >
> > return result
>
> I would prefer that properties_to_xml be a pure function. I.e. used like:
>
> for node in properties_to_xml(buddy.properties):
> properties.addChild(node)
>
Seems sane. done.
> I think much of this logic could be moved into the model, and that the code
> in iq_buddy_query() be limited to converting to/from XML.
Agree. done.
> > + def activity_by_participants(self, buddies):
> > + results = []
> > + for activity in self.activities.itervalues():
> > + if set(buddies).issubset(set(activity.members)):
> > + results.append(activity)
> > +
> > + return results
> > +
>
> These lookups seem like they will scale poorly. We shouldn't optimise them
> prematurely, but we should develop some stress tests for this code so that we
> can find bottlenecks. I have a little (uncommitted) code that stresses the
> model directly, but we should also test via the XMPP interface.
>
I added a comment warning about this.
> > @@ -118,11 +118,139 @@ class BuddyIqTest2(TestCase):
> > query = stanza.firstChildElement()
> > assert query.name == 'query'
> > elements = list(query.elements())
> > + elements.sort()
>
> I would prefer:
>
> elements = sorted(query.elements())
>
Fixed.
I also add a cmp function so elements are sorted in a more predictable
way.
> > assert len(elements) == 2
> > assert elements[0].name == 'buddy'
> > - assert elements[0]['jid'] == 'foo'
> > + assert elements[0]['jid'] == 'bar'
> > assert elements[1].name == 'buddy'
> > - assert elements[1]['jid'] == 'bar'
> > + assert elements[1]['jid'] == 'foo'
> > + self.done.callback(None)
> > +
> > +class BuddyIqTest3(TestCase):
> ...
> > +
> > +class BuddyIqTest4(TestCase):
> ...
> > +
> > +class BuddyIqTest5(TestCase):
> ...
> >
> > class ActivityIqTest(TestCase):
> ...
> >
> > +class ActivityIqTest4(TestCase):
> ...
> > +
> > +class ActivityIqTest5(TestCase):
> ...
> > +
> > +class ActivityIqTest6(TestCase):
> ...
> > +
> > +class ActivityIqTest7(TestCase):
> ...
> > +
> > +class ActivityIqTest8(TestCase):
> ...
>
> There's a lot of repeated/boilerplate code in these tests that makes them hard
> to read. We should try and factor out the common elements, either by adding
> some convenience functions (easier), or developing a framework that allows us
> to write tests directly in terms of XMPP stanzas sent and received (harder,
> but I think a bigger win). I imagine the second option involving having a
> directory containing text files, where each file is a test consisting of a
> trace of stanzas sent and received, similar to what you can do with Python
> doctests. The format might be something like:
>
> -> <iq type="get" from="bob at foo.org" to="gadget at bar.org">
> -> ...
> -> </iq>
>
> <- <iq type="result" from="gadget at bar.org" to="bob at foo.org">
> <- ...
> <- </iq>
>
> The main difficulty I see with this approach is that it's harder to set up
> specific scenarios, but perhaps we can allow inserting Python code as well.
>
> >>> model.buddy_add(...)
> >>> model.activity_add(...)
>
> -> <iq type="get" from="bob at foo.org" to="gadget at bar.org">
> ...
>
> Another option would be to just use doctests, with some convenience code for
> sending/receiving stanzas.
>
> >>> send('''
> ... <iq type="get" from="bob at foo.org" to="gadget at bar.org">
> ... ...
> ... </iq>''')
>
> >>> print recv()
> <iq type="result" from="gadget at bar.org" to="bob at foo.org">
> ...
> </iq>
>
I totally agree, having "XML oriented" test would be a big win. But I
think we could do that as as separated branch so this test refactoring
won't block the merge.
Thanks for your review,
G.
More information about the Sugar
mailing list