summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonny Lamb <jonny.lamb@collabora.co.uk>2011-09-06 14:45:21 +0100
committerJonny Lamb <jonny.lamb@collabora.co.uk>2011-09-07 14:56:56 +0100
commit0b24adad71ac6d5fe256f4cd852cadb2c129fd4d (patch)
tree0986ebfba884972f78fdbe93f157df44129a8413
parent61ef28a7b70026a55eb1cb0401541e9708246bf0 (diff)
tests: add start of gabble test suite
Signed-off-by: Jonny Lamb <jonny.lamb@collabora.co.uk>
-rw-r--r--.gitignore3
-rw-r--r--configure.ac2
-rw-r--r--tests/twisted/Makefile.am11
-rw-r--r--tests/twisted/gabble/success.py19
-rw-r--r--tests/twisted/gabblecaps_helper.py356
-rw-r--r--tests/twisted/gabbleconstants.py458
-rw-r--r--tests/twisted/gabbleservicetest.py640
-rw-r--r--tests/twisted/gabbletest.py824
-rw-r--r--tests/twisted/tools/Makefile.am10
-rw-r--r--tests/twisted/tools/gabble-exec-with-log.sh.in40
-rw-r--r--tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in3
11 files changed, 2361 insertions, 5 deletions
diff --git a/.gitignore b/.gitignore
index 8371169..3d5763b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,12 +32,15 @@ stamp-h1
/tests/twisted/tools/exec-with-log.sh
/tests/twisted/tools/salut-exec-with-log.sh
+/tests/twisted/tools/gabble-exec-with-log.sh
/tests/twisted/tools/org.freedesktop.Telepathy.Client.Logger.service
/tests/twisted/tools/org.freedesktop.Telepathy.MissionControl5.service
/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.salut.service
+/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service
/tests/twisted/tools/tmp-session-bus.conf
/tests/twisted/tools/missioncontrol*.log
/tests/twisted/tools/salut-testing.log
+/tests/twisted/tools/gabble-testing.log
/tests/twisted/tmp-*/
/tests/twisted/config.py
/tests/twisted/with-session-bus*
diff --git a/configure.ac b/configure.ac
index c7feb23..ca71e49 100644
--- a/configure.ac
+++ b/configure.ac
@@ -137,8 +137,10 @@ AC_OUTPUT([
tests/twisted/tools/Makefile
tests/twisted/tools/exec-with-log.sh
tests/twisted/tools/salut-exec-with-log.sh
+ tests/twisted/tools/gabble-exec-with-log.sh
tests/twisted/tools/tmp-session-bus.conf
tests/twisted/tools/org.freedesktop.Telepathy.MissionControl5.service
tests/twisted/tools/org.freedesktop.Telepathy.Client.Logger.service
tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.salut.service
+ tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service
])
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am
index 78175e5..a47cf59 100644
--- a/tests/twisted/Makefile.am
+++ b/tests/twisted/Makefile.am
@@ -46,7 +46,9 @@ BASIC_TESTS_ENVIRONMENT = \
XDG_CACHE_HOME=@abs_top_builddir@/tests/twisted/tmp-$(TMPSUFFIX) \
G_DEBUG=fatal_criticals \
SALUT_PLUGIN_DIR=@abs_top_builddir@/salut/.libs \
- SALUT_TEST_BACKTRACE=1
+ SALUT_TEST_BACKTRACE=1 \
+ GABBLE_PLUGIN_DIR=@abs_top_builddir@/gabble/.libs \
+ GABBLE_TEST_BACKTRACE=1
WITH_SESSION_BUS = \
sh $(srcdir)/tools/with-session-bus.sh \
@@ -63,6 +65,7 @@ check-twisted:
rm -f tools/core
rm -f tools/missioncontrol-*.log
rm -f tools/salut-testing.log
+ rm -f tools/gabble-testing.log
mkdir tmp-$$$$ && { \
$(MAKE) check-combined TMPSUFFIX=$$$$; \
e=$$?; \
@@ -143,7 +146,11 @@ EXTRA_DIST = \
avahitest.py \
xmppstream.py \
ipv6.py \
- caps_helper.py
+ caps_helper.py \
+ gabbleservicetest.py \
+ gabbletest.py \
+ gabbleconstants.py \
+ gabblecaps_helper.py
CLEANFILES = \
accounts/accounts.cfg \
diff --git a/tests/twisted/gabble/success.py b/tests/twisted/gabble/success.py
new file mode 100644
index 0000000..13ff34c
--- /dev/null
+++ b/tests/twisted/gabble/success.py
@@ -0,0 +1,19 @@
+
+"""
+Test connecting to a server.
+"""
+
+from gabbletest import exec_test
+import gabbleconstants as cs
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged', args=[cs.CONN_STATUS_CONNECTING, cs.CSR_REQUESTED])
+ q.expect('stream-authenticated')
+ q.expect('dbus-signal', signal='PresenceUpdate',
+ args=[{1L: (0L, {u'available': {}})}])
+ q.expect('dbus-signal', signal='StatusChanged', args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+if __name__ == '__main__':
+ exec_test(test, do_connect=False)
+
diff --git a/tests/twisted/gabblecaps_helper.py b/tests/twisted/gabblecaps_helper.py
new file mode 100644
index 0000000..6f80ba1
--- /dev/null
+++ b/tests/twisted/gabblecaps_helper.py
@@ -0,0 +1,356 @@
+# vim: set fileencoding=utf-8 :
+import hashlib
+import base64
+import dbus
+
+from twisted.words.xish import domish, xpath
+from gabbletest import make_result_iq, make_presence, elem_iq, elem
+from gabbleservicetest import (
+ EventPattern,
+ assertEquals, assertContains, assertDoesNotContain, assertLength,
+ )
+
+import config
+import ns
+import gabbleconstants as cs
+
+# The caps we always have, regardless of any clients' caps
+FIXED_CAPS = [
+ ns.JINGLE,
+ ns.JINGLE_015,
+ ns.GOOGLE_FEAT_SESSION,
+ ns.JINGLE_TRANSPORT_RAWUDP,
+ ns.NICK,
+ ns.NICK + '+notify',
+ ns.CHAT_STATES,
+ ns.SI,
+ ns.IBB,
+ ns.BYTESTREAMS,
+ ]
+
+JINGLE_CAPS = [
+ # Additional Jingle transports
+ ns.JINGLE_TRANSPORT_ICEUDP,
+ ns.GOOGLE_P2P,
+ # Jingle content types
+ ns.GOOGLE_FEAT_VOICE,
+ ns.GOOGLE_FEAT_VIDEO,
+ ns.JINGLE_015_AUDIO,
+ ns.JINGLE_015_VIDEO,
+ ns.JINGLE_RTP,
+ ns.JINGLE_RTP_AUDIO,
+ ns.JINGLE_RTP_VIDEO,
+ ]
+
+VARIABLE_CAPS = (
+ JINGLE_CAPS +
+ [
+ ns.FILE_TRANSFER,
+
+ # FIXME: currently we always advertise these, but in future we should
+ # only advertise them if >= 1 client supports them:
+ # ns.TUBES,
+
+ # there is an unlimited set of these; only the ones actually relevant to
+ # the tests so far are shown here
+ ns.TUBES + '/stream#x-abiword',
+ ns.TUBES + '/stream#daap',
+ ns.TUBES + '/stream#http',
+ ns.TUBES + '/dbus#com.example.Go',
+ ns.TUBES + '/dbus#com.example.Xiangqi',
+ ])
+
+def check_caps(namespaces, desired):
+ """Assert that all the FIXED_CAPS are supported, and of the VARIABLE_CAPS,
+ every capability in desired is supported, and every other capability is
+ not.
+ """
+ for c in FIXED_CAPS:
+ assertContains(c, namespaces)
+
+ for c in VARIABLE_CAPS:
+ if c in desired:
+ assertContains(c, namespaces)
+ else:
+ assertDoesNotContain(c, namespaces)
+
+text_fixed_properties = dbus.Dictionary({
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT
+ })
+text_allowed_properties = dbus.Array([cs.TARGET_HANDLE])
+
+stream_tube_fixed_properties = dbus.Dictionary({
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_STREAM_TUBE
+ })
+stream_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE,
+ cs.TARGET_ID, cs.STREAM_TUBE_SERVICE])
+
+dbus_tube_fixed_properties = dbus.Dictionary({
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_DBUS_TUBE
+ })
+dbus_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE,
+ cs.TARGET_ID, cs.DBUS_TUBE_SERVICE_NAME])
+
+ft_fixed_properties = dbus.Dictionary({
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER,
+ })
+ft_allowed_properties = dbus.Array([
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHashType',
+ cs.TARGET_HANDLE,
+ cs.TARGET_ID,
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentType',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.Filename',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.Size',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHash',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.Description',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.Date',
+ cs.FT_URI])
+
+fake_client_dataforms = {
+ 'urn:xmpp:dataforms:softwareinfo':
+ {'software': ['A Fake Client with Twisted'],
+ 'software_version': ['5.11.2-svn-20080512'],
+ 'os': ['Debian GNU/Linux unstable (sid) unstable sid'],
+ 'os_version': ['2.6.24-1-amd64'],
+ },
+}
+
+def compute_caps_hash(identities, features, dataforms):
+ """
+ Accepts a list of slash-separated identities, a list of feature namespaces,
+ and a map from FORM_TYPE to (map from field name to values), returns the
+ verification string as defined by
+ <http://xmpp.org/extensions/xep-0115.html#ver>.
+ """
+ components = []
+
+ for identity in sorted(identities):
+ if len(identity.split('/')) != 4:
+ raise ValueError(
+ "expecting identities of the form " +
+ "'category/type/lang/client': got " + repr(identity))
+
+ components.append(identity)
+
+ for feature in sorted(features):
+ components.append(feature)
+
+ for form_type in sorted(dataforms.keys()):
+ components.append(form_type)
+
+ for var in sorted(dataforms[form_type].keys()):
+ components.append(var)
+
+ for value in sorted(dataforms[form_type][var]):
+ components.append(value)
+
+ components.append('')
+
+ m = hashlib.sha1()
+ S = u'<'.join(components)
+ m.update(S.encode('utf-8'))
+ return base64.b64encode(m.digest())
+
+def make_caps_disco_reply(stream, req, identities, features, dataforms={}):
+ iq = make_result_iq(stream, req)
+ query = iq.firstChildElement()
+
+ for identity in identities:
+ category, type_, lang, name = identity.split('/')
+ el = query.addElement('identity')
+ el['category'] = category
+ el['type'] = type_
+ el['name'] = name
+ el['xml:lang'] = lang
+
+ for f in features:
+ el = domish.Element((None, 'feature'))
+ el['var'] = f
+ query.addChild(el)
+
+ for type, fields in dataforms.iteritems():
+ x = query.addElement((ns.X_DATA, 'x'))
+ x['type'] = 'result'
+
+ field = x.addElement('field')
+ field['var'] = 'FORM_TYPE'
+ field['type'] = 'hidden'
+ field.addElement('value', content=type)
+
+ for var, values in fields.iteritems():
+ field = x.addElement('field')
+ field['var'] = var
+
+ for value in values:
+ field.addElement('value', content=value)
+
+ return iq
+
+def receive_presence_and_ask_caps(q, stream, expect_dbus=True):
+ # receive presence stanza
+ if expect_dbus:
+ presence, event_dbus = q.expect_many(
+ EventPattern('stream-presence'),
+ EventPattern('dbus-signal', signal='ContactCapabilitiesChanged')
+ )
+ assertLength(1, event_dbus.args)
+ signaled_caps = event_dbus.args[0]
+ else:
+ presence = q.expect('stream-presence')
+ signaled_caps = None
+
+ return disco_caps(q, stream, presence) + (signaled_caps,)
+
+def extract_disco_parts(stanza):
+ identity_nodes = xpath.queryForNodes('/iq/query/identity', stanza)
+ assertLength(1, identity_nodes)
+ identity_node = identity_nodes[0]
+
+ assertEquals('client', identity_node['category'])
+ assertDoesNotContain('xml:lang', identity_node.attributes)
+
+ identity = 'client/%s//%s' % (identity_node['type'], identity_node['name'])
+
+ features = []
+ for feature in xpath.queryForNodes('/iq/query/feature', stanza):
+ features.append(feature['var'])
+
+ # a quick and ugly data form extractor
+ x_nodes = xpath.queryForNodes('/iq/query/x', stanza) or []
+ dataforms = {}
+ for form in x_nodes:
+ name = None
+ fields = {}
+ for field in xpath.queryForNodes('/x/field', form):
+ if field['var'] == 'FORM_TYPE':
+ name = str(field.firstChildElement())
+ else:
+ values = [str(x) for x in xpath.queryForNodes('/field/value', field)]
+
+ fields[field['var']] = values
+
+ if name is not None:
+ dataforms[name] = fields
+
+ return ([identity], features, dataforms)
+
+def disco_caps(q, stream, presence):
+ c_nodes = xpath.queryForNodes('/presence/c', presence.stanza)
+ assert c_nodes is not None
+ assertLength(1, c_nodes)
+ hash = c_nodes[0].attributes['hash']
+ ver = c_nodes[0].attributes['ver']
+ node = c_nodes[0].attributes['node']
+ assertEquals('sha-1', hash)
+
+ # ask caps
+ request = \
+ elem_iq(stream, 'get', from_='fake_contact@jabber.org/resource')(
+ elem(ns.DISCO_INFO, 'query', node=(node + '#' + ver))
+ )
+ stream.send(request)
+
+ # receive caps
+ event = q.expect('stream-iq', query_ns=ns.DISCO_INFO, iq_id=request['id'])
+
+ # Check that Gabble's announcing the identity we think it should be.
+ (identities, features, dataforms) = extract_disco_parts(event.stanza)
+
+ # Check if the hash matches the announced capabilities
+ assertEquals(compute_caps_hash(identities, features, dataforms), ver)
+
+ return (event, features, dataforms)
+
+def caps_contain(event, cap):
+ node = xpath.queryForNodes('/iq/query/feature[@var="%s"]'
+ % cap,
+ event.stanza)
+ if node is None:
+ return False
+ if len(node) != 1:
+ return False
+ var = node[0].attributes['var']
+ if var is None:
+ return False
+ return var == cap
+
+def presence_and_disco(q, conn, stream, contact, disco,
+ client, caps,
+ features, identities=[], dataforms={},
+ initial=True, show=None):
+ h = send_presence(q, conn, stream, contact, caps, initial=initial,
+ show=show)
+
+ if disco:
+ stanza = expect_disco(q, contact, client, caps)
+ send_disco_reply(stream, stanza, identities, features, dataforms)
+
+ return h
+
+def send_presence(q, conn, stream, contact, caps, initial=True, show=None):
+ h = conn.RequestHandles(cs.HT_CONTACT, [contact])[0]
+
+ if initial:
+ stream.send(make_presence(contact, status='hello'))
+
+ q.expect_many(
+ EventPattern('dbus-signal', signal='PresenceUpdate',
+ args=[{h:
+ (0L, {u'available': {'message': 'hello'}})}]),
+ EventPattern('dbus-signal', signal='PresencesChanged',
+ args=[{h:
+ (2, u'available', 'hello')}]))
+
+ # no special capabilities
+ assertEquals([(h, cs.CHANNEL_TYPE_TEXT, 3, 0)],
+ conn.Capabilities.GetCapabilities([h]))
+
+ # send updated presence with caps info
+ stream.send(make_presence(contact, show=show, status='hello', caps=caps))
+
+ return h
+
+def expect_disco(q, contact, client, caps):
+ # Gabble looks up our capabilities
+ event = q.expect('stream-iq', to=contact, query_ns=ns.DISCO_INFO)
+ assertEquals(client + '#' + caps['ver'], event.query['node'])
+
+ return event.stanza
+
+def send_disco_reply(stream, stanza, identities, features, dataforms={}):
+ stream.send(
+ make_caps_disco_reply(stream, stanza, identities, features, dataforms))
+
+if __name__ == '__main__':
+ # example from XEP-0115
+ assertEquals('QgayPKawpkPSDYmwT/WM94uAlu0=',
+ compute_caps_hash(['client/pc//Exodus 0.9.1'],
+ ["http://jabber.org/protocol/disco#info",
+ "http://jabber.org/protocol/disco#items",
+ "http://jabber.org/protocol/muc",
+ "http://jabber.org/protocol/caps"],
+ {}))
+
+ # another example from XEP-0115
+ identities = [u'client/pc/en/Psi 0.11', u'client/pc/el/Ψ 0.11']
+ features = [
+ u'http://jabber.org/protocol/caps',
+ u'http://jabber.org/protocol/disco#info',
+ u'http://jabber.org/protocol/disco#items',
+ u'http://jabber.org/protocol/muc',
+ ]
+ dataforms = {
+ u'urn:xmpp:dataforms:softwareinfo':
+ { u'ip_version': [u'ipv4', u'ipv6'],
+ u'os': [u'Mac'],
+ u'os_version': [u'10.5.1'],
+ u'software': [u'Psi'],
+ u'software_version': [u'0.11'],
+ },
+ }
+ assertEquals('q07IKJEyjvHSyhy//CH0CxmKi8w=',
+ compute_caps_hash(identities, features, dataforms))
diff --git a/tests/twisted/gabbleconstants.py b/tests/twisted/gabbleconstants.py
new file mode 100644
index 0000000..aff01d1
--- /dev/null
+++ b/tests/twisted/gabbleconstants.py
@@ -0,0 +1,458 @@
+"""
+Some handy constants for other tests to share and enjoy.
+"""
+
+from dbus import PROPERTIES_IFACE
+
+CM = "org.freedesktop.Telepathy.ConnectionManager"
+
+HT_NONE = 0
+HT_CONTACT = 1
+HT_ROOM = 2
+HT_LIST = 3
+HT_GROUP = 4
+
+CHANNEL = "org.freedesktop.Telepathy.Channel"
+
+CHANNEL_IFACE_CALL_STATE = CHANNEL + ".Interface.CallState"
+CHANNEL_IFACE_CHAT_STATE = CHANNEL + '.Interface.ChatState'
+CHANNEL_IFACE_DESTROYABLE = CHANNEL + ".Interface.Destroyable"
+CHANNEL_IFACE_DTMF = CHANNEL + ".Interface.DTMF"
+CHANNEL_IFACE_GROUP = CHANNEL + ".Interface.Group"
+CHANNEL_IFACE_HOLD = CHANNEL + ".Interface.Hold"
+CHANNEL_IFACE_MEDIA_SIGNALLING = CHANNEL + ".Interface.MediaSignalling"
+CHANNEL_IFACE_MESSAGES = CHANNEL + ".Interface.Messages"
+CHANNEL_IFACE_PASSWORD = CHANNEL + ".Interface.Password"
+CHANNEL_IFACE_TUBE = CHANNEL + ".Interface.Tube"
+CHANNEL_IFACE_SASL_AUTH = CHANNEL + ".Interface.SASLAuthentication"
+CHANNEL_IFACE_CONFERENCE = CHANNEL + '.Interface.Conference'
+CHANNEL_IFACE_ROOM = CHANNEL + '.Interface.Room.DRAFT'
+
+CHANNEL_TYPE_CALL = CHANNEL + ".Type.Call.DRAFT"
+CHANNEL_TYPE_CONTACT_LIST = CHANNEL + ".Type.ContactList"
+CHANNEL_TYPE_CONTACT_SEARCH = CHANNEL + ".Type.ContactSearch"
+CHANNEL_TYPE_TEXT = CHANNEL + ".Type.Text"
+CHANNEL_TYPE_TUBES = CHANNEL + ".Type.Tubes"
+CHANNEL_TYPE_STREAM_TUBE = CHANNEL + ".Type.StreamTube"
+CHANNEL_TYPE_DBUS_TUBE = CHANNEL + ".Type.DBusTube"
+CHANNEL_TYPE_STREAMED_MEDIA = CHANNEL + ".Type.StreamedMedia"
+CHANNEL_TYPE_TEXT = CHANNEL + ".Type.Text"
+CHANNEL_TYPE_FILE_TRANSFER = CHANNEL + ".Type.FileTransfer"
+CHANNEL_TYPE_SERVER_AUTHENTICATION = \
+ CHANNEL + ".Type.ServerAuthentication"
+CHANNEL_TYPE_SERVER_TLS_CONNECTION = \
+ CHANNEL + ".Type.ServerTLSConnection"
+
+TP_AWKWARD_PROPERTIES = "org.freedesktop.Telepathy.Properties"
+PROPERTY_FLAG_READ = 1
+PROPERTY_FLAG_WRITE = 2
+PROPERTY_FLAGS_RW = PROPERTY_FLAG_READ | PROPERTY_FLAG_WRITE
+
+CHANNEL_TYPE = CHANNEL + '.ChannelType'
+TARGET_HANDLE_TYPE = CHANNEL + '.TargetHandleType'
+TARGET_HANDLE = CHANNEL + '.TargetHandle'
+TARGET_ID = CHANNEL + '.TargetID'
+REQUESTED = CHANNEL + '.Requested'
+INITIATOR_HANDLE = CHANNEL + '.InitiatorHandle'
+INITIATOR_ID = CHANNEL + '.InitiatorID'
+INTERFACES = CHANNEL + '.Interfaces'
+
+INITIAL_AUDIO = CHANNEL_TYPE_STREAMED_MEDIA + '.InitialAudio'
+INITIAL_VIDEO = CHANNEL_TYPE_STREAMED_MEDIA + '.InitialVideo'
+IMMUTABLE_STREAMS = CHANNEL_TYPE_STREAMED_MEDIA + '.ImmutableStreams'
+
+CALL_INITIAL_AUDIO = CHANNEL_TYPE_CALL + '.InitialAudio'
+CALL_INITIAL_AUDIO_NAME = CHANNEL_TYPE_CALL + '.InitialAudioName'
+CALL_INITIAL_VIDEO = CHANNEL_TYPE_CALL + '.InitialVideo'
+CALL_INITIAL_VIDEO_NAME = CHANNEL_TYPE_CALL + '.InitialVideoName'
+CALL_MUTABLE_CONTENTS = CHANNEL_TYPE_CALL + '.MutableContents'
+
+CALL_CONTENT = 'org.freedesktop.Telepathy.Call.Content.DRAFT'
+CALL_CONTENT_IFACE_MEDIA = \
+ 'org.freedesktop.Telepathy.Call.Content.Interface.Media.DRAFT'
+
+CALL_CONTENT_CODECOFFER = \
+ 'org.freedesktop.Telepathy.Call.Content.CodecOffer.DRAFT'
+
+CALL_STREAM = 'org.freedesktop.Telepathy.Call.Stream.DRAFT'
+CALL_STREAM_IFACE_MEDIA = \
+ 'org.freedesktop.Telepathy.Call.Stream.Interface.Media.DRAFT'
+
+CALL_STREAM_ENDPOINT = 'org.freedesktop.Telepathy.Call.Stream.Endpoint.DRAFT'
+
+CALL_MEDIA_TYPE_AUDIO = 0
+CALL_MEDIA_TYPE_VIDEO = 1
+
+CALL_CONTENT_PACKETIZATION_RTP = 0
+CALL_CONTENT_PACKETIZATION_RAW = 1
+CALL_CONTENT_PACKETIZATION_MSN_WEBCAM = 2
+
+CALL_STREAM_TRANSPORT_RAW_UDP = 1
+CALL_STREAM_TRANSPORT_ICE = 2
+CALL_STREAM_TRANSPORT_GOOGLE = 3
+
+CALL_STATE_UNKNOWN = 0
+CALL_STATE_PENDING_INITIATOR = 1
+CALL_STATE_PENDING_RECEIVER = 2
+CALL_STATE_ACCEPTED = 3
+CALL_STATE_ENDED = 4
+
+CALL_MEMBER_FLAG_RINGING = 1
+CALL_MEMBER_FLAG_HELD = 2
+
+CALL_DISPOSITION_NONE = 0
+CALL_DISPOSITION_INITIAL = 1
+
+CALL_SENDING_STATE_NONE = 0
+CALL_SENDING_STATE_PENDING_SEND = 1
+CALL_SENDING_STATE_SENDING = 2
+
+SUBSCRIPTION_STATE_UNKNOWN = 0
+SUBSCRIPTION_STATE_NO = 1
+SUBSCRIPTION_STATE_REMOVED_REMOTELY = 2
+SUBSCRIPTION_STATE_ASK = 3
+SUBSCRIPTION_STATE_YES = 4
+
+CONTACT_LIST_STATE_NONE = 0
+CONTACT_LIST_STATE_WAITING = 1
+CONTACT_LIST_STATE_FAILURE = 2
+CONTACT_LIST_STATE_SUCCESS = 3
+
+CONN = "org.freedesktop.Telepathy.Connection"
+CONN_IFACE_AVATARS = CONN + '.Interface.Avatars'
+CONN_IFACE_ALIASING = CONN + '.Interface.Aliasing'
+CONN_IFACE_CAPS = CONN + '.Interface.Capabilities'
+CONN_IFACE_CONTACTS = CONN + '.Interface.Contacts'
+CONN_IFACE_CONTACT_CAPS = CONN + '.Interface.ContactCapabilities'
+CONN_IFACE_CONTACT_INFO = CONN + ".Interface.ContactInfo"
+CONN_IFACE_PRESENCE = CONN + '.Interface.Presence'
+CONN_IFACE_SIMPLE_PRESENCE = CONN + '.Interface.SimplePresence'
+CONN_IFACE_REQUESTS = CONN + '.Interface.Requests'
+CONN_IFACE_LOCATION = CONN + '.Interface.Location'
+CONN_IFACE_GABBLE_DECLOAK = CONN + '.Interface.Gabble.Decloak'
+CONN_IFACE_MAIL_NOTIFICATION = CONN + '.Interface.MailNotification'
+CONN_IFACE_CONTACT_LIST = CONN + '.Interface.ContactList'
+CONN_IFACE_CONTACT_GROUPS = CONN + '.Interface.ContactGroups'
+CONN_IFACE_CLIENT_TYPES = CONN + '.Interface.ClientTypes'
+CONN_IFACE_POWER_SAVING = CONN + '.Interface.PowerSaving'
+
+ATTR_CONTACT_CAPABILITIES = CONN_IFACE_CONTACT_CAPS + '/capabilities'
+
+STREAM_HANDLER = 'org.freedesktop.Telepathy.Media.StreamHandler'
+
+ERROR = 'org.freedesktop.Telepathy.Error'
+INVALID_ARGUMENT = ERROR + '.InvalidArgument'
+NOT_IMPLEMENTED = ERROR + '.NotImplemented'
+NOT_AVAILABLE = ERROR + '.NotAvailable'
+PERMISSION_DENIED = ERROR + '.PermissionDenied'
+OFFLINE = ERROR + '.Offline'
+NOT_CAPABLE = ERROR + '.NotCapable'
+CONNECTION_REFUSED = ERROR + '.ConnectionRefused'
+CONNECTION_FAILED = ERROR + '.ConnectionFailed'
+CONNECTION_LOST = ERROR + '.ConnectionLost'
+CANCELLED = ERROR + '.Cancelled'
+DISCONNECTED = ERROR + '.Disconnected'
+REGISTRATION_EXISTS = ERROR + '.RegistrationExists'
+AUTHENTICATION_FAILED = ERROR + '.AuthenticationFailed'
+CONNECTION_REPLACED = ERROR + '.ConnectionReplaced'
+ALREADY_CONNECTED = ERROR + '.AlreadyConnected'
+NETWORK_ERROR = ERROR + '.NetworkError'
+NOT_YET = ERROR + '.NotYet'
+INVALID_HANDLE = ERROR + '.InvalidHandle'
+CERT_UNTRUSTED = ERROR + '.Cert.Untrusted'
+SERVICE_BUSY = ERROR + '.ServiceBusy'
+SERVICE_CONFUSED = ERROR + '.ServiceConfused'
+
+BANNED = ERROR + '.Channel.Banned'
+
+UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
+
+TUBE_PARAMETERS = CHANNEL_IFACE_TUBE + '.Parameters'
+TUBE_STATE = CHANNEL_IFACE_TUBE + '.State'
+STREAM_TUBE_SERVICE = CHANNEL_TYPE_STREAM_TUBE + '.Service'
+DBUS_TUBE_SERVICE_NAME = CHANNEL_TYPE_DBUS_TUBE + '.ServiceName'
+DBUS_TUBE_DBUS_NAMES = CHANNEL_TYPE_DBUS_TUBE + '.DBusNames'
+DBUS_TUBE_SUPPORTED_ACCESS_CONTROLS = CHANNEL_TYPE_DBUS_TUBE + '.SupportedAccessControls'
+STREAM_TUBE_SUPPORTED_SOCKET_TYPES = CHANNEL_TYPE_STREAM_TUBE + '.SupportedSocketTypes'
+
+CONFERENCE_INITIAL_CHANNELS = CHANNEL_IFACE_CONFERENCE + '.InitialChannels'
+CONFERENCE_INITIAL_INVITEE_HANDLES = CHANNEL_IFACE_CONFERENCE + '.InitialInviteeHandles'
+CONFERENCE_INITIAL_INVITEE_IDS = CHANNEL_IFACE_CONFERENCE + '.InitialInviteeIDs'
+
+CONTACT_SEARCH_ASK = CHANNEL_TYPE_CONTACT_SEARCH + '.AvailableSearchKeys'
+CONTACT_SEARCH_SERVER = CHANNEL_TYPE_CONTACT_SEARCH + '.Server'
+CONTACT_SEARCH_STATE = CHANNEL_TYPE_CONTACT_SEARCH + '.SearchState'
+
+SEARCH_NOT_STARTED = 0
+SEARCH_IN_PROGRESS = 1
+SEARCH_MORE_AVAILABLE = 2
+SEARCH_COMPLETED = 3
+SEARCH_FAILED = 4
+
+TUBE_CHANNEL_STATE_LOCAL_PENDING = 0
+TUBE_CHANNEL_STATE_REMOTE_PENDING = 1
+TUBE_CHANNEL_STATE_OPEN = 2
+TUBE_CHANNEL_STATE_NOT_OFFERED = 3
+
+MEDIA_STREAM_TYPE_AUDIO = 0
+MEDIA_STREAM_TYPE_VIDEO = 1
+
+MEDIA_STREAM_BASE_PROTO_UDP = 0
+MEDIA_STREAM_BASE_PROTO_TCP = 1
+
+MEDIA_STREAM_TRANSPORT_TYPE_LOCAL = 0
+MEDIA_STREAM_TRANSPORT_TYPE_DERIVED = 1
+MEDIA_STREAM_TRANSPORT_TYPE_RELAY = 2
+
+SOCKET_ADDRESS_TYPE_UNIX = 0
+SOCKET_ADDRESS_TYPE_ABSTRACT_UNIX = 1
+SOCKET_ADDRESS_TYPE_IPV4 = 2
+SOCKET_ADDRESS_TYPE_IPV6 = 3
+
+SOCKET_ACCESS_CONTROL_LOCALHOST = 0
+SOCKET_ACCESS_CONTROL_PORT = 1
+SOCKET_ACCESS_CONTROL_NETMASK = 2
+SOCKET_ACCESS_CONTROL_CREDENTIALS = 3
+
+TUBE_STATE_LOCAL_PENDING = 0
+TUBE_STATE_REMOTE_PENDING = 1
+TUBE_STATE_OPEN = 2
+TUBE_STATE_NOT_OFFERED = 3
+
+TUBE_TYPE_DBUS = 0
+TUBE_TYPE_STREAM = 1
+
+MEDIA_STREAM_DIRECTION_NONE = 0
+MEDIA_STREAM_DIRECTION_SEND = 1
+MEDIA_STREAM_DIRECTION_RECEIVE = 2
+MEDIA_STREAM_DIRECTION_BIDIRECTIONAL = 3
+
+MEDIA_STREAM_PENDING_LOCAL_SEND = 1
+MEDIA_STREAM_PENDING_REMOTE_SEND = 2
+
+MEDIA_STREAM_TYPE_AUDIO = 0
+MEDIA_STREAM_TYPE_VIDEO = 1
+
+MEDIA_STREAM_STATE_DISCONNECTED = 0
+MEDIA_STREAM_STATE_CONNECTING = 1
+MEDIA_STREAM_STATE_CONNECTED = 2
+
+MEDIA_STREAM_DIRECTION_NONE = 0
+MEDIA_STREAM_DIRECTION_SEND = 1
+MEDIA_STREAM_DIRECTION_RECEIVE = 2
+MEDIA_STREAM_DIRECTION_BIDIRECTIONAL = 3
+
+FT_STATE_NONE = 0
+FT_STATE_PENDING = 1
+FT_STATE_ACCEPTED = 2
+FT_STATE_OPEN = 3
+FT_STATE_COMPLETED = 4
+FT_STATE_CANCELLED = 5
+
+FT_STATE_CHANGE_REASON_NONE = 0
+FT_STATE_CHANGE_REASON_REQUESTED = 1
+FT_STATE_CHANGE_REASON_LOCAL_STOPPED = 2
+FT_STATE_CHANGE_REASON_REMOTE_STOPPED = 3
+FT_STATE_CHANGE_REASON_LOCAL_ERROR = 4
+FT_STATE_CHANGE_REASON_REMOTE_ERROR = 5
+
+FILE_HASH_TYPE_NONE = 0
+FILE_HASH_TYPE_MD5 = 1
+FILE_HASH_TYPE_SHA1 = 2
+FILE_HASH_TYPE_SHA256 = 3
+
+FT_STATE = CHANNEL_TYPE_FILE_TRANSFER + '.State'
+FT_CONTENT_TYPE = CHANNEL_TYPE_FILE_TRANSFER + '.ContentType'
+FT_FILENAME = CHANNEL_TYPE_FILE_TRANSFER + '.Filename'
+FT_SIZE = CHANNEL_TYPE_FILE_TRANSFER + '.Size'
+FT_CONTENT_HASH_TYPE = CHANNEL_TYPE_FILE_TRANSFER + '.ContentHashType'
+FT_CONTENT_HASH = CHANNEL_TYPE_FILE_TRANSFER + '.ContentHash'
+FT_DESCRIPTION = CHANNEL_TYPE_FILE_TRANSFER + '.Description'
+FT_DATE = CHANNEL_TYPE_FILE_TRANSFER + '.Date'
+FT_AVAILABLE_SOCKET_TYPES = CHANNEL_TYPE_FILE_TRANSFER + '.AvailableSocketTypes'
+FT_TRANSFERRED_BYTES = CHANNEL_TYPE_FILE_TRANSFER + '.TransferredBytes'
+FT_INITIAL_OFFSET = CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset'
+FT_FILE_COLLECTION = CHANNEL_TYPE_FILE_TRANSFER + '.FUTURE.FileCollection'
+FT_URI = CHANNEL_TYPE_FILE_TRANSFER + '.URI'
+
+GF_CAN_ADD = 1
+GF_CAN_REMOVE = 2
+GF_CAN_RESCIND = 4
+GF_MESSAGE_ADD = 8
+GF_MESSAGE_REMOVE = 16
+GF_MESSAGE_ACCEPT = 32
+GF_MESSAGE_REJECT = 64
+GF_MESSAGE_RESCIND = 128
+GF_CHANNEL_SPECIFIC_HANDLES = 256
+GF_ONLY_ONE_GROUP = 512
+GF_HANDLE_OWNERS_NOT_AVAILABLE = 1024
+GF_PROPERTIES = 2048
+GF_MEMBERS_CHANGED_DETAILED = 4096
+
+GC_REASON_NONE = 0
+GC_REASON_OFFLINE = 1
+GC_REASON_KICKED = 2
+GC_REASON_BUSY = 3
+GC_REASON_INVITED = 4
+GC_REASON_BANNED = 5
+GC_REASON_ERROR = 6
+GC_REASON_INVALID_CONTACT = 7
+GC_REASON_NO_ANSWER = 8
+GC_REASON_RENAMED = 9
+GC_REASON_PERMISSION_DENIED = 10
+GC_REASON_SEPARATED = 11
+
+HS_UNHELD = 0
+HS_HELD = 1
+HS_PENDING_HOLD = 2
+HS_PENDING_UNHOLD = 3
+
+HSR_NONE = 0
+HSR_REQUESTED = 1
+HSR_RESOURCE_NOT_AVAILABLE = 2
+
+CALL_STATE_RINGING = 1
+CALL_STATE_QUEUED = 2
+CALL_STATE_HELD = 4
+CALL_STATE_FORWARDED = 8
+
+CONN_STATUS_CONNECTED = 0
+CONN_STATUS_CONNECTING = 1
+CONN_STATUS_DISCONNECTED = 2
+
+CSR_NONE_SPECIFIED = 0
+CSR_REQUESTED = 1
+CSR_NETWORK_ERROR = 2
+CSR_AUTHENTICATION_FAILED = 3
+CSR_ENCRYPTION_ERROR = 4
+CSR_NAME_IN_USE = 5
+CSR_CERT_NOT_PROVIDED = 6
+CSR_CERT_UNTRUSTED = 7
+CSR_CERT_EXPIRED = 8
+CSR_CERT_NOT_ACTIVATED = 9
+CSR_CERT_HOSTNAME_MISMATCH = 10
+CSR_CERT_FINGERPRINT_MISMATCH = 11
+CSR_CERT_SELF_SIGNED = 12
+CSR_CERT_OTHER_ERROR = 13
+
+BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo'
+ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties'
+
+CHAT_STATE_GONE = 0
+CHAT_STATE_INACTIVE = 1
+CHAT_STATE_ACTIVE = 2
+CHAT_STATE_PAUSED = 3
+CHAT_STATE_COMPOSING = 4
+
+# Channel_Media_Capabilities
+MEDIA_CAP_AUDIO = 1
+MEDIA_CAP_VIDEO = 2
+MEDIA_CAP_STUN = 4
+MEDIA_CAP_GTALKP2P = 8
+MEDIA_CAP_ICEUDP = 16
+MEDIA_CAP_IMMUTABLE_STREAMS = 32
+
+CLIENT = 'org.freedesktop.Telepathy.Client'
+
+PRESENCE_OFFLINE = 1
+PRESENCE_AVAILABLE = 2
+PRESENCE_AWAY = 3
+PRESENCE_EXTENDED_AWAY = 4
+PRESENCE_HIDDEN = 5
+PRESENCE_BUSY = 6
+PRESENCE_UNKNOWN = 7
+PRESENCE_ERROR = 8
+
+CONTACT_INFO_FLAG_CAN_SET = 1
+CONTACT_INFO_FLAG_PUSH = 2
+CONTACT_INFO_FIELD_FLAG_PARAMETERS_EXACT = 1
+CONTACT_INFO_FIELD_FLAG_OVERWRITTEN_BY_NICKNAME = 2
+
+# Channel_Interface_SaslAuthentication
+SASL_STATUS_NOT_STARTED = 0
+SASL_STATUS_IN_PROGRESS = 1
+SASL_STATUS_SERVER_SUCCEEDED = 2
+SASL_STATUS_CLIENT_ACCEPTED = 3
+SASL_STATUS_SUCCEEDED = 4
+SASL_STATUS_SERVER_FAILED = 5
+SASL_STATUS_CLIENT_FAILED = 6
+
+SASL_ABORT_REASON_INVALID_CHALLENGE = 0
+SASL_ABORT_REASON_USER_ABORT = 1
+
+AUTH_METHOD = CHANNEL_TYPE_SERVER_AUTHENTICATION + ".AuthenticationMethod"
+SASL_AVAILABLE_MECHANISMS = CHANNEL_IFACE_SASL_AUTH + ".AvailableMechanisms"
+SASL_STATUS = CHANNEL_IFACE_SASL_AUTH + ".SASLStatus"
+SASL_ERROR = CHANNEL_IFACE_SASL_AUTH + ".SASLError"
+SASL_ERROR_DETAILS = CHANNEL_IFACE_SASL_AUTH + ".SASLErrorDetails"
+SASL_CONTEXT = CHANNEL_IFACE_SASL_AUTH + ".SASLContext"
+SASL_AUTHORIZATION_IDENTITY = CHANNEL_IFACE_SASL_AUTH + ".AuthorizationIdentity"
+SASL_DEFAULT_REALM = CHANNEL_IFACE_SASL_AUTH + ".DefaultRealm"
+SASL_DEFAULT_USERNAME = CHANNEL_IFACE_SASL_AUTH + ".DefaultUsername"
+
+# Channel_Type_ServerTLSConnection
+TLS_CERT_PATH = CHANNEL_TYPE_SERVER_TLS_CONNECTION + ".ServerCertificate"
+TLS_HOSTNAME = CHANNEL_TYPE_SERVER_TLS_CONNECTION + ".Hostname"
+TLS_REFERENCE_IDENTITIES = \
+ CHANNEL_TYPE_SERVER_TLS_CONNECTION + ".ReferenceIdentities"
+
+# Connection.Interface.Location
+
+LOCATION_FEATURE_CAN_SET = 1
+
+# Channel.Type.Text
+
+MT_NORMAL = 0
+MT_ACTION = 1
+MT_NOTICE = 2
+MT_AUTO_REPLY = 3
+MT_DELIVERY_REPORT = 4
+
+PROTOCOL = 'org.freedesktop.Telepathy.Protocol'
+PROTOCOL_IFACE_PRESENCES = PROTOCOL + '.Interface.Presence'
+PARAM_REQUIRED = 1
+PARAM_REGISTER = 2
+PARAM_HAS_DEFAULT = 4
+PARAM_SECRET = 8
+PARAM_DBUS_PROPERTY = 16
+
+AUTHENTICATION = 'org.freedesktop.Telepathy.Authentication'
+AUTH_TLS_CERT = AUTHENTICATION + ".TLSCertificate"
+
+TLS_CERT_STATE_PENDING = 0
+TLS_CERT_STATE_ACCEPTED = 1
+TLS_CERT_STATE_REJECTED = 2
+
+TLS_REJECT_REASON_UNKNOWN = 0
+TLS_REJECT_REASON_UNTRUSTED = 1
+
+# Channel.Interface.Messages
+
+MESSAGE_PART_SUPPORT_FLAGS = CHANNEL_IFACE_MESSAGES + '.MessagePartSupportFlags'
+DELIVERY_REPORTING_SUPPORT = CHANNEL_IFACE_MESSAGES + '.DeliveryReportingSupport'
+SUPPORTED_CONTENT_TYPES = CHANNEL_IFACE_MESSAGES + '.SupportedContentTypes'
+
+MSG_SENDING_FLAGS_REPORT_DELIVERY = 1
+MSG_SENDING_FLAGS_REPORT_READ = 2
+MSG_SENDING_FLAGS_REPORT_DELETED = 4
+
+DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_FAILURES = 1
+DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_SUCCESSES = 2
+DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_READ = 4
+DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_DELETED = 8
+
+MEDIA_STREAM_ERROR_UNKNOWN = 0
+MEDIA_STREAM_ERROR_EOS = 1
+MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED = 2
+MEDIA_STREAM_ERROR_CONNECTION_FAILED = 3
+MEDIA_STREAM_ERROR_NETWORK_ERROR = 4
+MEDIA_STREAM_ERROR_NO_CODECS = 5
+MEDIA_STREAM_ERROR_INVALID_CM_BEHAVIOR = 6
+MEDIA_STREAM_ERROR_MEDIA_ERROR = 7
+
+PASSWORD_FLAG_PROVIDE = 8
+
+# Channel.Interface.Room
+ROOM_ROOM_ID = CHANNEL_IFACE_ROOM + '.RoomID'
+ROOM_SERVER = CHANNEL_IFACE_ROOM + '.Server'
+ROOM_SUBJECT = CHANNEL_IFACE_ROOM + '.Subject'
diff --git a/tests/twisted/gabbleservicetest.py b/tests/twisted/gabbleservicetest.py
new file mode 100644
index 0000000..8d217ca
--- /dev/null
+++ b/tests/twisted/gabbleservicetest.py
@@ -0,0 +1,640 @@
+
+"""
+Infrastructure code for testing connection managers.
+"""
+
+from twisted.internet import glib2reactor
+from twisted.internet.protocol import Protocol, Factory, ClientFactory
+glib2reactor.install()
+import sys
+import time
+
+import pprint
+import unittest
+
+import dbus.glib
+
+from twisted.internet import reactor
+
+import gabbleconstants as cs
+
+tp_name_prefix = 'org.freedesktop.Telepathy'
+tp_path_prefix = '/org/freedesktop/Telepathy'
+
+class DictionarySupersetOf (object):
+ """Utility class for expecting "a dictionary with at least these keys"."""
+ def __init__(self, dictionary):
+ self._dictionary = dictionary
+ def __repr__(self):
+ return "DictionarySupersetOf(%s)" % self._dictionary
+ def __eq__(self, other):
+ """would like to just do:
+ return set(other.items()).issuperset(self._dictionary.items())
+ but it turns out that this doesn't work if you have another dict
+ nested in the values of your dicts"""
+ try:
+ for k,v in self._dictionary.items():
+ if k not in other or other[k] != v:
+ return False
+ return True
+ except TypeError: # other is not iterable
+ return False
+
+class Event(object):
+ def __init__(self, type, **kw):
+ self.__dict__.update(kw)
+ self.type = type
+ (self.subqueue, self.subtype) = type.split ("-", 1)
+
+ def __str__(self):
+ return '\n'.join([ str(type(self)) ] + format_event(self))
+
+def format_event(event):
+ ret = ['- type %s' % event.type]
+
+ for key in sorted(dir(event)):
+ if key != 'type' and not key.startswith('_'):
+ ret.append('- %s: %s' % (
+ key, pprint.pformat(getattr(event, key))))
+
+ if key == 'error':
+ ret.append('%s' % getattr(event, key))
+
+ return ret
+
+class EventPattern:
+ def __init__(self, type, **properties):
+ self.type = type
+ self.predicate = None
+ if 'predicate' in properties:
+ self.predicate = properties['predicate']
+ del properties['predicate']
+ self.properties = properties
+ (self.subqueue, self.subtype) = type.split ("-", 1)
+
+ def __repr__(self):
+ properties = dict(self.properties)
+
+ if self.predicate is not None:
+ properties['predicate'] = self.predicate
+
+ return '%s(%r, **%r)' % (
+ self.__class__.__name__, self.type, properties)
+
+ def match(self, event):
+ if event.type != self.type:
+ return False
+
+ for key, value in self.properties.iteritems():
+ try:
+ if getattr(event, key) != value:
+ return False
+ except AttributeError:
+ return False
+
+ if self.predicate is None or self.predicate(event):
+ return True
+
+ return False
+
+
+class TimeoutError(Exception):
+ pass
+
+class ForbiddenEventOccurred(Exception):
+ def __init__(self, event):
+ Exception.__init__(self)
+ self.event = event
+
+ def __str__(self):
+ return '\n' + '\n'.join(format_event(self.event))
+
+class BaseEventQueue:
+ """Abstract event queue base class.
+
+ Implement the wait() method to have something that works.
+ """
+
+ def __init__(self, timeout=None):
+ self.verbose = False
+ self.forbidden_events = set()
+ self.event_queues = {}
+
+ if timeout is None:
+ self.timeout = 5
+ else:
+ self.timeout = timeout
+
+ def log(self, s):
+ if self.verbose:
+ print s
+
+ def log_queues(self, queues):
+ self.log ("Waiting for event on: %s" % ", ".join(queues))
+
+ def log_event(self, event):
+ self.log('got event:')
+
+ if self.verbose:
+ map(self.log, format_event(event))
+
+ def forbid_events(self, patterns):
+ """
+ Add patterns (an iterable of EventPattern) to the set of forbidden
+ events. If a forbidden event occurs during an expect or expect_many,
+ the test will fail.
+ """
+ self.forbidden_events.update(set(patterns))
+
+ def unforbid_events(self, patterns):
+ """
+ Remove 'patterns' (an iterable of EventPattern) from the set of
+ forbidden events. These must be the same EventPattern pointers that
+ were passed to forbid_events.
+ """
+ self.forbidden_events.difference_update(set(patterns))
+
+ def _check_forbidden(self, event):
+ for e in self.forbidden_events:
+ if e.match(event):
+ raise ForbiddenEventOccurred(event)
+
+ def expect(self, type, **kw):
+ """
+ Waits for an event matching the supplied pattern to occur, and returns
+ it. For example, to await a D-Bus signal with particular arguments:
+
+ e = q.expect('dbus-signal', signal='Badgers', args=["foo", 42])
+ """
+ pattern = EventPattern(type, **kw)
+ t = time.time()
+
+ while True:
+ event = self.wait([pattern.subqueue])
+ self._check_forbidden(event)
+
+ if pattern.match(event):
+ self.log('handled, took %0.3f ms'
+ % ((time.time() - t) * 1000.0) )
+ self.log('')
+ return event
+
+ self.log('not handled')
+ self.log('')
+
+ def expect_many(self, *patterns):
+ """
+ Waits for events matching all of the supplied EventPattern instances to
+ return, and returns a list of events in the same order as the patterns
+ they matched. After a pattern is successfully matched, it is not
+ considered for future events; if more than one unsatisfied pattern
+ matches an event, the first "wins".
+
+ Note that the expected events may occur in any order. If you're
+ expecting a series of events in a particular order, use repeated calls
+ to expect() instead.
+
+ This method is useful when you're awaiting a number of events which may
+ happen in any order. For instance, in telepathy-gabble, calling a D-Bus
+ method often causes a value to be returned immediately, as well as a
+ query to be sent to the server. Since these events may reach the test
+ in either order, the following is incorrect and will fail if the IQ
+ happens to reach the test first:
+
+ ret = q.expect('dbus-return', method='Foo')
+ query = q.expect('stream-iq', query_ns=ns.FOO)
+
+ The following would be correct:
+
+ ret, query = q.expect_many(
+ EventPattern('dbus-return', method='Foo'),
+ EventPattern('stream-iq', query_ns=ns.FOO),
+ )
+ """
+ ret = [None] * len(patterns)
+ t = time.time()
+
+ while None in ret:
+ try:
+ queues = set()
+ for i, pattern in enumerate(patterns):
+ if ret[i] is None:
+ queues.add(pattern.subqueue)
+ event = self.wait(queues)
+ except TimeoutError:
+ self.log('timeout')
+ self.log('still expecting:')
+ for i, pattern in enumerate(patterns):
+ if ret[i] is None:
+ self.log(' - %r' % pattern)
+ raise
+ self._check_forbidden(event)
+
+ for i, pattern in enumerate(patterns):
+ if ret[i] is None and pattern.match(event):
+ self.log('handled, took %0.3f ms'
+ % ((time.time() - t) * 1000.0) )
+ self.log('')
+ ret[i] = event
+ break
+ else:
+ self.log('not handled')
+ self.log('')
+
+ return ret
+
+ def demand(self, type, **kw):
+ pattern = EventPattern(type, **kw)
+
+ event = self.wait([pattern.subqueue])
+
+ if pattern.match(event):
+ self.log('handled')
+ self.log('')
+ return event
+
+ self.log('not handled')
+ raise RuntimeError('expected %r, got %r' % (pattern, event))
+
+ def queues_available(self, queues):
+ if queues == None:
+ return self.event_queues.keys()
+ else:
+ available = self.event_queues.keys()
+ return filter(lambda x: x in available, queues)
+
+
+ def pop_next(self, queue):
+ events = self.event_queues[queue]
+ e = events.pop(0)
+ if not events:
+ self.event_queues.pop (queue)
+ return e
+
+ def append(self, event):
+ self.log ("Adding to queue")
+ self.log_event (event)
+ self.event_queues[event.subqueue] = \
+ self.event_queues.get(event.subqueue, []) + [event]
+
+class IteratingEventQueue(BaseEventQueue):
+ """Event queue that works by iterating the Twisted reactor."""
+
+ def __init__(self, timeout=None):
+ BaseEventQueue.__init__(self, timeout)
+
+ def wait(self, queues=None):
+ stop = [False]
+
+ def later():
+ stop[0] = True
+
+ delayed_call = reactor.callLater(self.timeout, later)
+
+ self.log_queues(queues)
+
+ qa = self.queues_available(queues)
+ while not qa and (not stop[0]):
+ reactor.iterate(0.01)
+ qa = self.queues_available(queues)
+
+ if qa:
+ delayed_call.cancel()
+ e = self.pop_next (qa[0])
+ self.log_event (e)
+ return e
+ else:
+ raise TimeoutError
+
+class TestEventQueue(BaseEventQueue):
+ def __init__(self, events):
+ BaseEventQueue.__init__(self)
+ for e in events:
+ self.append (e)
+
+ def wait(self, queues = None):
+ qa = self.queues_available(queues)
+
+ if qa:
+ return self.pop_next (qa[0])
+ else:
+ raise TimeoutError
+
+class EventQueueTest(unittest.TestCase):
+ def test_expect(self):
+ queue = TestEventQueue([Event('test-foo'), Event('test-bar')])
+ assert queue.expect('test-foo').type == 'test-foo'
+ assert queue.expect('test-bar').type == 'test-bar'
+
+ def test_expect_many(self):
+ queue = TestEventQueue([Event('test-foo'),
+ Event('test-bar')])
+ bar, foo = queue.expect_many(
+ EventPattern('test-bar'),
+ EventPattern('test-foo'))
+ assert bar.type == 'test-bar'
+ assert foo.type == 'test-foo'
+
+ def test_expect_many2(self):
+ # Test that events are only matched against patterns that haven't yet
+ # been matched. This tests a regression.
+ queue = TestEventQueue([Event('test-foo', x=1), Event('test-foo', x=2)])
+ foo1, foo2 = queue.expect_many(
+ EventPattern('test-foo'),
+ EventPattern('test-foo'))
+ assert foo1.type == 'test-foo' and foo1.x == 1
+ assert foo2.type == 'test-foo' and foo2.x == 2
+
+ def test_expect_queueing(self):
+ queue = TestEventQueue([Event('foo-test', x=1),
+ Event('foo-test', x=2)])
+
+ queue.append(Event('bar-test', x=1))
+ queue.append(Event('bar-test', x=2))
+
+ queue.append(Event('baz-test', x=1))
+ queue.append(Event('baz-test', x=2))
+
+ for x in xrange(1,2):
+ e = queue.expect ('baz-test')
+ assertEquals (x, e.x)
+
+ e = queue.expect ('bar-test')
+ assertEquals (x, e.x)
+
+ e = queue.expect ('foo-test')
+ assertEquals (x, e.x)
+
+ def test_timeout(self):
+ queue = TestEventQueue([])
+ self.assertRaises(TimeoutError, queue.expect, 'test-foo')
+
+ def test_demand(self):
+ queue = TestEventQueue([Event('test-foo'), Event('test-bar')])
+ foo = queue.demand('test-foo')
+ assert foo.type == 'test-foo'
+
+ def test_demand_fail(self):
+ queue = TestEventQueue([Event('test-foo'), Event('test-bar')])
+ self.assertRaises(RuntimeError, queue.demand, 'test-bar')
+
+def unwrap(x):
+ """Hack to unwrap D-Bus values, so that they're easier to read when
+ printed."""
+
+ if isinstance(x, list):
+ return map(unwrap, x)
+
+ if isinstance(x, tuple):
+ return tuple(map(unwrap, x))
+
+ if isinstance(x, dict):
+ return dict([(unwrap(k), unwrap(v)) for k, v in x.iteritems()])
+
+ if isinstance(x, dbus.Boolean):
+ return bool(x)
+
+ for t in [unicode, str, long, int, float]:
+ if isinstance(x, t):
+ return t(x)
+
+ return x
+
+def call_async(test, proxy, method, *args, **kw):
+ """Call a D-Bus method asynchronously and generate an event for the
+ resulting method return/error."""
+
+ def reply_func(*ret):
+ test.append(Event('dbus-return', method=method,
+ value=unwrap(ret)))
+
+ def error_func(err):
+ test.append(Event('dbus-error', method=method, error=err,
+ name=err.get_dbus_name(), message=str(err)))
+
+ method_proxy = getattr(proxy, method)
+ kw.update({'reply_handler': reply_func, 'error_handler': error_func})
+ method_proxy(*args, **kw)
+
+def sync_dbus(bus, q, conn):
+ # Dummy D-Bus method call. We can't use DBus.Peer.Ping() because libdbus
+ # replies to that message immediately, rather than handing it up to
+ # dbus-glib and thence Gabble, which means that Ping()ing Gabble doesn't
+ # ensure that it's processed all D-Bus messages prior to our ping.
+ #
+ # This won't do the right thing unless the proxy has a unique name.
+ assert conn.object.bus_name.startswith(':')
+ root_object = bus.get_object(conn.object.bus_name, '/', introspect=False)
+ call_async(q,
+ dbus.Interface(root_object, 'org.freedesktop.Telepathy.Tests'),
+ 'DummySyncDBus')
+ q.expect('dbus-error', method='DummySyncDBus')
+
+class ProxyWrapper:
+ def __init__(self, object, default, others={}):
+ self.object = object
+ self.default_interface = dbus.Interface(object, default)
+ self.Properties = dbus.Interface(object, dbus.PROPERTIES_IFACE)
+ self.TpProperties = \
+ dbus.Interface(object, tp_name_prefix + '.Properties')
+ self.interfaces = dict([
+ (name, dbus.Interface(object, iface))
+ for name, iface in others.iteritems()])
+
+ def __getattr__(self, name):
+ if name in self.interfaces:
+ return self.interfaces[name]
+
+ if name in self.object.__dict__:
+ return getattr(self.object, name)
+
+ return getattr(self.default_interface, name)
+
+def wrap_connection(conn):
+ return ProxyWrapper(conn, tp_name_prefix + '.Connection',
+ dict([
+ (name, tp_name_prefix + '.Connection.Interface.' + name)
+ for name in ['Aliasing', 'Avatars', 'Capabilities', 'Contacts',
+ 'Presence', 'SimplePresence', 'Requests']] +
+ [('Peer', 'org.freedesktop.DBus.Peer'),
+ ('ContactCapabilities', cs.CONN_IFACE_CONTACT_CAPS),
+ ('ContactInfo', cs.CONN_IFACE_CONTACT_INFO),
+ ('Location', cs.CONN_IFACE_LOCATION),
+ ('Future', tp_name_prefix + '.Connection.FUTURE'),
+ ('MailNotification', cs.CONN_IFACE_MAIL_NOTIFICATION),
+ ('ContactList', cs.CONN_IFACE_CONTACT_LIST),
+ ('ContactGroups', cs.CONN_IFACE_CONTACT_GROUPS),
+ ('PowerSaving', cs.CONN_IFACE_POWER_SAVING),
+ ]))
+
+def wrap_channel(chan, type_, extra=None):
+ interfaces = {
+ type_: tp_name_prefix + '.Channel.Type.' + type_,
+ 'Group': tp_name_prefix + '.Channel.Interface.Group',
+ }
+
+ if extra:
+ interfaces.update(dict([
+ (name, tp_name_prefix + '.Channel.Interface.' + name)
+ for name in extra]))
+
+ return ProxyWrapper(chan, tp_name_prefix + '.Channel', interfaces)
+
+def make_connection(bus, event_func, name, proto, params):
+ cm = bus.get_object(
+ tp_name_prefix + '.ConnectionManager.%s' % name,
+ tp_path_prefix + '/ConnectionManager/%s' % name,
+ introspect=False)
+ cm_iface = dbus.Interface(cm, tp_name_prefix + '.ConnectionManager')
+
+ connection_name, connection_path = cm_iface.RequestConnection(
+ proto, dbus.Dictionary(params, signature='sv'))
+ conn = wrap_connection(bus.get_object(connection_name, connection_path))
+
+ return conn
+
+def make_channel_proxy(conn, path, iface):
+ bus = dbus.SessionBus()
+ chan = bus.get_object(conn.object.bus_name, path)
+ chan = dbus.Interface(chan, tp_name_prefix + '.' + iface)
+ return chan
+
+# block_reading can be used if the test want to choose when we start to read
+# data from the socket.
+class EventProtocol(Protocol):
+ def __init__(self, queue=None, block_reading=False):
+ self.queue = queue
+ self.block_reading = block_reading
+
+ def dataReceived(self, data):
+ if self.queue is not None:
+ self.queue.append(Event('socket-data', protocol=self,
+ data=data))
+
+ def sendData(self, data):
+ self.transport.write(data)
+
+ def connectionMade(self):
+ if self.block_reading:
+ self.transport.stopReading()
+
+ def connectionLost(self, reason=None):
+ if self.queue is not None:
+ self.queue.append(Event('socket-disconnected', protocol=self))
+
+class EventProtocolFactory(Factory):
+ def __init__(self, queue, block_reading=False):
+ self.queue = queue
+ self.block_reading = block_reading
+
+ def _create_protocol(self):
+ return EventProtocol(self.queue, self.block_reading)
+
+ def buildProtocol(self, addr):
+ proto = self._create_protocol()
+ self.queue.append(Event('socket-connected', protocol=proto))
+ return proto
+
+class EventProtocolClientFactory(EventProtocolFactory, ClientFactory):
+ pass
+
+def watch_tube_signals(q, tube):
+ def got_signal_cb(*args, **kwargs):
+ q.append(Event('tube-signal',
+ path=kwargs['path'],
+ signal=kwargs['member'],
+ args=map(unwrap, args),
+ tube=tube))
+
+ tube.add_signal_receiver(got_signal_cb,
+ path_keyword='path', member_keyword='member',
+ byte_arrays=True)
+
+def pretty(x):
+ return pprint.pformat(unwrap(x))
+
+def assertEquals(expected, value):
+ if expected != value:
+ raise AssertionError(
+ "expected:\n%s\ngot:\n%s" % (pretty(expected), pretty(value)))
+
+def assertSameSets(expected, value):
+ exp_set = set(expected)
+ val_set = set(value)
+
+ if exp_set != val_set:
+ raise AssertionError(
+ "expected contents:\n%s\ngot:\n%s" % (
+ pretty(exp_set), pretty(val_set)))
+
+def assertNotEquals(expected, value):
+ if expected == value:
+ raise AssertionError(
+ "expected something other than:\n%s" % pretty(value))
+
+def assertContains(element, value):
+ if element not in value:
+ raise AssertionError(
+ "expected:\n%s\nin:\n%s" % (pretty(element), pretty(value)))
+
+def assertDoesNotContain(element, value):
+ if element in value:
+ raise AssertionError(
+ "expected:\n%s\nnot in:\n%s" % (pretty(element), pretty(value)))
+
+def assertLength(length, value):
+ if len(value) != length:
+ raise AssertionError("expected: length %d, got length %d:\n%s" % (
+ length, len(value), pretty(value)))
+
+def assertFlagsSet(flags, value):
+ masked = value & flags
+ if masked != flags:
+ raise AssertionError(
+ "expected flags %u, of which only %u are set in %u" % (
+ flags, masked, value))
+
+def assertFlagsUnset(flags, value):
+ masked = value & flags
+ if masked != 0:
+ raise AssertionError(
+ "expected none of flags %u, but %u are set in %u" % (
+ flags, masked, value))
+
+def assertDBusError(name, error):
+ if error.get_dbus_name() != name:
+ raise AssertionError(
+ "expected DBus error named:\n %s\ngot:\n %s\n(with message: %s)"
+ % (name, error.get_dbus_name(), error.message))
+
+def install_colourer():
+ def red(s):
+ return '\x1b[31m%s\x1b[0m' % s
+
+ def green(s):
+ return '\x1b[32m%s\x1b[0m' % s
+
+ patterns = {
+ 'handled': green,
+ 'not handled': red,
+ }
+
+ class Colourer:
+ def __init__(self, fh, patterns):
+ self.fh = fh
+ self.patterns = patterns
+
+ def write(self, s):
+ for p, f in self.patterns.items():
+ if s.startswith(p):
+ self.fh.write(f(p) + s[len(p):])
+ return
+
+ self.fh.write(s)
+
+ sys.stdout = Colourer(sys.stdout, patterns)
+ return sys.stdout
+
+if __name__ == '__main__':
+ unittest.main()
+
diff --git a/tests/twisted/gabbletest.py b/tests/twisted/gabbletest.py
new file mode 100644
index 0000000..a635d19
--- /dev/null
+++ b/tests/twisted/gabbletest.py
@@ -0,0 +1,824 @@
+
+"""
+Infrastructure code for testing Gabble by pretending to be a Jabber server.
+"""
+
+import base64
+import os
+import hashlib
+import sys
+import random
+import re
+import traceback
+
+import ns
+import gabbleconstants as cs
+import gabbleservicetest as servicetest
+from gabbleservicetest import (
+ assertEquals, assertLength, assertContains, wrap_channel,
+ EventPattern, call_async, unwrap, Event)
+import twisted
+from twisted.words.xish import domish, xpath
+from twisted.words.protocols.jabber.client import IQ
+from twisted.words.protocols.jabber import xmlstream
+from twisted.internet import reactor, ssl
+
+import dbus
+
+def make_result_iq(stream, iq, add_query_node=True):
+ result = IQ(stream, "result")
+ result["id"] = iq["id"]
+ to = iq.getAttribute('to')
+ if to is not None:
+ result["from"] = to
+ query = iq.firstChildElement()
+
+ if query and add_query_node:
+ result.addElement((query.uri, query.name))
+
+ return result
+
+def acknowledge_iq(stream, iq):
+ stream.send(make_result_iq(stream, iq))
+
+def send_error_reply(stream, iq, error_stanza=None):
+ result = IQ(stream, "error")
+ result["id"] = iq["id"]
+ query = iq.firstChildElement()
+ to = iq.getAttribute('to')
+ if to is not None:
+ result["from"] = to
+
+ if query:
+ result.addElement((query.uri, query.name))
+
+ if error_stanza:
+ result.addChild(error_stanza)
+
+ stream.send(result)
+
+def request_muc_handle(q, conn, stream, muc_jid):
+ servicetest.call_async(q, conn, 'RequestHandles', 2, [muc_jid])
+ event = q.expect('dbus-return', method='RequestHandles')
+ return event.value[0][0]
+
+def make_muc_presence(affiliation, role, muc_jid, alias, jid=None, photo=None):
+ presence = domish.Element((None, 'presence'))
+ presence['from'] = '%s/%s' % (muc_jid, alias)
+ x = presence.addElement((ns.MUC_USER, 'x'))
+ item = x.addElement('item')
+ item['affiliation'] = affiliation
+ item['role'] = role
+ if jid is not None:
+ item['jid'] = jid
+
+ if photo is not None:
+ presence.addChild(
+ elem(ns.VCARD_TEMP_UPDATE, 'x')(
+ elem('photo')(unicode(photo))
+ ))
+
+ return presence
+
+def sync_stream(q, stream):
+ """Used to ensure that Gabble has processed all stanzas sent to it."""
+
+ iq = IQ(stream, "get")
+ id = iq['id']
+ iq.addElement(('http://jabber.org/protocol/disco#info', 'query'))
+ stream.send(iq)
+ q.expect('stream-iq', query_ns='http://jabber.org/protocol/disco#info',
+ predicate=(lambda event:
+ event.stanza['id'] == id and event.iq_type == 'result'))
+
+class GabbleAuthenticator(xmlstream.Authenticator):
+ def __init__(self, username, password, resource=None):
+ self.username = username
+ self.password = password
+ self.resource = resource
+ self.bare_jid = None
+ self.full_jid = None
+ self._event_func = lambda e: None
+ xmlstream.Authenticator.__init__(self)
+
+ def set_event_func(self, event_func):
+ self._event_func = event_func
+
+class JabberAuthenticator(GabbleAuthenticator):
+ "Trivial XML stream authenticator that accepts one username/digest pair."
+
+ # Patch in fix from http://twistedmatrix.com/trac/changeset/23418.
+ # This monkeypatch taken from Gadget source code
+ from twisted.words.xish.utility import EventDispatcher
+
+ def _addObserver(self, onetime, event, observerfn, priority, *args,
+ **kwargs):
+ if self._dispatchDepth > 0:
+ self._updateQueue.append(lambda: self._addObserver(onetime, event,
+ observerfn, priority, *args, **kwargs))
+
+ return self._oldAddObserver(onetime, event, observerfn, priority,
+ *args, **kwargs)
+
+ EventDispatcher._oldAddObserver = EventDispatcher._addObserver
+ EventDispatcher._addObserver = _addObserver
+
+ def __init__(self, username, password, resource=None, emit_events=False):
+ GabbleAuthenticator.__init__(self, username, password, resource)
+ self.emit_events = emit_events
+
+ def streamStarted(self, root=None):
+ if root:
+ self.xmlstream.sid = '%x' % random.randint(1, sys.maxint)
+
+ self.xmlstream.sendHeader()
+ self.xmlstream.addOnetimeObserver(
+ "/iq/query[@xmlns='jabber:iq:auth']", self.initialIq)
+
+ def initialIq(self, iq):
+ if self.emit_events:
+ self._event_func(Event('auth-initial-iq', authenticator=self,
+ iq=iq, id=iq["id"]))
+ else:
+ self.respondToInitialIq(iq)
+
+ self.xmlstream.addOnetimeObserver('/iq/query/username', self.secondIq)
+
+ def respondToInitialIq(self, iq):
+ result = IQ(self.xmlstream, "result")
+ result["id"] = iq["id"]
+ query = result.addElement('query')
+ query["xmlns"] = "jabber:iq:auth"
+ query.addElement('username', content='test')
+ query.addElement('password')
+ query.addElement('digest')
+ query.addElement('resource')
+ self.xmlstream.send(result)
+
+ def secondIq(self, iq):
+ if self.emit_events:
+ self._event_func(Event('auth-second-iq', authenticator=self,
+ iq=iq, id=iq["id"]))
+ else:
+ self.respondToSecondIq(self, iq)
+
+ def respondToSecondIq(self, iq):
+ username = xpath.queryForNodes('/iq/query/username', iq)
+ assert map(str, username) == [self.username]
+
+ digest = xpath.queryForNodes('/iq/query/digest', iq)
+ expect = hashlib.sha1(self.xmlstream.sid + self.password).hexdigest()
+ assert map(str, digest) == [expect]
+
+ resource = xpath.queryForNodes('/iq/query/resource', iq)
+ assertLength(1, resource)
+ if self.resource is not None:
+ assertEquals(self.resource, str(resource[0]))
+
+ self.bare_jid = '%s@localhost' % self.username
+ self.full_jid = '%s/%s' % (self.bare_jid, resource)
+
+ result = IQ(self.xmlstream, "result")
+ result["id"] = iq["id"]
+ self.xmlstream.send(result)
+ self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
+
+class XmppAuthenticator(GabbleAuthenticator):
+ def __init__(self, username, password, resource=None):
+ GabbleAuthenticator.__init__(self, username, password, resource)
+ self.authenticated = False
+
+ def streamInitialize(self, root):
+ if root:
+ self.xmlstream.sid = root.getAttribute('id')
+
+ if self.xmlstream.sid is None:
+ self.xmlstream.sid = '%x' % random.randint(1, sys.maxint)
+
+ self.xmlstream.sendHeader()
+
+ def streamIQ(self):
+ features = elem(xmlstream.NS_STREAMS, 'features')(
+ elem(ns.NS_XMPP_BIND, 'bind'),
+ elem(ns.NS_XMPP_SESSION, 'session'),
+ )
+ self.xmlstream.send(features)
+
+ self.xmlstream.addOnetimeObserver(
+ "/iq/bind[@xmlns='%s']" % ns.NS_XMPP_BIND, self.bindIq)
+ self.xmlstream.addOnetimeObserver(
+ "/iq/session[@xmlns='%s']" % ns.NS_XMPP_SESSION, self.sessionIq)
+
+ def streamSASL(self):
+ features = domish.Element((xmlstream.NS_STREAMS, 'features'))
+ mechanisms = features.addElement((ns.NS_XMPP_SASL, 'mechanisms'))
+ mechanism = mechanisms.addElement('mechanism', content='PLAIN')
+ self.xmlstream.send(features)
+
+ self.xmlstream.addOnetimeObserver("/auth", self.auth)
+
+ def streamStarted(self, root=None):
+ self.streamInitialize(root)
+
+ if self.authenticated:
+ # Initiator authenticated itself, and has started a new stream.
+ self.streamIQ()
+ else:
+ self.streamSASL()
+
+ def auth(self, auth):
+ assert (base64.b64decode(str(auth)) ==
+ '\x00%s\x00%s' % (self.username, self.password))
+
+ success = domish.Element((ns.NS_XMPP_SASL, 'success'))
+ self.xmlstream.send(success)
+ self.xmlstream.reset()
+ self.authenticated = True
+
+ def bindIq(self, iq):
+ resource = xpath.queryForString('/iq/bind/resource', iq)
+ if self.resource is not None:
+ assertEquals(self.resource, resource)
+ else:
+ assert resource is not None
+
+ result = IQ(self.xmlstream, "result")
+ result["id"] = iq["id"]
+ bind = result.addElement((ns.NS_XMPP_BIND, 'bind'))
+ self.bare_jid = '%s@localhost' % self.username
+ self.full_jid = '%s/%s' % (self.bare_jid, resource)
+ jid = bind.addElement('jid', content=self.full_jid)
+ self.xmlstream.send(result)
+
+ self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
+
+ def sessionIq(self, iq):
+ self.xmlstream.send(make_result_iq(self.xmlstream, iq))
+
+class StreamEvent(servicetest.Event):
+ def __init__(self, type_, stanza, stream):
+ servicetest.Event.__init__(self, type_, stanza=stanza)
+ self.stream = stream
+ self.to = stanza.getAttribute("to")
+
+class IQEvent(StreamEvent):
+ def __init__(self, stream, iq):
+ StreamEvent.__init__(self, 'stream-iq', iq, stream)
+ self.iq_type = iq.getAttribute("type")
+ self.iq_id = iq.getAttribute("id")
+
+ query = iq.firstChildElement()
+
+ if query:
+ self.query = query
+ self.query_ns = query.uri
+ self.query_name = query.name
+
+ if query.getAttribute("node"):
+ self.query_node = query.getAttribute("node")
+ else:
+ self.query = None
+
+class PresenceEvent(StreamEvent):
+ def __init__(self, stream, stanza):
+ StreamEvent.__init__(self, 'stream-presence', stanza, stream)
+ self.presence_type = stanza.getAttribute('type')
+
+ statuses = xpath.queryForNodes('/presence/status', stanza)
+
+ if statuses:
+ self.presence_status = str(statuses[0])
+
+class MessageEvent(StreamEvent):
+ def __init__(self, stream, stanza):
+ StreamEvent.__init__(self, 'stream-message', stanza, stream)
+ self.message_type = stanza.getAttribute('type')
+
+class StreamFactory(twisted.internet.protocol.Factory):
+ def __init__(self, streams, jids):
+ self.streams = streams
+ self.jids = jids
+ self.presences = {}
+ self.mappings = dict(map (lambda jid, stream: (jid, stream),
+ jids, streams))
+
+ # Make a copy of the streams
+ self.factory_streams = list(streams)
+ self.factory_streams.reverse()
+
+ # Do not add observers for single instances because it's unnecessary and
+ # some unit tests need to respond to the roster request, and we shouldn't
+ # answer it for them otherwise we break compatibility
+ if len(streams) > 1:
+ # We need to have a function here because lambda keeps a reference on
+ # the stream and jid and in the for loop, there is no context
+ def addObservers(stream, jid):
+ stream.addObserver('/iq', lambda x: \
+ self.forward_iq(stream, jid, x))
+ stream.addObserver('/presence', lambda x: \
+ self.got_presence(stream, jid, x))
+
+ for (jid, stream) in self.mappings.items():
+ addObservers(stream, jid)
+
+ def protocol(self, *args):
+ return self.factory_streams.pop()
+
+
+ def got_presence (self, stream, jid, stanza):
+ stanza.attributes['from'] = jid
+ self.presences[jid] = stanza
+
+ for dest_jid in self.presences.keys():
+ # Dispatch the new presence to other clients
+ stanza.attributes['to'] = dest_jid
+ self.mappings[dest_jid].send(stanza)
+
+ # Don't echo the presence twice
+ if dest_jid != jid:
+ # Dispatch other client's presence to this stream
+ presence = self.presences[dest_jid]
+ presence.attributes['to'] = jid
+ stream.send(presence)
+
+ def lost_presence(self, stream, jid):
+ if self.presences.has_key(jid):
+ del self.presences[jid]
+ for dest_jid in self.presences.keys():
+ presence = domish.Element(('jabber:client', 'presence'))
+ presence['from'] = jid
+ presence['to'] = dest_jid
+ presence['type'] = 'unavailable'
+ self.mappings[dest_jid].send(presence)
+
+ def forward_iq(self, stream, jid, stanza):
+ stanza.attributes['from'] = jid
+
+ query = stanza.firstChildElement()
+
+ # Fake other accounts as being part of our roster
+ if query and query.uri == ns.ROSTER:
+ roster = make_result_iq(stream, stanza)
+ query = roster.firstChildElement()
+ for roster_jid in self.mappings.keys():
+ if jid != roster_jid:
+ item = query.addElement('item')
+ item['jid'] = roster_jid
+ item['subscription'] = 'both'
+ stream.send(roster)
+ return
+
+ to = stanza.getAttribute('to')
+ dest = None
+ if to is not None:
+ dest = self.mappings.get(to)
+
+ if dest is not None:
+ dest.send(stanza)
+
+class BaseXmlStream(xmlstream.XmlStream):
+ initiating = False
+ namespace = 'jabber:client'
+ pep_support = True
+ disco_features = []
+ handle_privacy_lists = True
+
+ def __init__(self, event_func, authenticator):
+ xmlstream.XmlStream.__init__(self, authenticator)
+ self.event_func = event_func
+ self.addObserver('//iq', lambda x: event_func(
+ IQEvent(self, x)))
+ self.addObserver('//message', lambda x: event_func(
+ MessageEvent(self, x)))
+ self.addObserver('//presence', lambda x: event_func(
+ PresenceEvent(self, x)))
+ self.addObserver('//event/stream/authd', self._cb_authd)
+ if self.handle_privacy_lists:
+ self.addObserver("/iq/query[@xmlns='%s']" % ns.PRIVACY,
+ self._cb_priv_list)
+
+ def _cb_priv_list(self, iq):
+ send_error_reply(self, iq)
+
+ def _cb_authd(self, _):
+ # called when stream is authenticated
+ assert self.authenticator.full_jid is not None
+ assert self.authenticator.bare_jid is not None
+
+ self.addObserver(
+ "/iq[@to='localhost']/query[@xmlns='http://jabber.org/protocol/disco#info']",
+ self._cb_disco_iq)
+ self.addObserver(
+ "/iq[@to='%s']/query[@xmlns='http://jabber.org/protocol/disco#info']"
+ % self.authenticator.bare_jid,
+ self._cb_bare_jid_disco_iq)
+ self.event_func(servicetest.Event('stream-authenticated'))
+
+ def _cb_disco_iq(self, iq):
+ nodes = xpath.queryForNodes(
+ "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", iq)
+ query = nodes[0]
+
+ for feature in self.disco_features:
+ query.addChild(elem('feature', var=feature))
+
+ iq['type'] = 'result'
+ iq['from'] = iq['to']
+ self.send(iq)
+
+ def _cb_bare_jid_disco_iq(self, iq):
+ # advertise PEP support
+ nodes = xpath.queryForNodes(
+ "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']",
+ iq)
+ query = nodes[0]
+ identity = query.addElement('identity')
+ identity['category'] = 'pubsub'
+ identity['type'] = 'pep'
+
+ iq['type'] = 'result'
+ iq['from'] = iq['to']
+ self.send(iq)
+
+ def onDocumentEnd(self):
+ self.event_func(servicetest.Event('stream-closed'))
+ # We don't chain up XmlStream.onDocumentEnd() because it will
+ # disconnect the TCP connection making tests as
+ # connect/disconnect-timeout.py not working
+
+ def send_stream_error(self, error='system-shutdown'):
+ # Yes, there are meant to be two different STREAMS namespaces.
+ go_away = \
+ elem(xmlstream.NS_STREAMS, 'error')(
+ elem(ns.STREAMS, error)
+ )
+
+ self.send(go_away)
+
+class JabberXmlStream(BaseXmlStream):
+ version = (0, 9)
+
+class XmppXmlStream(BaseXmlStream):
+ version = (1, 0)
+
+class GoogleXmlStream(BaseXmlStream):
+ version = (1, 0)
+
+ pep_support = False
+ disco_features = [ns.GOOGLE_ROSTER,
+ ns.GOOGLE_JINGLE_INFO,
+ ns.GOOGLE_MAIL_NOTIFY,
+ ns.GOOGLE_QUEUE,
+ ]
+
+ def _cb_bare_jid_disco_iq(self, iq):
+ # Google talk doesn't support PEP :(
+ iq['type'] = 'result'
+ iq['from'] = iq['to']
+ self.send(iq)
+
+
+def make_connection(bus, event_func, params=None, suffix=''):
+ # Gabble accepts a resource in 'account', but the value of 'resource'
+ # overrides it if there is one.
+ test_name = re.sub('(.*tests/twisted/|\./)', '', sys.argv[0])
+ account = 'test%s@localhost/%s' % (suffix, test_name)
+
+ default_params = {
+ 'account': account,
+ 'password': 'pass',
+ 'resource': 'Resource',
+ 'server': 'localhost',
+ 'port': dbus.UInt32(4242),
+ 'fallback-socks5-proxies': dbus.Array([], signature='s'),
+ 'require-encryption': False,
+ }
+
+ if params:
+ default_params.update(params)
+
+ # Allow omitting the 'password' param
+ if default_params['password'] is None:
+ del default_params['password']
+
+ # Allow omitting the 'account' param
+ if default_params['account'] is None:
+ del default_params['account']
+
+ jid = default_params.get('account', None)
+ conn = servicetest.make_connection(bus, event_func, 'gabble', 'jabber',
+ default_params)
+ return (conn, jid)
+
+def make_stream(event_func, authenticator=None, protocol=None,
+ resource=None, suffix=''):
+ # set up Jabber server
+ if authenticator is None:
+ authenticator = XmppAuthenticator('test%s' % suffix, 'pass', resource=resource)
+
+ authenticator.set_event_func(event_func)
+
+ if protocol is None:
+ protocol = XmppXmlStream
+
+ stream = protocol(event_func, authenticator)
+ return stream
+
+def disconnect_conn(q, conn, stream, expected_before=[], expected_after=[]):
+ call_async(q, conn, 'Disconnect')
+
+ tmp = expected_before + [
+ EventPattern('dbus-signal', signal='StatusChanged', args=[cs.CONN_STATUS_DISCONNECTED, cs.CSR_REQUESTED]),
+ EventPattern('stream-closed')]
+
+ before_events = q.expect_many(*tmp)
+
+ stream.sendFooter()
+
+ tmp = expected_after + [EventPattern('dbus-return', method='Disconnect')]
+ after_events = q.expect_many(*tmp)
+
+ return before_events[:-2], after_events[:-1]
+
+def exec_test_deferred(fun, params, protocol=None, timeout=None,
+ authenticator=None, num_instances=1,
+ do_connect=True):
+ # hack to ease debugging
+ domish.Element.__repr__ = domish.Element.toXml
+ colourer = None
+
+ if sys.stdout.isatty() or 'CHECK_FORCE_COLOR' in os.environ:
+ colourer = servicetest.install_colourer()
+
+ bus = dbus.SessionBus()
+
+ queue = servicetest.IteratingEventQueue(timeout)
+ queue.verbose = (
+ os.environ.get('CHECK_TWISTED_VERBOSE', '') != ''
+ or '-v' in sys.argv)
+
+ conns = []
+ jids = []
+ streams = []
+ resource = params.get('resource') if params is not None else None
+ for i in range(0, num_instances):
+ if i == 0:
+ suffix = ''
+ else:
+ suffix = str(i)
+
+ try:
+ (conn, jid) = make_connection(bus, queue.append, params, suffix)
+ except Exception, e:
+ # Crap. This is normally because the connection's still kicking
+ # around on the bus. Let's bin any connections we *did* manage to
+ # get going and then bail out unceremoniously.
+ print e
+
+ for conn in conns:
+ conn.Disconnect()
+
+ os._exit(1)
+
+ conns.append(conn)
+ jids.append(jid)
+ streams.append(make_stream(queue.append, protocol=protocol,
+ authenticator=authenticator,
+ resource=resource, suffix=suffix))
+
+ factory = StreamFactory(streams, jids)
+ port = reactor.listenTCP(4242, factory, interface='localhost')
+
+ def signal_receiver(*args, **kw):
+ if kw['path'] == '/org/freedesktop/DBus' and \
+ kw['member'] == 'NameOwnerChanged':
+ bus_name, old_name, new_name = args
+ if new_name == '':
+ for i, conn in enumerate(conns):
+ stream = streams[i]
+ jid = jids[i]
+ if conn._requested_bus_name == bus_name:
+ factory.lost_presence(stream, jid)
+ break
+ queue.append(Event('dbus-signal',
+ path=unwrap(kw['path']),
+ signal=kw['member'], args=map(unwrap, args),
+ interface=kw['interface']))
+
+ match_all_signals = bus.add_signal_receiver(
+ signal_receiver,
+ None, # signal name
+ None, # interface
+ None,
+ path_keyword='path',
+ member_keyword='member',
+ interface_keyword='interface',
+ byte_arrays=True
+ )
+
+ error = None
+
+ try:
+ if do_connect:
+ for conn in conns:
+ conn.Connect()
+ queue.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTING, cs.CSR_REQUESTED])
+ queue.expect('stream-authenticated')
+ queue.expect('dbus-signal', signal='PresenceUpdate',
+ args=[{1L: (0L, {u'available': {}})}])
+ queue.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ if len(conns) == 1:
+ fun(queue, bus, conns[0], streams[0])
+ else:
+ fun(queue, bus, conns, streams)
+ except Exception, e:
+ traceback.print_exc()
+ error = e
+ queue.verbose = False
+
+ if colourer:
+ sys.stdout = colourer.fh
+
+ d = port.stopListening()
+
+ # Does the Connection object still exist?
+ for i, conn in enumerate(conns):
+ if not bus.name_has_owner(conn.object.bus_name):
+ # Connection has already been disconnected and destroyed
+ continue
+ try:
+ if conn.GetStatus() == cs.CONN_STATUS_CONNECTED:
+ # Connection is connected, properly disconnect it
+ disconnect_conn(queue, conn, streams[i])
+ else:
+ # Connection is not connected, call Disconnect() to destroy it
+ conn.Disconnect()
+ except dbus.DBusException, e:
+ pass
+ except Exception, e:
+ traceback.print_exc()
+ error = e
+
+ try:
+ conn.Disconnect()
+ raise AssertionError("Connection didn't disappear; "
+ "all subsequent tests will probably fail")
+ except dbus.DBusException, e:
+ pass
+ except Exception, e:
+ traceback.print_exc()
+ error = e
+
+ match_all_signals.remove()
+
+ if error is None:
+ d.addBoth((lambda *args: reactor.crash()))
+ else:
+ # please ignore the POSIX behind the curtain
+ d.addBoth((lambda *args: os._exit(1)))
+
+
+def exec_test(fun, params=None, protocol=None, timeout=None,
+ authenticator=None, num_instances=1, do_connect=True):
+ reactor.callWhenRunning(
+ exec_test_deferred, fun, params, protocol, timeout, authenticator, num_instances,
+ do_connect)
+ reactor.run()
+
+# Useful routines for server-side vCard handling
+current_vcard = domish.Element(('vcard-temp', 'vCard'))
+
+def expect_and_handle_get_vcard(q, stream):
+ get_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP,
+ query_name='vCard', iq_type='get')
+
+ iq = get_vcard_event.stanza
+ vcard = iq.firstChildElement()
+ assert vcard.name == 'vCard', vcard.toXml()
+
+ # Send back current vCard
+ result = make_result_iq(stream, iq, add_query_node=False)
+ result.addChild(current_vcard)
+ stream.send(result)
+
+def expect_and_handle_set_vcard(q, stream, check=None):
+ global current_vcard
+ set_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP,
+ query_name='vCard', iq_type='set')
+ iq = set_vcard_event.stanza
+ vcard = iq.firstChildElement()
+ assert vcard.name == 'vCard', vcard.toXml()
+
+ if check is not None:
+ check(vcard)
+
+ # Update current vCard
+ current_vcard = vcard
+
+ stream.send(make_result_iq(stream, iq))
+
+def _elem_add(elem, *children):
+ for child in children:
+ if isinstance(child, domish.Element):
+ elem.addChild(child)
+ elif isinstance(child, unicode):
+ elem.addContent(child)
+ else:
+ raise ValueError(
+ 'invalid child object %r (must be element or unicode)', child)
+
+def elem(a, b=None, attrs={}, **kw):
+ r"""
+ >>> elem('foo')().toXml()
+ u'<foo/>'
+ >>> elem('foo', x='1')().toXml()
+ u"<foo x='1'/>"
+ >>> elem('foo', x='1')(u'hello').toXml()
+ u"<foo x='1'>hello</foo>"
+ >>> elem('foo', x='1')(u'hello',
+ ... elem('http://foo.org', 'bar', y='2')(u'bye')).toXml()
+ u"<foo x='1'>hello<bar xmlns='http://foo.org' y='2'>bye</bar></foo>"
+ >>> elem('foo', attrs={'xmlns:bar': 'urn:bar', 'bar:cake': 'yum'})(
+ ... elem('bar:e')(u'i')
+ ... ).toXml()
+ u"<foo xmlns:bar='urn:bar' bar:cake='yum'><bar:e>i</bar:e></foo>"
+ """
+
+ class _elem(domish.Element):
+ def __call__(self, *children):
+ _elem_add(self, *children)
+ return self
+
+ if b is not None:
+ elem = _elem((a, b))
+ else:
+ elem = _elem((None, a))
+
+ # Can't just update kw into attrs, because that *modifies the parameter's
+ # default*. Thanks python.
+ allattrs = {}
+ allattrs.update(kw)
+ allattrs.update(attrs)
+
+ # First, let's pull namespaces out
+ realattrs = {}
+ for k, v in allattrs.iteritems():
+ if k.startswith('xmlns:'):
+ abbr = k[len('xmlns:'):]
+ elem.localPrefixes[abbr] = v
+ else:
+ realattrs[k] = v
+
+ for k, v in realattrs.iteritems():
+ if k == 'from_':
+ elem['from'] = v
+ else:
+ elem[k] = v
+
+ return elem
+
+def elem_iq(server, type, **kw):
+ class _iq(IQ):
+ def __call__(self, *children):
+ _elem_add(self, *children)
+ return self
+
+ iq = _iq(server, type)
+
+ for k, v in kw.iteritems():
+ if k == 'from_':
+ iq['from'] = v
+ else:
+ iq[k] = v
+
+ return iq
+
+def make_presence(_from, to='test@localhost', type=None, show=None,
+ status=None, caps=None, photo=None):
+ presence = domish.Element((None, 'presence'))
+ presence['from'] = _from
+ presence['to'] = to
+
+ if type is not None:
+ presence['type'] = type
+
+ if show is not None:
+ presence.addElement('show', content=show)
+
+ if status is not None:
+ presence.addElement('status', content=status)
+
+ if caps is not None:
+ cel = presence.addElement(('http://jabber.org/protocol/caps', 'c'))
+ for key,value in caps.items():
+ cel[key] = value
+
+ # <x xmlns="vcard-temp:x:update"><photo>4a1...</photo></x>
+ if photo is not None:
+ x = presence.addElement((ns.VCARD_TEMP_UPDATE, 'x'))
+ x.addElement('photo').addContent(photo)
+
+ return presence
diff --git a/tests/twisted/tools/Makefile.am b/tests/twisted/tools/Makefile.am
index 66e9265..fe7b620 100644
--- a/tests/twisted/tools/Makefile.am
+++ b/tests/twisted/tools/Makefile.am
@@ -2,7 +2,8 @@
service_in_files = \
org.freedesktop.Telepathy.MissionControl5.service.in \
org.freedesktop.Telepathy.Client.Logger.service.in \
- org.freedesktop.Telepathy.ConnectionManager.salut.service.in
+ org.freedesktop.Telepathy.ConnectionManager.salut.service.in \
+ org.freedesktop.Telepathy.ConnectionManager.gabble.service.in
service_files = $(service_in_files:.service.in=.service)
# D-Bus config file for testing
@@ -13,13 +14,15 @@ BUILT_SOURCES = \
$(service_files) \
$(conf_files) \
exec-with-log.sh \
- salut-exec-with-log.sh
+ salut-exec-with-log.sh \
+ gabble-exec-with-log.sh
EXTRA_DIST = \
$(service_in_files) \
$(conf_in_files) \
exec-with-log.sh.in \
salut-exec-with-log.sh.in \
+ gabble-exec-with-log.sh.in \
fake-startup.sh \
valgrind.supp \
with-session-bus.sh \
@@ -29,4 +32,5 @@ CLEANFILES = \
$(BUILT_SOURCES) \
missioncontrol.log \
missioncontrol-*.log \
- salut-testing.log
+ salut-testing.log \
+ gabble-testing.log
diff --git a/tests/twisted/tools/gabble-exec-with-log.sh.in b/tests/twisted/tools/gabble-exec-with-log.sh.in
new file mode 100644
index 0000000..48a6a43
--- /dev/null
+++ b/tests/twisted/tools/gabble-exec-with-log.sh.in
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+cd "@abs_top_builddir@/tests/twisted/tools"
+
+export GABBLE_DEBUG=all LM_DEBUG=net GIBBER_DEBUG=all WOCKY_DEBUG=all
+export GABBLE_TIMING=1
+export WOCKY_CAPS_CACHE=:memory: WOCKY_CAPS_CACHE_SIZE=50
+ulimit -c unlimited
+exec >> gabble-testing.log 2>&1
+
+export G_SLICE=debug-blocks
+
+if test -n "$GABBLE_TEST_VALGRIND"; then
+ export G_DEBUG=${G_DEBUG:+"${G_DEBUG},"}gc-friendly
+ export G_SLICE=${G_SLICE},always-malloc
+ export DBUS_DISABLE_MEM_POOLS=1
+ GABBLE_WRAPPER="valgrind --leak-check=full --num-callers=20"
+ GABBLE_WRAPPER="$GABBLE_WRAPPER --show-reachable=yes"
+ GABBLE_WRAPPER="$GABBLE_WRAPPER --gen-suppressions=all"
+ GABBLE_WRAPPER="$GABBLE_WRAPPER --child-silent-after-fork=yes"
+ GABBLE_WRAPPER="$GABBLE_WRAPPER --suppressions=@abs_top_srcdir@/tests/suppressions/tp-glib.supp"
+ GABBLE_WRAPPER="$GABBLE_WRAPPER --suppressions=@abs_top_srcdir@/tests/suppressions/gabble.supp"
+elif test -n "$GABBLE_TEST_REFDBG"; then
+ if test -z "$REFDBG_OPTIONS" ; then
+ export REFDBG_OPTIONS="btnum=10"
+ fi
+ if test -z "$GABBLE_WRAPPER" ; then
+ GABBLE_WRAPPER="refdbg"
+ fi
+elif test -n "$GABBLE_TEST_STRACE"; then
+ GABBLE_WRAPPER="strace -o strace.log"
+elif test -n "$GABBLE_TEST_BACKTRACE"; then
+ GABBLE_WRAPPER="gdb -x run_and_bt.gdb"
+fi
+
+# Prevent libproxy from hitting the network for wpad configuration
+export PX_MODULE_BLACKLIST=config_wpad
+
+export G_DEBUG=fatal-warnings,fatal-criticals" ${G_DEBUG}"
+exec @abs_top_builddir@/libtool --mode=execute $GABBLE_WRAPPER @GABBLE_EXECUTABLE@
diff --git a/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in
new file mode 100644
index 0000000..3e3c58d
--- /dev/null
+++ b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.Telepathy.ConnectionManager.gabble
+Exec=/bin/sh @abs_top_builddir@/tests/twisted/tools/gabble-exec-with-log.sh