[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