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