1
2"""
3Infrastructure code for testing Salut
4"""
5
6import os
7import sys
8import time
9import re
10from subprocess import Popen
11
12import servicetest
13from servicetest import call_async, EventPattern, Event, unwrap
14from twisted.internet import reactor
15import constants as cs
16from twisted.words.protocols.jabber.client import IQ
17from twisted.words.xish import domish, xpath
18import ns
19
20import dbus
21import glib
22
23# keep sync with src/capabilities.c:self_advertised_features
24fixed_features = [ns.SI, ns.TUBES, ns.IQ_OOB, ns.X_OOB, ns.TP_FT_METADATA]
25
26def make_result_iq(iq):
27    result = IQ(None, "result")
28    result["id"] = iq["id"]
29    query = iq.firstChildElement()
30
31    if query:
32        result.addElement((query.uri, query.name))
33
34    return result
35
36def sync_stream(q, xmpp_connection):
37    """Used to ensure that Salut has processed all stanzas sent to it on this
38       xmpp_connection."""
39
40    iq = IQ(None, "get")
41    iq.addElement(('http://jabber.org/protocol/disco#info', 'query'))
42    xmpp_connection.send(iq)
43    q.expect('stream-iq', query_ns='http://jabber.org/protocol/disco#info')
44
45def make_connection(bus, event_func, params=None):
46    default_params = {
47        'published-name': 'testsuite',
48        'first-name': 'test',
49        'last-name': 'suite',
50        'nickname': re.sub('(.*tests/twisted/|\./)', '', sys.argv[0]),
51        }
52
53    if params:
54        default_params.update(params)
55
56    return servicetest.make_connection(bus, event_func, 'salut',
57        'local-xmpp', default_params)
58
59def ensure_avahi_is_running():
60    bus = dbus.SystemBus()
61    bus_obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
62    if bus_obj.NameHasOwner('org.freedesktop.Avahi',
63                            dbus_interface='org.freedesktop.DBus'):
64        return
65
66    loop = glib.MainLoop()
67    def name_owner_changed_cb(name, old_owner, new_owner):
68        loop.quit()
69
70    noc = bus.add_signal_receiver(name_owner_changed_cb,
71                                  signal_name='NameOwnerChanged',
72                                  dbus_interface='org.freedesktop.DBus',
73                                  arg0='org.freedesktop.Avahi')
74
75    # Cannot use D-Bus activation because we have no way to pass to activated
76    # clients the address of the system bus and we cannot host the service in
77    # this process because we are going to make blocking calls and we would
78    # deadlock.
79    tests_dir = os.path.dirname(__file__)
80    avahimock_path = os.path.join(tests_dir, 'avahimock.py')
81    Popen([avahimock_path])
82
83    loop.run()
84
85    noc.remove()
86
87def exec_test_deferred (fun, params, protocol=None, timeout=None,
88        make_conn=True):
89    colourer = None
90
91    if 'SALUT_TEST_REAL_AVAHI' not in os.environ:
92        ensure_avahi_is_running()
93
94    if sys.stdout.isatty() or 'CHECK_FORCE_COLOR' in os.environ:
95        colourer = servicetest.install_colourer()
96
97    bus = dbus.SessionBus()
98
99    queue = servicetest.IteratingEventQueue(timeout)
100    queue.verbose = (
101        os.environ.get('CHECK_TWISTED_VERBOSE', '') != ''
102        or '-v' in sys.argv)
103
104    if make_conn:
105        try:
106            conn = make_connection(bus, queue.append, params)
107        except Exception, e:
108            # This is normally because the connection's still kicking around
109            # on the bus from a previous test. Let's bail out unceremoniously.
110            print e
111            os._exit(1)
112    else:
113        conn = None
114
115    def signal_receiver(*args, **kw):
116        queue.append(Event('dbus-signal',
117                           path=unwrap(kw['path']),
118                           signal=kw['member'], args=map(unwrap, args),
119                           interface=kw['interface']))
120
121    bus.add_signal_receiver(
122        signal_receiver,
123        None,       # signal name
124        None,       # interface
125        None,
126        path_keyword='path',
127        member_keyword='member',
128        interface_keyword='interface',
129        byte_arrays=True
130        )
131
132    error = None
133
134    try:
135        fun(queue, bus, conn)
136    except Exception, e:
137        import traceback
138        traceback.print_exc()
139        error = e
140        queue.verbose = False
141
142    if colourer:
143        sys.stdout = colourer.fh
144
145    if bus.name_has_owner(conn.object.bus_name):
146        # Connection hasn't already been disconnected and destroyed
147        try:
148            if conn.GetStatus() == cs.CONN_STATUS_CONNECTED:
149                # Connection is connected, properly disconnect it
150                call_async(queue, conn, 'Disconnect')
151                queue.expect_many(EventPattern('dbus-signal', signal='StatusChanged',
152                                           args=[cs.CONN_STATUS_DISCONNECTED, cs.CSR_REQUESTED]),
153                                  EventPattern('dbus-return', method='Disconnect'))
154            else:
155                # Connection is not connected, call Disconnect() to destroy it
156                conn.Disconnect()
157        except dbus.DBusException, e:
158            pass
159
160        try:
161            conn.Disconnect()
162            raise AssertionError("Connection didn't disappear; "
163                                 "all subsequent tests will probably fail")
164        except dbus.DBusException, e:
165            pass
166        except Exception, e:
167            traceback.print_exc()
168            error = e
169
170    if error is None:
171        reactor.callLater(0, reactor.crash)
172    else:
173        # please ignore the POSIX behind the curtain
174        os._exit(1)
175
176    if 'SALUT_TEST_REFDBG' in os.environ:
177        # we have to wait that Salut timeouts so the process is properly
178        # exited and refdbg can generates its report
179        time.sleep(5.5)
180
181def exec_test(fun, params=None, protocol=None, timeout=None,
182        make_conn=True):
183  reactor.callWhenRunning (exec_test_deferred, fun, params, protocol, timeout,
184          make_conn)
185  reactor.run()
186
187def wait_for_contact_list(q, conn):
188    """Request contact list channels and wait for their NewChannel signals.
189    This is useful to avoid these signals to interfere with your test."""
190
191    #FIXME: this maybe racy if there are other contacts connected
192    requestotron = dbus.Interface(conn, cs.CONN_IFACE_REQUESTS)
193
194    # publish
195    requestotron.EnsureChannel({
196        cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST,
197        cs.TARGET_HANDLE_TYPE: cs.HT_LIST,
198        cs.TARGET_ID: 'publish'})
199    q.expect('dbus-signal', signal='NewChannel')
200    # subscribe
201    requestotron.EnsureChannel({
202        cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST,
203        cs.TARGET_HANDLE_TYPE: cs.HT_LIST,
204        cs.TARGET_ID: 'subscribe'})
205    q.expect('dbus-signal', signal='NewChannel')
206
207def wait_for_contact_in_publish(q, bus, conn, contact_name):
208    publish_handle = conn.RequestHandles(cs.HT_LIST, ["publish"])[0]
209    publish = conn.RequestChannel(cs.CHANNEL_TYPE_CONTACT_LIST,
210        cs.HT_LIST, publish_handle, False)
211
212    handle = 0
213    # Wait until the record shows up in publish
214    while handle == 0:
215        e = q.expect('dbus-signal', signal='MembersChangedDetailed',
216                path=publish)
217        # Versions of telepathy-glib prior to 0.14.6 incorrectly used the name
218        # 'member-ids'.
219        try:
220            ids = e.args[4]['contact-ids']
221        except KeyError:
222            ids = e.args[4]['member-ids']
223
224        for h in e.args[0]:
225            name = ids[h]
226            if name == contact_name:
227                handle = h
228
229    return handle
230
231def _elem_add(elem, *children):
232    for child in children:
233        if isinstance(child, domish.Element):
234            elem.addChild(child)
235        elif isinstance(child, unicode):
236            elem.addContent(child)
237        else:
238            raise ValueError(
239                'invalid child object %r (must be element or unicode)', child)
240
241def elem(a, b=None, attrs={}, **kw):
242    r"""
243    >>> elem('foo')().toXml()
244    u'<foo/>'
245    >>> elem('foo', x='1')().toXml()
246    u"<foo x='1'/>"
247    >>> elem('foo', x='1')(u'hello').toXml()
248    u"<foo x='1'>hello</foo>"
249    >>> elem('foo', x='1')(u'hello',
250    ...         elem('http://foo.org', 'bar', y='2')(u'bye')).toXml()
251    u"<foo x='1'>hello<bar xmlns='http://foo.org' y='2'>bye</bar></foo>"
252    >>> elem('foo', attrs={'xmlns:bar': 'urn:bar', 'bar:cake': 'yum'})(
253    ...   elem('bar:e')(u'i')
254    ... ).toXml()
255    u"<foo xmlns:bar='urn:bar' bar:cake='yum'><bar:e>i</bar:e></foo>"
256    """
257
258    class _elem(domish.Element):
259        def __call__(self, *children):
260            _elem_add(self, *children)
261            return self
262
263    if b is not None:
264        elem = _elem((a, b))
265    else:
266        elem = _elem((None, a))
267
268    # Can't just update kw into attrs, because that *modifies the parameter's
269    # default*. Thanks python.
270    allattrs = {}
271    allattrs.update(kw)
272    allattrs.update(attrs)
273
274    # First, let's pull namespaces out
275    realattrs = {}
276    for k, v in allattrs.iteritems():
277        if k.startswith('xmlns:'):
278            abbr = k[len('xmlns:'):]
279            elem.localPrefixes[abbr] = v
280        else:
281            realattrs[k] = v
282
283    for k, v in realattrs.iteritems():
284        if k == 'from_':
285            elem['from'] = v
286        else:
287            elem[k] = v
288
289    return elem
290
291def elem_iq(server, type, **kw):
292    class _iq(IQ):
293        def __call__(self, *children):
294            _elem_add(self, *children)
295            return self
296
297    iq = _iq(server, type)
298
299    for k, v in kw.iteritems():
300        if k == 'from_':
301            iq['from'] = v
302        else:
303            iq[k] = v
304
305    return iq
306
307def make_presence(_from, to, type=None, show=None,
308        status=None, caps=None, photo=None):
309    presence = domish.Element((None, 'presence'))
310    presence['from'] = _from
311    presence['to'] = to
312
313    if type is not None:
314        presence['type'] = type
315
316    if show is not None:
317        presence.addElement('show', content=show)
318
319    if status is not None:
320        presence.addElement('status', content=status)
321
322    if caps is not None:
323        cel = presence.addElement(('http://jabber.org/protocol/caps', 'c'))
324        for key,value in caps.items():
325            cel[key] = value
326
327    # <x xmlns="vcard-temp:x:update"><photo>4a1...</photo></x>
328    if photo is not None:
329        x = presence.addElement((ns.VCARD_TEMP_UPDATE, 'x'))
330        x.addElement('photo').addContent(photo)
331
332    return presence
333