[Telepathy-commits] [telepathy-haze/master] Add infrastructure to run tests with Python and Twisted, taken from Gabble

Simon McVittie simon.mcvittie at collabora.co.uk
Fri Jan 16 01:33:52 PST 2009


No tests are actually included yet: authentication with hazetest.py
doesn't seem to work with libpurple's XMPP implementation.
---
 .gitignore                    |    5 +-
 Makefile.am                   |    2 +-
 configure.ac                  |   19 ++
 tests/Makefile.am             |   34 +++
 tests/exec-with-log.sh        |   34 +++
 tests/haze.service.in         |    3 +
 tests/tmp-session-bus.conf.in |   30 +++
 tests/twisted/Makefile.am     |   23 ++
 tests/twisted/hazetest.py     |  499 ++++++++++++++++++++++++++++++++++++++++
 tests/twisted/ns.py           |   18 ++
 tests/twisted/servicetest.py  |  505 +++++++++++++++++++++++++++++++++++++++++
 tools/Makefile.am             |   13 +
 tools/with-session-bus.sh     |   84 +++++++
 13 files changed, 1267 insertions(+), 2 deletions(-)
 create mode 100644 tests/Makefile.am
 create mode 100755 tests/exec-with-log.sh
 create mode 100644 tests/haze.service.in
 create mode 100644 tests/tmp-session-bus.conf.in
 create mode 100644 tests/twisted/Makefile.am
 create mode 100644 tests/twisted/hazetest.py
 create mode 100644 tests/twisted/ns.py
 create mode 100644 tests/twisted/servicetest.py
 create mode 100644 tools/Makefile.am
 create mode 100644 tools/with-session-bus.sh

diff --git a/.gitignore b/.gitignore
index 82e5cbf..fd9393b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,9 +20,12 @@ missing
 .libs
 src/telepathy-haze
 src/telepathy-haze.8
+/tests/haze-testing.log
+/tests/org.freedesktop.Telepathy.ConnectionManager.haze.service
+/tests/tmp-session-bus.conf
 
 *.o
 *.swp
 *~
+*.pyc
 autom4te.cache
-
diff --git a/Makefile.am b/Makefile.am
index 1cc0c72..ca6f644 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,5 +1,5 @@
 ACLOCAL_AMFLAGS = -I m4
 
-SUBDIRS = src data m4
+SUBDIRS = tools src data m4 tests
 
 EXTRA_DIST = autogen.sh
diff --git a/configure.ac b/configure.ac
index 872908c..6a60fb5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -75,6 +75,22 @@ AC_CHECK_LIB(purple, purple_dbus_uninit,
     [Define if purple_dbus_uninit is present in libpurple]) ],
   [], [$PURPLE_LIBS])
 
+# Check for a Python >= 2.5 with Twisted, to run the tests
+AC_MSG_CHECKING([for Python >= 2.5 with Twisted and XMPP protocol support])
+for TEST_PYTHON in python2.5 python2.6 python; do
+  if $TEST_PYTHON -c "from sys import version_info; import dbus, dbus.mainloop.glib; raise SystemExit(version_info < (2, 5, 0, 'final', 0))" >/dev/null 2>&1; then
+    if $TEST_PYTHON -c "import twisted.words.xish.domish, twisted.words.protocols.jabber, twisted.internet.reactor" >/dev/null 2>&1; then
+      AC_MSG_RESULT([$TEST_PYTHON])
+      AM_CONDITIONAL([WANT_TWISTED_TESTS], true)
+      break
+    else
+      TEST_PYTHON=false
+    fi
+  fi
+done
+AC_SUBST(TEST_PYTHON)
+AM_CONDITIONAL([WANT_TWISTED_TESTS], test false != "$TEST_PYTHON")
+
 #AS_AC_EXPAND(DATADIR, $datadir)
 #DBUS_SERVICES_DIR="$DATADIR/dbus-1/services"
 #AC_SUBST(DBUS_SERVICES_DIR)
@@ -84,4 +100,7 @@ AC_OUTPUT([Makefile
            src/Makefile
            data/Makefile
            m4/Makefile
+	   tests/Makefile
+	   tests/twisted/Makefile
+	   tools/Makefile
            ])
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..89c31e8
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,34 @@
+SUBDIRS = .
+
+if WANT_TWISTED_TESTS
+SUBDIRS += twisted
+endif
+
+%.conf: %.conf.in
+	sed -e "s|[@]abs_top_builddir[@]|@abs_top_builddir@|g" $< > $@
+
+# We don't use the full filename for the .in because > 99 character filenames
+# in tarballs are non-portable (and automake 1.8 doesn't let us build
+# non-archaic tarballs)
+org.freedesktop.Telepathy.ConnectionManager.%.service: %.service.in
+	sed -e "s|[@]abs_top_builddir[@]|@abs_top_builddir@|g" \
+		-e "s|[@]abs_top_srcdir[@]|@abs_top_srcdir@|g" $< > $@
+
+# D-Bus service file for testing
+service_in_files = haze.service.in
+service_files = org.freedesktop.Telepathy.ConnectionManager.haze.service
+
+# D-Bus config file for testing
+conf_in_files = tmp-session-bus.conf.in
+conf_files = $(conf_in_files:.conf.in=.conf)
+
+BUILT_SOURCES = $(service_files) $(conf_files)
+
+EXTRA_DIST = \
+	$(service_in_files) \
+	$(conf_in_files) \
+	exec-with-log.sh
+
+CLEANFILES = \
+    $(BUILT_SOURCES) \
+    haze-testing.log
diff --git a/tests/exec-with-log.sh b/tests/exec-with-log.sh
new file mode 100755
index 0000000..abfff63
--- /dev/null
+++ b/tests/exec-with-log.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+abs_top_srcdir="$1"
+shift
+abs_top_builddir="$1"
+shift
+
+cd "${abs_top_builddir}/tests"
+
+export HAZE_DEBUG=all
+ulimit -c unlimited
+exec >> haze-testing.log 2>&1
+
+if test -n "$HAZE_TEST_VALGRIND"; then
+        export G_DEBUG=${G_DEBUG:+"${G_DEBUG},"}gc-friendly
+        export G_SLICE=always-malloc
+        HAZE_WRAPPER="valgrind --leak-check=full --num-callers=20"
+        HAZE_WRAPPER="$HAZE_WRAPPER --show-reachable=yes"
+        HAZE_WRAPPER="$HAZE_WRAPPER --gen-suppressions=all"
+        HAZE_WRAPPER="$HAZE_WRAPPER --child-silent-after-fork=yes"
+        HAZE_WRAPPER="$HAZE_WRAPPER --suppressions=${abs_top_srcdir}/tools/tp-glib.supp"
+elif test -n "$HAZE_TEST_REFDBG"; then
+        if test -z "$REFDBG_OPTIONS" ; then
+                export REFDBG_OPTIONS="btnum=10"
+        fi
+        if test -z "$HAZE_WRAPPER" ; then
+                HAZE_WRAPPER="refdbg"
+        fi
+fi
+
+# not suitable for haze:
+#export G_DEBUG=fatal-warnings" ${G_DEBUG}"
+exec "${abs_top_builddir}/libtool" --mode=execute $HAZE_WRAPPER \
+    "${abs_top_builddir}/src/telepathy-haze"
diff --git a/tests/haze.service.in b/tests/haze.service.in
new file mode 100644
index 0000000..f129869
--- /dev/null
+++ b/tests/haze.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.Telepathy.ConnectionManager.haze
+Exec=@abs_top_builddir@/tests/exec-with-log.sh @abs_top_srcdir@ @abs_top_builddir@
diff --git a/tests/tmp-session-bus.conf.in b/tests/tmp-session-bus.conf.in
new file mode 100644
index 0000000..c73caac
--- /dev/null
+++ b/tests/tmp-session-bus.conf.in
@@ -0,0 +1,30 @@
+<!-- This configuration file controls the per-user-login-session message bus.
+     Add a session-local.conf and edit that rather than changing this 
+     file directly. -->
+
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+  <!-- Our well-known bus type, don't change this -->
+  <type>session</type>
+
+  <listen>unix:tmpdir=/tmp</listen>
+
+  <servicedir>@abs_top_builddir@/tests</servicedir>
+
+  <policy context="default">
+    <!-- Allow everything to be sent -->
+    <allow send_destination="*" eavesdrop="true"/>
+    <!-- Allow everything to be received -->
+    <allow eavesdrop="true"/>
+    <!-- Allow anyone to own anything -->
+    <allow own="*"/>
+  </policy>
+
+  <!-- This is included last so local configuration can override what's 
+       in this standard file -->
+  
+
+  
+
+</busconfig>
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am
new file mode 100644
index 0000000..22239d8
--- /dev/null
+++ b/tests/twisted/Makefile.am
@@ -0,0 +1,23 @@
+TWISTED_TESTS =
+
+TESTS =
+
+TESTS_ENVIRONMENT = \
+	PYTHONPATH=@abs_top_srcdir@/tests/twisted:@abs_top_builddir@/tests/twisted
+
+check-local: check-twisted
+
+check-twisted:
+	sh $(top_srcdir)/tools/with-session-bus.sh \
+		--config-file=$(top_builddir)/tests/tmp-session-bus.conf \
+		-- $(MAKE) check-TESTS \
+			TESTS="$(TWISTED_TESTS)" \
+			TESTS_ENVIRONMENT="$(TESTS_ENVIRONMENT) $(TEST_PYTHON)"
+
+EXTRA_DIST = \
+	$(TWISTED_TESTS) \
+	hazetest.py \
+	servicetest.py \
+	ns.py
+
+CLEANFILES = *.pyc */*.pyc
diff --git a/tests/twisted/hazetest.py b/tests/twisted/hazetest.py
new file mode 100644
index 0000000..1291ab5
--- /dev/null
+++ b/tests/twisted/hazetest.py
@@ -0,0 +1,499 @@
+
+"""
+Infrastructure code for testing Haze by pretending to be a Jabber server.
+"""
+
+import base64
+import os
+import sha
+import sys
+import time
+
+import ns
+import servicetest
+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
+
+import dbus
+
+NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
+NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'
+
+def make_result_iq(stream, iq):
+    result = IQ(stream, "result")
+    result["id"] = iq["id"]
+    query = iq.firstChildElement()
+
+    if query:
+        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):
+    result = IQ(stream, "error")
+    result["id"] = iq["id"]
+    query = iq.firstChildElement()
+
+    if query:
+        result.addElement((query.uri, query.name))
+
+    stream.send(result)
+
+def request_muc_handle(q, conn, stream, muc_jid):
+    servicetest.call_async(q, conn, 'RequestHandles', 2, [muc_jid])
+    host = muc_jid.split('@')[1]
+    event = q.expect('stream-iq', to=host, query_ns=ns.DISCO_INFO)
+    result = make_result_iq(stream, event.stanza)
+    feature = result.firstChildElement().addElement('feature')
+    feature['var'] = ns.MUC
+    stream.send(result)
+    event = q.expect('dbus-return', method='RequestHandles')
+    return event.value[0][0]
+
+def make_muc_presence(affiliation, role, muc_jid, alias):
+    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
+    return presence
+
+def sync_stream(q, stream):
+    """Used to ensure that the CM has processed all stanzas sent to it."""
+
+    iq = IQ(stream, "get")
+    iq.addElement(('http://jabber.org/protocol/disco#info', 'query'))
+    stream.send(iq)
+    q.expect('stream-iq', query_ns='http://jabber.org/protocol/disco#info')
+
+class JabberAuthenticator(xmlstream.Authenticator):
+    "Trivial XML stream authenticator that accepts one username/digest pair."
+
+    def __init__(self, username, password):
+        self.username = username
+        self.password = password
+        xmlstream.Authenticator.__init__(self)
+
+    # 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 streamStarted(self, root=None):
+        if root:
+            self.xmlstream.sid = root.getAttribute('id')
+
+        self.xmlstream.sendHeader()
+        self.xmlstream.addOnetimeObserver(
+            "/iq/query[@xmlns='jabber:iq:auth']", self.initialIq)
+
+    def initialIq(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.addOnetimeObserver('/iq/query/username', self.secondIq)
+        self.xmlstream.send(result)
+
+    def secondIq(self, iq):
+        username = xpath.queryForNodes('/iq/query/username', iq)
+        assert map(str, username) == [self.username]
+
+        digest = xpath.queryForNodes('/iq/query/digest', iq)
+        expect = sha.sha(self.xmlstream.sid + self.password).hexdigest()
+        assert map(str, digest) == [expect]
+
+        resource = xpath.queryForNodes('/iq/query/resource', iq)
+        assert map(str, resource) == ['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(xmlstream.Authenticator):
+    def __init__(self, username, password):
+        xmlstream.Authenticator.__init__(self)
+        self.username = username
+        self.password = password
+        self.authenticated = False
+
+    def streamStarted(self, root=None):
+        if root:
+            self.xmlstream.sid = root.getAttribute('id')
+
+        self.xmlstream.sendHeader()
+
+        if self.authenticated:
+            # Initiator authenticated itself, and has started a new stream.
+
+            features = domish.Element((xmlstream.NS_STREAMS, 'features'))
+            bind = features.addElement((NS_XMPP_BIND, 'bind'))
+            self.xmlstream.send(features)
+
+            self.xmlstream.addOnetimeObserver(
+                "/iq/bind[@xmlns='%s']" % NS_XMPP_BIND, self.bindIq)
+        else:
+            features = domish.Element((xmlstream.NS_STREAMS, 'features'))
+            mechanisms = features.addElement((NS_XMPP_SASL, 'mechanisms'))
+            mechanism = mechanisms.addElement('mechanism', content='PLAIN')
+            self.xmlstream.send(features)
+
+            self.xmlstream.addOnetimeObserver("/auth", self.auth)
+
+    def auth(self, auth):
+        assert (base64.b64decode(str(auth)) ==
+            '\x00%s\x00%s' % (self.username, self.password))
+
+        success = domish.Element((NS_XMPP_SASL, 'success'))
+        self.xmlstream.send(success)
+        self.xmlstream.reset()
+        self.authenticated = True
+
+    def bindIq(self, iq):
+        assert xpath.queryForString('/iq/bind/resource', iq) == 'Resource'
+
+        result = IQ(self.xmlstream, "result")
+        result["id"] = iq["id"]
+        bind = result.addElement((NS_XMPP_BIND, 'bind'))
+        jid = bind.addElement('jid', content='test at localhost/Resource')
+        self.xmlstream.send(result)
+
+        self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
+
+def make_stream_event(type, stanza):
+    event = servicetest.Event(type, stanza=stanza)
+    event.to = stanza.getAttribute("to")
+    return event
+
+def make_iq_event(iq):
+    event = make_stream_event('stream-iq', iq)
+    event.iq_type = iq.getAttribute("type")
+    query = iq.firstChildElement()
+
+    if query:
+        event.query = query
+        event.query_ns = query.uri
+        event.query_name = query.name
+
+        if query.getAttribute("node"):
+            event.query_node = query.getAttribute("node")
+
+    return event
+
+def make_presence_event(stanza):
+    event = make_stream_event('stream-presence', stanza)
+    event.presence_type = stanza.getAttribute('type')
+    return event
+
+def make_message_event(stanza):
+    event = make_stream_event('stream-message', stanza)
+    event.message_type = stanza.getAttribute('type')
+    return event
+
+class BaseXmlStream(xmlstream.XmlStream):
+    initiating = False
+    namespace = 'jabber:client'
+
+    def __init__(self, event_func, authenticator):
+        xmlstream.XmlStream.__init__(self, authenticator)
+        self.event_func = event_func
+        self.addObserver('//iq', lambda x: event_func(
+            make_iq_event(x)))
+        self.addObserver('//message', lambda x: event_func(
+            make_message_event(x)))
+        self.addObserver('//presence', lambda x: event_func(
+            make_presence_event(x)))
+        self.addObserver('//event/stream/authd', self._cb_authd)
+
+    def _cb_authd(self, _):
+        # called when stream is authenticated
+        self.addObserver(
+            "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']",
+            self._cb_disco_iq)
+        self.event_func(servicetest.Event('stream-authenticated'))
+
+    def _cb_disco_iq(self, iq):
+        if iq.getAttribute('to') == 'localhost':
+            # add 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'
+            self.send(iq)
+
+class JabberXmlStream(BaseXmlStream):
+    version = (0, 9)
+
+class XmppXmlStream(BaseXmlStream):
+    version = (1, 0)
+
+def make_connection(bus, event_func, params=None):
+    default_params = {
+        'account': 'test at localhost/Resource',
+        'password': 'pass',
+        #'resource': 'Resource',
+        'server': 'localhost',
+        # FIXME: the spec says this is a UInt32 and Gabble agrees
+        'port': dbus.Int32(4242),
+        }
+
+    if params:
+        default_params.update(params)
+
+    return servicetest.make_connection(bus, event_func, 'haze', 'jabber',
+        default_params)
+
+def make_stream(event_func, authenticator=None, protocol=None, port=4242):
+    # set up Jabber server
+
+    if authenticator is None:
+        authenticator = JabberAuthenticator('test', 'pass')
+
+    if protocol is None:
+        protocol = JabberXmlStream
+
+    stream = protocol(event_func, authenticator)
+    factory = twisted.internet.protocol.Factory()
+    factory.protocol = lambda *args: stream
+    port = reactor.listenTCP(port, factory)
+    return (stream, port)
+
+def go(params=None, authenticator=None, protocol=None, start=None):
+    # hack to ease debugging
+    domish.Element.__repr__ = domish.Element.toXml
+
+    bus = dbus.SessionBus()
+    handler = servicetest.EventTest()
+    conn = make_connection(bus, handler.handle_event, params)
+    (stream, _) = make_stream(handler.handle_event, authenticator, protocol)
+    handler.data = {
+        'bus': bus,
+        'conn': conn,
+        'conn_iface': dbus.Interface(conn,
+            'org.freedesktop.Telepathy.Connection'),
+        'stream': stream}
+    handler.data['test'] = handler
+    handler.verbose = (os.environ.get('CHECK_TWISTED_VERBOSE', '') != '')
+    map(handler.expect, servicetest.load_event_handlers())
+
+    if '-v' in sys.argv:
+        handler.verbose = True
+
+    if start is None:
+        handler.data['conn'].Connect()
+    else:
+        start(handler.data)
+
+    reactor.run()
+
+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):
+            f = self.patterns.get(s, lambda x: x)
+            self.fh.write(f(s))
+
+    sys.stdout = Colourer(sys.stdout, patterns)
+    return sys.stdout
+
+
+def exec_test_deferred (funs, params, protocol=None, timeout=None):
+    # hack to ease debugging
+    domish.Element.__repr__ = domish.Element.toXml
+    colourer = None
+
+    if sys.stdout.isatty():
+        colourer = install_colourer()
+
+    queue = servicetest.IteratingEventQueue(timeout)
+    queue.verbose = (
+        os.environ.get('CHECK_TWISTED_VERBOSE', '') != ''
+        or '-v' in sys.argv)
+
+    bus = dbus.SessionBus()
+    # conn = make_connection(bus, queue.append, params)
+    (stream, port) = make_stream(queue.append, protocol=protocol)
+
+    error = None
+
+    try:
+        for f in funs:
+            conn = make_connection(bus, queue.append, params)
+            f(queue, bus, conn, stream)
+    except Exception, e:
+        import traceback
+        traceback.print_exc()
+        error = e
+
+    try:
+        if colourer:
+          sys.stdout = colourer.fh
+        d = port.stopListening()
+        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)))
+
+        conn.Disconnect()
+
+        if 'HAZE_TEST_REFDBG' in os.environ:
+            # we have to wait that the CM timeouts so the process is properly
+            # exited and refdbg can generates its report
+            time.sleep(5.5)
+
+    except dbus.DBusException, e:
+        pass
+
+def exec_tests(funs, params=None, protocol=None, timeout=None):
+  reactor.callWhenRunning (exec_test_deferred, funs, params, protocol, timeout)
+  reactor.run()
+
+def exec_test(fun, params=None, protocol=None, timeout=None):
+  exec_tests([fun], params, protocol, timeout)
+
+# Useful routines for server-side vCard handling
+current_vcard = domish.Element(('vcard-temp', 'vCard'))
+
+def handle_get_vcard(event, data):
+    iq = event.stanza
+
+    if iq['type'] != 'get':
+        return False
+
+    if iq.uri != 'jabber:client':
+        return False
+
+    vcard = list(iq.elements())[0]
+
+    if vcard.name != 'vCard':
+        return False
+
+    # Send back current vCard
+    new_iq = IQ(data['stream'], 'result')
+    new_iq['id'] = iq['id']
+    new_iq.addChild(current_vcard)
+    data['stream'].send(new_iq)
+    return True
+
+def handle_set_vcard(event, data):
+    global current_vcard
+    iq = event.stanza
+
+    if iq['type'] != 'set':
+        return False
+
+    if iq.uri != 'jabber:client':
+        return False
+
+    vcard = list(iq.elements())[0]
+
+    if vcard.name != 'vCard':
+        return False
+
+    current_vcard = iq.firstChildElement()
+
+    new_iq = IQ(data['stream'], 'result')
+    new_iq['id'] = iq['id']
+    data['stream'].send(new_iq)
+    return True
+
+
+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', child)
+
+def elem(a, b=None, **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>"
+    """
+
+    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))
+
+    for k, v in kw.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
diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py
new file mode 100644
index 0000000..e0d0aa0
--- /dev/null
+++ b/tests/twisted/ns.py
@@ -0,0 +1,18 @@
+AMP = "http://jabber.org/protocol/amp"
+DISCO_INFO = "http://jabber.org/protocol/disco#info"
+DISCO_ITEMS = "http://jabber.org/protocol/disco#items"
+MUC = 'http://jabber.org/protocol/muc'
+MUC_OWNER = '%s#owner' % MUC
+MUC_USER = '%s#user' % MUC
+OLPC_ACTIVITIES = "http://laptop.org/xmpp/activities"
+OLPC_ACTIVITIES_NOTIFY = "%s+notify" % OLPC_ACTIVITIES
+OLPC_ACTIVITY = "http://laptop.org/xmpp/activity"
+OLPC_ACTIVITY_PROPS = "http://laptop.org/xmpp/activity-properties"
+OLPC_ACTIVITY_PROPS_NOTIFY = "%s+notify" % OLPC_ACTIVITY_PROPS
+OLPC_BUDDY = "http://laptop.org/xmpp/buddy"
+OLPC_BUDDY_PROPS = "http://laptop.org/xmpp/buddy-properties"
+OLPC_BUDDY_PROPS_NOTIFY = "%s+notify" % OLPC_BUDDY_PROPS
+OLPC_CURRENT_ACTIVITY = "http://laptop.org/xmpp/current-activity"
+OLPC_CURRENT_ACTIVITY_NOTIFY = "%s+notify" % OLPC_CURRENT_ACTIVITY
+PUBSUB = "http://jabber.org/protocol/pubsub"
+STANZA = "urn:ietf:params:xml:ns:xmpp-stanzas"
diff --git a/tests/twisted/servicetest.py b/tests/twisted/servicetest.py
new file mode 100644
index 0000000..e63235e
--- /dev/null
+++ b/tests/twisted/servicetest.py
@@ -0,0 +1,505 @@
+
+"""
+Infrastructure code for testing connection managers.
+"""
+
+from twisted.internet import glib2reactor
+from twisted.internet.protocol import Protocol, Factory, ClientFactory
+glib2reactor.install()
+
+import pprint
+import traceback
+import unittest
+
+import dbus.glib
+
+from twisted.internet import reactor
+
+tp_name_prefix = 'org.freedesktop.Telepathy'
+tp_path_prefix = '/org/freedesktop/Telepathy'
+
+class TryNextHandler(Exception):
+    pass
+
+def lazy(func):
+    def handler(event, data):
+        if func(event, data):
+            return True
+        else:
+            raise TryNextHandler()
+    handler.__name__ = func.__name__
+    return handler
+
+def match(type, **kw):
+    def decorate(func):
+        def handler(event, data, *extra, **extra_kw):
+            if event.type != type:
+                return False
+
+            for key, value in kw.iteritems():
+                if not hasattr(event, key):
+                    return False
+
+                if getattr(event, key) != value:
+                    return False
+
+            return func(event, data, *extra, **extra_kw)
+
+        handler.__name__ = func.__name__
+        return handler
+
+    return decorate
+
+class Event:
+    def __init__(self, type, **kw):
+        self.__dict__.update(kw)
+        self.type = type
+
+def format_event(event):
+    ret = ['- type %s' % event.type]
+
+    for key in 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 EventTest:
+    """Somewhat odd event dispatcher for asynchronous tests.
+
+    Callbacks are kept in a queue. Incoming events are passed to the first
+    callback. If the callback returns True, the callback is removed. If the
+    callback raises AssertionError, the test fails. If there are no more
+    callbacks, the test passes. The reactor is stopped when the test passes.
+    """
+
+    def __init__(self):
+        self.queue = []
+        self.data = {'test': self}
+        self.timeout_delayed_call = reactor.callLater(5, self.timeout_cb)
+        #self.verbose = True
+        self.verbose = False
+        # ugh
+        self.stopping = False
+
+    def timeout_cb(self):
+        print 'timed out waiting for events'
+        print self.queue[0]
+        self.fail()
+
+    def fail(self):
+        # ugh; better way to stop the reactor and exit(1)?
+        import os
+        os._exit(1)
+
+    def expect(self, f):
+        self.queue.append(f)
+
+    def log(self, s):
+        if self.verbose:
+            print s
+
+    def try_stop(self):
+        if self.stopping:
+            return True
+
+        if not self.queue:
+            self.log('no handlers left; stopping')
+            self.stopping = True
+            reactor.stop()
+            return True
+
+        return False
+
+    def call_handlers(self, event):
+        self.log('trying %r' % self.queue[0])
+        handler = self.queue.pop(0)
+
+        try:
+            ret = handler(event, self.data)
+            if not ret:
+                self.queue.insert(0, handler)
+        except TryNextHandler, e:
+            if self.queue:
+                ret = self.call_handlers(event)
+            else:
+                ret = False
+            self.queue.insert(0, handler)
+
+        return ret
+
+    def handle_event(self, event):
+        if self.try_stop():
+            return
+
+        self.log('got event:')
+        self.log('- type: %s' % event.type)
+        map(self.log, format_event(event))
+
+        try:
+            ret = self.call_handlers(event)
+        except SystemExit, e:
+            if e.code:
+                print "Unsuccessful exit:", e
+                self.fail()
+            else:
+                self.queue[:] = []
+                ret = True
+        except AssertionError, e:
+            print 'test failed:'
+            traceback.print_exc()
+            self.fail()
+        except (Exception, KeyboardInterrupt), e:
+            print 'error in handler:'
+            traceback.print_exc()
+            self.fail()
+
+        if ret not in (True, False):
+            print ("warning: %s() returned something other than True or False"
+                % self.queue[0].__name__)
+
+        if ret:
+            self.timeout_delayed_call.reset(5)
+            self.log('event handled')
+        else:
+            self.log('event not handled')
+
+        self.log('')
+        self.try_stop()
+
+class EventPattern:
+    def __init__(self, type, **properties):
+        self.type = type
+        self.predicate = lambda x: True
+        if 'predicate' in properties:
+            self.predicate = properties['predicate']
+            del properties['predicate']
+        self.properties = 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(event):
+            return True
+
+        return False
+
+
+class TimeoutError(Exception):
+    pass
+
+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.past_events = []
+
+        if timeout is None:
+            self.timeout = 5
+        else:
+            self.timeout = timeout
+
+    def log(self, s):
+        if self.verbose:
+            print s
+
+    def flush_past_events(self):
+        self.past_events = []
+
+    def expect_racy(self, type, **kw):
+        pattern = EventPattern(type, **kw)
+
+        for event in self.past_events:
+            if pattern.match(event):
+                self.log('past event handled')
+                map(self.log, format_event(event))
+                self.log('')
+                self.past_events.remove(event)
+                return event
+
+        return self.expect(type, **kw)
+
+    def expect(self, type, **kw):
+        pattern = EventPattern(type, **kw)
+
+        while True:
+            event = self.wait()
+            self.log('got event:')
+            map(self.log, format_event(event))
+
+            if pattern.match(event):
+                self.log('handled')
+                self.log('')
+                return event
+
+            self.past_events.append(event)
+            self.log('not handled')
+            self.log('')
+
+    def expect_many(self, *patterns):
+        ret = [None] * len(patterns)
+
+        while None in ret:
+            event = self.wait()
+            self.log('got event:')
+            map(self.log, format_event(event))
+
+            for i, pattern in enumerate(patterns):
+                if pattern.match(event):
+                    self.log('handled')
+                    self.log('')
+                    ret[i] = event
+                    break
+            else:
+                self.past_events.append(event)
+                self.log('not handled')
+                self.log('')
+
+        return ret
+
+    def demand(self, type, **kw):
+        pattern = EventPattern(type, **kw)
+
+        event = self.wait()
+        self.log('got event:')
+        map(self.log, format_event(event))
+
+        if pattern.match(event):
+            self.log('handled')
+            self.log('')
+            return event
+
+        self.log('not handled')
+        raise RuntimeError('expected %r, got %r' % (pattern, event))
+
+class IteratingEventQueue(BaseEventQueue):
+    """Event queue that works by iterating the Twisted reactor."""
+
+    def __init__(self, timeout=None):
+        BaseEventQueue.__init__(self, timeout)
+        self.events = []
+
+    def wait(self):
+        stop = [False]
+
+        def later():
+            stop[0] = True
+
+        delayed_call = reactor.callLater(self.timeout, later)
+
+        while (not self.events) and (not stop[0]):
+            reactor.iterate(0.1)
+
+        if self.events:
+            delayed_call.cancel()
+            return self.events.pop(0)
+        else:
+            raise TimeoutError
+
+    def append(self, event):
+        self.events.append(event)
+
+    # compatibility
+    handle_event = append
+
+class TestEventQueue(BaseEventQueue):
+    def __init__(self, events):
+        BaseEventQueue.__init__(self)
+        self.events = events
+
+    def wait(self):
+        if self.events:
+            return self.events.pop(0)
+        else:
+            raise TimeoutError
+
+class EventQueueTest(unittest.TestCase):
+    def test_expect(self):
+        queue = TestEventQueue([Event('foo'), Event('bar')])
+        assert queue.expect('foo').type == 'foo'
+        assert queue.expect('bar').type == 'bar'
+
+    def test_expect_many(self):
+        queue = TestEventQueue([Event('foo'), Event('bar')])
+        bar, foo = queue.expect_many(
+            EventPattern('bar'),
+            EventPattern('foo'))
+        assert bar.type == 'bar'
+        assert foo.type == 'foo'
+
+    def test_timeout(self):
+        queue = TestEventQueue([])
+        self.assertRaises(TimeoutError, queue.expect, 'foo')
+
+    def test_demand(self):
+        queue = TestEventQueue([Event('foo'), Event('bar')])
+        foo = queue.demand('foo')
+        assert foo.type == 'foo'
+
+    def test_demand_fail(self):
+        queue = TestEventQueue([Event('foo'), Event('bar')])
+        self.assertRaises(RuntimeError, queue.demand, '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()])
+
+    for t in [unicode, str, long, int, float, bool]:
+        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.handle_event(Event('dbus-return', method=method,
+            value=unwrap(ret)))
+
+    def error_func(err):
+        test.handle_event(Event('dbus-error', method=method, error=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
+    call_async(q, conn, "InspectHandles", 1, [])
+
+    event = q.expect('dbus-return', method='InspectHandles')
+
+class ProxyWrapper:
+    def __init__(self, object, default, others):
+        self.object = object
+        self.default_interface = dbus.Interface(object, default)
+        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 make_connection(bus, event_func, name, proto, params):
+    cm = bus.get_object(
+        tp_name_prefix + '.ConnectionManager.%s' % name,
+        tp_path_prefix + '/ConnectionManager/%s' % name)
+    cm_iface = dbus.Interface(cm, tp_name_prefix + '.ConnectionManager')
+
+    connection_name, connection_path = cm_iface.RequestConnection(
+        proto, params)
+    conn = bus.get_object(connection_name, connection_path)
+    conn = ProxyWrapper(conn, tp_name_prefix + '.Connection',
+        dict([
+            (name, tp_name_prefix + '.Connection.Interface.' + name)
+            for name in ['Aliasing', 'Avatars', 'Capabilities', 'Contacts',
+              'Presence']] +
+        [('Peer', 'org.freedesktop.DBus.Peer')]))
+
+    bus.add_signal_receiver(
+        lambda *args, **kw:
+            event_func(
+                Event('dbus-signal',
+                    path=unwrap(kw['path'])[len(tp_path_prefix):],
+                    signal=kw['member'], args=map(unwrap, args),
+                    interface=kw['interface'])),
+        None,       # signal name
+        None,       # interface
+        cm._named_service,
+        path_keyword='path',
+        member_keyword='member',
+        interface_keyword='interface',
+        byte_arrays=True
+        )
+
+    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
+
+def load_event_handlers():
+    path, _, _, _ = traceback.extract_stack()[0]
+    import compiler
+    import __main__
+    ast = compiler.parseFile(path)
+    return [
+        getattr(__main__, node.name)
+        for node in ast.node.asList()
+        if node.__class__ == compiler.ast.Function and
+            node.name.startswith('expect_')]
+
+class EventProtocol(Protocol):
+    def __init__(self, queue=None):
+        self.queue = queue
+
+    def dataReceived(self, data):
+        if self.queue is not None:
+            self.queue.handle_event(Event('socket-data', protocol=self,
+                data=data))
+
+    def sendData(self, data):
+        self.transport.write(data)
+
+class EventProtocolFactory(Factory):
+    def __init__(self, queue):
+        self.queue = queue
+
+    def buildProtocol(self, addr):
+        proto =  EventProtocol(self.queue)
+        self.queue.handle_event(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.handle_event(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)
+
+if __name__ == '__main__':
+    unittest.main()
+
diff --git a/tools/Makefile.am b/tools/Makefile.am
new file mode 100644
index 0000000..9998ee0
--- /dev/null
+++ b/tools/Makefile.am
@@ -0,0 +1,13 @@
+EXTRA_DIST = \
+    with-session-bus.sh
+
+all: $(EXTRA_DIST)
+
+TELEPATHY_GLIB_SRCDIR = $(top_srcdir)/../telepathy-glib
+maintainer-update-from-telepathy-glib:
+	set -e && cd $(srcdir) && \
+	for x in $(EXTRA_DIST); do \
+		if test -f $(TELEPATHY_GLIB_SRCDIR)/tools/$$x; then \
+			cp $(TELEPATHY_GLIB_SRCDIR)/tools/$$x $$x; \
+		fi; \
+	done
diff --git a/tools/with-session-bus.sh b/tools/with-session-bus.sh
new file mode 100644
index 0000000..519b9b1
--- /dev/null
+++ b/tools/with-session-bus.sh
@@ -0,0 +1,84 @@
+#!/bin/sh
+# with-session-bus.sh - run a program with a temporary D-Bus session daemon
+#
+# The canonical location of this program is the telepathy-glib tools/
+# directory, please synchronize any changes with that copy.
+#
+# Copyright (C) 2007-2008 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# Copying and distribution of this file, with or without modification,
+# are permitted in any medium without royalty provided the copyright
+# notice and this notice are preserved.
+
+set -e
+
+me=with-session-bus
+
+dbus_daemon_args="--print-address=5 --print-pid=6 --fork"
+
+usage ()
+{
+  echo "usage: $me [options] -- program [program_options]" >&2
+  echo "Requires write access to the current directory." >&2
+  echo "" >&2
+  echo "If \$WITH_SESSION_BUS_FORK_DBUS_MONITOR is set, fork dbus-monitor" >&2
+  echo "with the arguments in \$WITH_SESSION_BUS_FORK_DBUS_MONITOR_OPT." >&2
+  echo "The output of dbus-monitor is saved in $me-<pid>.dbus-monitor-logs" >&2
+  exit 2
+}
+
+while test "z$1" != "z--"; do
+  case "$1" in
+  --session)
+    dbus_daemon_args="$dbus_daemon_args --session"
+    shift
+    ;;
+  --config-file=*)
+    # FIXME: assumes config file doesn't contain any special characters
+    dbus_daemon_args="$dbus_daemon_args $1"
+    shift
+    ;;
+  *)
+    usage
+    ;;
+  esac
+done
+shift
+if test "z$1" = "z"; then usage; fi
+
+exec 5> $me-$$.address
+exec 6> $me-$$.pid
+
+cleanup ()
+{
+  pid=`head -n1 $me-$$.pid`
+  if test -n "$pid" ; then
+    echo "Killing temporary bus daemon: $pid" >&2
+    kill -INT "$pid"
+  fi
+  rm -f $me-$$.address
+  rm -f $me-$$.pid
+}
+
+trap cleanup INT HUP TERM
+dbus-daemon $dbus_daemon_args
+
+{ echo -n "Temporary bus daemon is "; cat $me-$$.address; } >&2
+{ echo -n "Temporary bus daemon PID is "; head -n1 $me-$$.pid; } >&2
+
+e=0
+DBUS_SESSION_BUS_ADDRESS="`cat $me-$$.address`"
+export DBUS_SESSION_BUS_ADDRESS
+
+if [ -n "$WITH_SESSION_BUS_FORK_DBUS_MONITOR" ] ; then
+  echo -n "Forking dbus-monitor $WITH_SESSION_BUS_FORK_DBUS_MONITOR_OPT" >&2
+  dbus-monitor $WITH_SESSION_BUS_FORK_DBUS_MONITOR_OPT \
+        &> $me-$$.dbus-monitor-logs &
+fi
+
+"$@" || e=$?
+
+trap - INT HUP TERM
+cleanup
+
+exit $e
-- 
1.5.6.5




More information about the Telepathy-commits mailing list