summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--INSTALL97
-rw-r--r--Makefile.am2
-rw-r--r--configure.ac27
-rw-r--r--tests/Makefile.am48
-rw-r--r--tests/README86
-rw-r--r--tests/suppressions/Makefile.am2
-rw-r--r--tests/suppressions/tpl.supp250
-rw-r--r--tests/test-basic-connect.sh2
-rw-r--r--tests/test-tpl-channel.c29
-rwxr-xr-xtests/test-tpl-conf148
-rw-r--r--tests/test-tpl-conf.c30
-rw-r--r--tests/test-tpl-log-entry.c27
-rwxr-xr-xtests/test-tpl-observer148
-rw-r--r--tests/test-tpl-observer.c31
-rw-r--r--tests/tpl-channel-child.c665
-rw-r--r--tests/twisted/Makefile.am130
-rw-r--r--tests/twisted/bytestream.py882
-rw-r--r--tests/twisted/constants.py306
-rw-r--r--tests/twisted/gabbletest.py606
-rw-r--r--tests/twisted/gateways.py94
-rw-r--r--tests/twisted/httptest.py31
-rw-r--r--tests/twisted/main-debug.c73
-rw-r--r--tests/twisted/mucutil.py70
-rw-r--r--tests/twisted/ns.py61
-rw-r--r--tests/twisted/pubsub.py88
-rw-r--r--tests/twisted/servicetest.py510
-rw-r--r--tests/twisted/test-debug.py55
-rw-r--r--tests/twisted/text/destroy.py125
-rw-r--r--tests/twisted/text/ensure.py171
-rw-r--r--tests/twisted/text/initiate-requestotron.py72
-rw-r--r--tests/twisted/text/initiate.py93
-rw-r--r--tests/twisted/text/respawn.py172
-rw-r--r--tests/twisted/text/send-error.py184
-rw-r--r--tests/twisted/text/send-to-correct-resource.py79
-rw-r--r--tests/twisted/text/test-chat-state.py303
-rw-r--r--tests/twisted/text/test-text-delayed.py56
-rw-r--r--tests/twisted/text/test-text-no-body.py40
-rw-r--r--tests/twisted/text/test-text.py177
-rw-r--r--tests/twisted/tools/Makefile.am33
-rwxr-xr-xtests/twisted/tools/exec-with-log.sh33
-rw-r--r--tests/twisted/tools/exec-with-log.sh.in33
-rw-r--r--tests/twisted/tools/failure-helper.sh105
-rw-r--r--tests/twisted/tools/gabble.service.in3
-rw-r--r--tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service3
-rw-r--r--tests/twisted/tools/tmp-session-bus.conf30
-rw-r--r--tests/twisted/tools/tmp-session-bus.conf.in30
-rw-r--r--tests/twisted/tools/with-session-bus.sh96
47 files changed, 6316 insertions, 20 deletions
diff --git a/INSTALL b/INSTALL
index 2550dab..7d1c323 100644
--- a/INSTALL
+++ b/INSTALL
@@ -4,8 +4,10 @@ Installation Instructions
Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005,
2006, 2007, 2008, 2009 Free Software Foundation, Inc.
- This file is free documentation; the Free Software Foundation gives
-unlimited permission to copy, distribute and modify it.
+ 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. This file is offered as-is,
+without warranty of any kind.
Basic Installation
==================
@@ -13,7 +15,11 @@ Basic Installation
Briefly, the shell commands `./configure; make; make install' should
configure, build, and install this package. The following
more-detailed instructions are generic; see the `README' file for
-instructions specific to this package.
+instructions specific to this package. Some packages provide this
+`INSTALL' file but do not implement all of the features documented
+below. The lack of an optional feature in a given package is not
+necessarily a bug. More recommendations for GNU packages can be found
+in *note Makefile Conventions: (standards)Makefile Conventions.
The `configure' shell script attempts to guess correct values for
various system-dependent variables used during compilation. It uses
@@ -42,7 +48,7 @@ may remove or edit it.
you want to change it or regenerate `configure' using a newer version
of `autoconf'.
-The simplest way to compile this package is:
+ The simplest way to compile this package is:
1. `cd' to the directory containing the package's source code and type
`./configure' to configure the package for your system.
@@ -53,12 +59,22 @@ The simplest way to compile this package is:
2. Type `make' to compile the package.
3. Optionally, type `make check' to run any self-tests that come with
- the package.
+ the package, generally using the just-built uninstalled binaries.
4. Type `make install' to install the programs and any data files and
- documentation.
-
- 5. You can remove the program binaries and object files from the
+ documentation. When installing into a prefix owned by root, it is
+ recommended that the package be configured and built as a regular
+ user, and only the `make install' phase executed with root
+ privileges.
+
+ 5. Optionally, type `make installcheck' to repeat any self-tests, but
+ this time using the binaries in their final installed location.
+ This target does not install anything. Running this target as a
+ regular user, particularly if the prior `make install' required
+ root privileges, verifies that the installation completed
+ correctly.
+
+ 6. You can remove the program binaries and object files from the
source code directory by typing `make clean'. To also remove the
files that `configure' created (so you can compile the package for
a different kind of computer), type `make distclean'. There is
@@ -67,8 +83,15 @@ The simplest way to compile this package is:
all sorts of other programs in order to regenerate files that came
with the distribution.
- 6. Often, you can also type `make uninstall' to remove the installed
- files again.
+ 7. Often, you can also type `make uninstall' to remove the installed
+ files again. In practice, not all packages have tested that
+ uninstallation works correctly, even though it is required by the
+ GNU Coding Standards.
+
+ 8. Some packages, particularly those that use Automake, provide `make
+ distcheck', which can by used by developers to test that all other
+ targets like `make install' and `make uninstall' work correctly.
+ This target is generally not run by end users.
Compilers and Options
=====================
@@ -93,7 +116,8 @@ same time, by placing the object files for each architecture in their
own directory. To do this, you can use GNU `make'. `cd' to the
directory where you want the object files and executables to go and run
the `configure' script. `configure' automatically checks for the
-source code in the directory that `configure' is in and in `..'.
+source code in the directory that `configure' is in and in `..'. This
+is known as a "VPATH" build.
With a non-GNU `make', it is safer to compile the package for one
architecture at a time in the source code directory. After you have
@@ -120,7 +144,8 @@ Installation Names
By default, `make install' installs the package's commands under
`/usr/local/bin', include files under `/usr/local/include', etc. You
can specify an installation prefix other than `/usr/local' by giving
-`configure' the option `--prefix=PREFIX'.
+`configure' the option `--prefix=PREFIX', where PREFIX must be an
+absolute file name.
You can specify separate installation prefixes for
architecture-specific files and architecture-independent files. If you
@@ -131,15 +156,46 @@ Documentation and other data files still use the regular prefix.
In addition, if you use an unusual directory layout you can give
options like `--bindir=DIR' to specify different values for particular
kinds of files. Run `configure --help' for a list of the directories
-you can set and what kinds of files go in them.
+you can set and what kinds of files go in them. In general, the
+default for these options is expressed in terms of `${prefix}', so that
+specifying just `--prefix' will affect all of the other directory
+specifications that were not explicitly provided.
+
+ The most portable way to affect installation locations is to pass the
+correct locations to `configure'; however, many packages provide one or
+both of the following shortcuts of passing variable assignments to the
+`make install' command line to change installation locations without
+having to reconfigure or recompile.
+
+ The first method involves providing an override variable for each
+affected directory. For example, `make install
+prefix=/alternate/directory' will choose an alternate location for all
+directory configuration variables that were expressed in terms of
+`${prefix}'. Any directories that were specified during `configure',
+but not in terms of `${prefix}', must each be overridden at install
+time for the entire installation to be relocated. The approach of
+makefile variable overrides for each directory variable is required by
+the GNU Coding Standards, and ideally causes no recompilation.
+However, some platforms have known limitations with the semantics of
+shared libraries that end up requiring recompilation when using this
+method, particularly noticeable in packages that use GNU Libtool.
+
+ The second method involves providing the `DESTDIR' variable. For
+example, `make install DESTDIR=/alternate/directory' will prepend
+`/alternate/directory' before all installation names. The approach of
+`DESTDIR' overrides is not required by the GNU Coding Standards, and
+does not work on platforms that have drive letters. On the other hand,
+it does better at avoiding recompilation issues, and works well even
+when some directory options were not specified in terms of `${prefix}'
+at `configure' time.
+
+Optional Features
+=================
If the package supports it, you can cause programs to be installed
with an extra prefix or suffix on their names by giving `configure' the
option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'.
-Optional Features
-=================
-
Some packages pay attention to `--enable-FEATURE' options to
`configure', where FEATURE indicates an optional part of the package.
They may also pay attention to `--with-PACKAGE' options, where PACKAGE
@@ -152,6 +208,13 @@ find the X include and library files automatically, but if it doesn't,
you can use the `configure' options `--x-includes=DIR' and
`--x-libraries=DIR' to specify their locations.
+ Some packages offer the ability to configure how verbose the
+execution of `make' will be. For these packages, running `./configure
+--enable-silent-rules' sets the default to minimal output, which can be
+overridden with `make V=1'; while running `./configure
+--disable-silent-rules' sets the default to verbose, which can be
+overridden with `make V=0'.
+
Particular systems
==================
@@ -288,7 +351,7 @@ operates.
`configure' can determine that directory automatically.
`--prefix=DIR'
- Use DIR as the installation prefix. *Note Installation Names::
+ Use DIR as the installation prefix. *note Installation Names::
for more details, including other options available for fine-tuning
the installation locations.
diff --git a/Makefile.am b/Makefile.am
index 895c400..4e07192 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = tools telepathy-logger data src
+SUBDIRS = tools telepathy-logger data src tests
ACLOCAL_AMFLAGS = -I m4
diff --git a/configure.ac b/configure.ac
index 95d6640..3b95fbb 100644
--- a/configure.ac
+++ b/configure.ac
@@ -33,6 +33,8 @@ AC_COPYRIGHT([
# Minimal version required
GLIB_REQUIRED=2.22.0
+DBUS_REQUIRED=1.1.0
+DBUS_GLIB_REQUIRED=0.82
TELEPATHY_GLIB_REQUIRED=0.9.0
# Use --enable-maintainer-mode to disabled deprecated symbols
@@ -64,11 +66,29 @@ AM_GCONF_SOURCE_2
GLIB_GENMARSHAL=`$PKG_CONFIG glib-2.0 --variable=glib_genmarshal`
AC_SUBST(GLIB_GENMARSHAL)
+# -----------------------------------------------------------
+# Check for code generation tools
+# -----------------------------------------------------------
+XSLTPROC=
AC_CHECK_PROGS([XSLTPROC], [xsltproc])
if test -z "$XSLTPROC"; then
AC_MSG_ERROR([xsltproc (from libxslt) is required])
fi
+# -----------------------------------------------------------
+# Check for a Python >= 2.5 with Twisted, to run the tests
+# -----------------------------------------------------------
+AM_PATH_PYTHON([2.5])
+AC_MSG_CHECKING([for Python with Twisted])
+if $PYTHON -c "import twisted.internet.reactor" >/dev/null 2>&1; then
+ TEST_PYTHON="$PYTHON"
+else
+ TEST_PYTHON=false
+fi
+AC_MSG_RESULT([$TEST_PYTHON])
+AC_SUBST(TEST_PYTHON)
+AM_CONDITIONAL([WANT_TWISTED_TESTS], test false != "$TEST_PYTHON")
+
# -----------------------------------------------------------
# Error flags
@@ -126,7 +146,8 @@ PKG_CHECK_MODULES(LIBTPL,
PKG_CHECK_MODULES(TPL,
[
- dbus-glib-1
+ dbus-1 >= $DBUS_REQUIRED
+ dbus-glib-1 >= $DBUS_GLIB_REQUIRED
glib-2.0 >= $GLIB_REQUIRED
gobject-2.0
libxml-2.0
@@ -150,7 +171,7 @@ fi
# -----------------------------------------------------------
AC_ARG_ENABLE(coding-style-checks,
AC_HELP_STRING([--disable-coding-style-checks],
- [don't check coding style using grep]),
+ [do not check coding style using grep]),
[ENABLE_CODING_STYLE_CHECKS=$enableval], [ENABLE_CODING_STYLE_CHECKS=yes])
AC_SUBST([ENABLE_CODING_STYLE_CHECKS])
@@ -165,6 +186,7 @@ AC_OUTPUT([
telepathy-logger/Makefile
telepathy-logger/libtelepathy-logger.pc
tools/Makefile
+ tests/Makefile
shave
shave-libtool
])
@@ -177,4 +199,5 @@ Configure summary:
Prefix......................: ${prefix}
Shaved build................: ${enable_shave}
Coding style checks.........: ${ENABLE_CODING_STYLE_CHECKS}
+ Python Twisted tests........: ${TEST_PYTHON}
"
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..96ae708
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,48 @@
+if WANT_TWISTED_TESTS
+CHECKTWISTED = twisted
+endif
+
+#SUBDIRS = $(CHECKTWISTED) suppressions
+
+noinst_PROGRAMS = \
+ test-tpl-channel \
+ test-tpl-conf \
+ test-tpl-log-entry \
+ test-tpl-observer
+
+LDADD = \
+ $(top_builddir)/telepathy-logger/libtelepathy-logger.la \
+ $(LIBTPL_LIBS)
+
+AM_CFLAGS = $(ERROR_CFLAGS) $(LIBTPL_CFLAGS) \
+ -I $(top_srcdir)/telepathy-logger -I $(top_builddir)/src
+
+TESTS = $(noinst_PROGRAMS)
+
+TESTS_ENVIRONMENT = G_DEBUG=fatal-warnings,fatal-criticals
+
+check-valgrind: $(TESTS)
+ G_SLICE=always-malloc \
+ G_DEBUG=gc-friendly \
+ $(MAKE) \
+ TESTS_ENVIRONMENT="$(TESTS_ENVIRONMENT) \
+ libtool --mode=execute valgrind \
+ --leak-check=full \
+ --show-reachable=no \
+ --gen-suppressions=all \
+ --num-callers=20 \
+ --suppressions=@abs_top_srcdir@/tests/suppressions/gabble.supp \
+ --suppressions=@abs_top_srcdir@/tests/suppressions/tp-glib.supp \
+ --error-exitcode=1" \
+ check-TESTS
+
+check_c_sources = \
+ $(dbus_test_sources) \
+ tpl-channel-child.c \
+ test-tpl-channel.c \
+ test-tpl-conf.c \
+ test-tpl-log-entry.c \
+ test-tpl-observer.c
+
+include $(top_srcdir)/tools/check-coding-style.mk
+check-local: check-coding-style
diff --git a/tests/README b/tests/README
new file mode 100644
index 0000000..c9da6a5
--- /dev/null
+++ b/tests/README
@@ -0,0 +1,86 @@
+To run all tests:
+
+ make check
+
+or with coverage info:
+
+ ./configure --enable-compiler-coverage
+ make lcov-check
+
+== C tests ==
+
+To run all C tests (assuming the current directory is $top_srcdir):
+
+ make -C tests check-TESTS
+
+To run an individual test:
+
+ make -C tests check-TESTS TESTS=test-handles
+
+To run tests under Valgrind:
+
+ make -C tests check-valgrind
+
+To run an individual test under Valgrind:
+
+ make -C tests check-valgrind TESTS=test-handles
+
+== Twisted tests ==
+
+To run all Twisted tests:
+
+ make check-twisted
+
+To run an individual Twisted test:
+
+ make -C tests/twisted check-twisted TWISTED_TESTS=test-connect.py
+
+or:
+
+ cd tests/twisted
+ sh tools/with-session-bus.sh --config-file=tools/tmp-session-bus.conf \
+ -- python test-connect.py
+
+To run with debug information:
+
+ make -C tests/twisted check-twisted TWISTED_TESTS=test-connect.py \
+ CHECK_TWISTED_VERBOSE=1
+
+or:
+
+ cd tests/twisted
+ sh tools/with-session-bus.sh --config-file=tools/tmp-session-bus.conf \
+ -- python test-connect.py -v
+
+To debug an individual test you can set one of the following env variable:
+
+ * GABBLE_TEST_VALGRIND : to run Gabble inside valgrind. The report is
+ added to tools/gabble-testing.log.
+ export GABBLE_TEST_VALGRIND=1
+
+ * GABBLE_TEST_REFDBG : to run Gabble inside refdbg. The report is written
+ to tools/refdbg.log. You can change GABBLE_WRAPPER to use an alternative
+ refdbg and change REFDBG_OPTIONS to set your own parameters. Example:
+ export GABBLE_TEST_REFDBG=1
+ export GABBLE_WRAPPER="/path/to/refdbg"
+ export REFDBG_OPTIONS="btnum=16"
+
+ * GABBLE_WRAPPER="nemiver" : to run Gabble inside the graphical debugger
+ nemiver. You'll be able to set up breakpoints; then hit the "continue"
+ button to launch Gabble.
+
+ * GABBLE_TEST_STRACE : to run Gabble inside strace. The report is written
+ to tools/strace.log.
+ export GABBLE_TEST_STRACE=1
+
+
+== Jingle tests ==
+
+Various jingle tests run the same tests with different dialects. To only test
+certain dialects use the JINGLE_DIALECTS environment variable. Currently
+supported are jingle015, jingle031, gtalk03 and gtalk04. For example to only
+run generic tests with the two jingle dialects you can do:
+
+ make -C tests/twisted check-twisted TWISTED_TESTS=jingle/\*.py \
+ JINGLE_DIALECTS=jingle015,jingle031
+
diff --git a/tests/suppressions/Makefile.am b/tests/suppressions/Makefile.am
new file mode 100644
index 0000000..92b72a8
--- /dev/null
+++ b/tests/suppressions/Makefile.am
@@ -0,0 +1,2 @@
+EXTRA_DIST = tpl.supp
+
diff --git a/tests/suppressions/tpl.supp b/tests/suppressions/tpl.supp
new file mode 100644
index 0000000..18d0dac
--- /dev/null
+++ b/tests/suppressions/tpl.supp
@@ -0,0 +1,250 @@
+# Valgrind error suppression file
+
+# ============================= libc ==================================
+
+{
+ ld.so initialization + selinux
+ Memcheck:Leak
+ ...
+ fun:_dl_init
+ obj:/lib/ld-*.so
+}
+
+{
+ dlopen initialization, triggered by handle-leak-debug code
+ Memcheck:Leak
+ ...
+ fun:__libc_dlopen_mode
+ fun:init
+ fun:backtrace
+ fun:handle_leak_debug_bt
+ fun:dynamic_ensure_handle
+ fun:tp_handle_ensure
+}
+
+# ============================= GLib ==================================
+
+{
+ g_set_prgname copies its argument
+ Memcheck:Leak
+ ...
+ fun:g_set_prgname
+}
+
+{
+ one g_get_charset per child^Wprocess
+ Memcheck:Leak
+ ...
+ fun:g_get_charset
+}
+
+{
+ GQuarks can't be freed
+ Memcheck:Leak
+ ...
+ fun:g_quark_from_static_string
+}
+
+{
+ GQuarks can't be freed
+ Memcheck:Leak
+ ...
+ fun:g_quark_from_string
+}
+
+{
+ interned strings can't be freed
+ Memcheck:Leak
+ ...
+ fun:g_intern_string
+}
+
+{
+ interned strings can't be freed
+ Memcheck:Leak
+ ...
+ fun:g_intern_static_string
+}
+
+{
+ shared global default g_main_context
+ Memcheck:Leak
+ ...
+ fun:g_main_context_new
+ fun:g_main_context_default
+}
+
+{
+ GTest initialization
+ Memcheck:Leak
+ ...
+ fun:g_test_init
+ fun:main
+}
+
+{
+ GTest admin
+ Memcheck:Leak
+ ...
+ fun:g_test_add_vtable
+}
+
+{
+ GTest pseudorandomness
+ Memcheck:Leak
+ ...
+ fun:g_rand_new_with_seed_array
+ fun:test_run_seed
+ ...
+ fun:g_test_run
+}
+
+{
+ GSLice initialization
+ Memcheck:Leak
+ ...
+ fun:g_malloc0
+ fun:g_slice_init_nomessage
+ fun:g_slice_alloc
+}
+
+# ============================= GObject ===============================
+
+{
+ g_type_init
+ Memcheck:Leak
+ ...
+ fun:g_type_init
+}
+
+{
+ g_type_register_static
+ Memcheck:Leak
+ ...
+ fun:g_type_register_static
+}
+
+# ============================= dbus-glib =============================
+
+{
+ dbus-glib, https://bugs.freedesktop.org/show_bug.cgi?id=14125
+ Memcheck:Addr4
+ fun:g_hash_table_foreach
+ obj:/usr/lib/libdbus-glib-1.so.2.1.0
+ fun:g_object_run_dispose
+}
+
+{
+ registering marshallers is permanent
+ Memcheck:Leak
+ ...
+ fun:dbus_g_object_register_marshaller_array
+ fun:dbus_g_object_register_marshaller
+}
+
+{
+ dbus-glib specialized GTypes are permanent
+ Memcheck:Leak
+ ...
+ fun:dbus_g_type_specialized_init
+}
+
+{
+ libdbus shared connection
+ Memcheck:Leak
+ ...
+ fun:dbus_g_bus_get
+}
+
+{
+ dbus-gobject registrations aren't freed unless we fall off the bus
+ Memcheck:Leak
+ ...
+ fun:g_slist_append
+ fun:dbus_g_connection_register_g_object
+}
+
+{
+ DBusGProxy slots aren't freed unless we fall off the bus
+ Memcheck:Leak
+ ...
+ fun:dbus_connection_allocate_data_slot
+ ...
+ fun:dbus_g_proxy_constructor
+}
+
+{
+ error registrations are for life, not just for Christmas
+ Memcheck:Leak
+ ...
+ fun:dbus_g_error_domain_register
+}
+
+# ============================= telepathy-glib ========================
+
+{
+ tp_dbus_daemon_constructor @daemons once per DBusConnection
+ Memcheck:Leak
+ ...
+ fun:g_slice_alloc
+ fun:tp_dbus_daemon_constructor
+}
+
+{
+ tp_proxy_subclass_add_error_mapping refs the enum
+ Memcheck:Leak
+ ...
+ fun:g_type_class_ref
+ fun:tp_proxy_subclass_add_error_mapping
+}
+
+{
+ tp_proxy_or_subclass_hook_on_interface_add never frees its list
+ Memcheck:Leak
+ ...
+ fun:tp_proxy_or_subclass_hook_on_interface_add
+}
+
+{
+ tp_dbus_daemon_constructor filter not freed til we fall off the bus
+ Memcheck:Leak
+ ...
+ fun:dbus_connection_add_filter
+ fun:tp_dbus_daemon_constructor
+}
+
+# ============================= unclassified ==========================
+
+{
+ creating param specs in tp_proxy_class_intern_init
+ Memcheck:Leak
+ fun:memalign
+ fun:posix_memalign
+ fun:slab_allocator_alloc_chunk
+ fun:g_slice_alloc
+ fun:g_slice_alloc0
+ fun:g_type_create_instance
+ fun:g_param_spec_internal
+ fun:g_param_spec_string
+}
+
+{
+ ld.so initialization on glibc 2.9
+ Memcheck:Cond
+ fun:_dl_relocate_object
+ fun:dl_main
+ fun:_dl_sysdep_start
+ fun:_dl_start
+ obj:/lib/ld-2.9.so
+}
+
+{
+ ld.so initialization on glibc 2.9
+ Memcheck:Cond
+ fun:strlen
+ fun:_dl_init_paths
+ fun:dl_main
+ fun:_dl_sysdep_start
+ fun:_dl_start
+ obj:/lib/ld-2.9.so
+}
diff --git a/tests/test-basic-connect.sh b/tests/test-basic-connect.sh
new file mode 100644
index 0000000..93af74d
--- /dev/null
+++ b/tests/test-basic-connect.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+./run-with-tmp-session-bus.sh `pwd`/test-basic-connect.py BasicConnectTest
diff --git a/tests/test-tpl-channel.c b/tests/test-tpl-channel.c
new file mode 100644
index 0000000..ed0f442
--- /dev/null
+++ b/tests/test-tpl-channel.c
@@ -0,0 +1,29 @@
+#include <telepathy-logger/conf.h>
+
+#define gconf_client_get_bool(obj,key,err) g_print ("%s", key)
+
+int main (int argc, char **argv)
+{
+ TplChannelTest *chan;
+ GError *error;
+
+ g_type_init ();
+
+ tp_connection_new (
+
+ chan = tpl_channel_test_new (conn, path, props, acc, &err);
+
+ /* TplConf is a singleton, be sure both point to the same memory */
+ conf2 = tpl_conf_dup ();
+ g_assert (conf == conf2);
+
+ /* unref the second singleton pointer and check that the it is still
+ * valid: checking correct object ref-counting after each _dup() call */
+ g_object_unref (conf2);
+ g_assert (TPL_IS_CONF (conf));
+
+ g_object_unref (conf);
+
+ return 0;
+}
+
diff --git a/tests/test-tpl-conf b/tests/test-tpl-conf
new file mode 100755
index 0000000..d039565
--- /dev/null
+++ b/tests/test-tpl-conf
@@ -0,0 +1,148 @@
+#! /bin/sh
+
+# test-tpl-conf - temporary wrapper script for .libs/test-tpl-conf
+# Generated by ltmain.sh (GNU libtool) 2.2.6b Debian-2.2.6b-2
+#
+# The test-tpl-conf program cannot be directly executed until all the libtool
+# libraries that it depends on are installed.
+#
+# This wrapper script should never be moved out of the build directory.
+# If it is, it will not operate correctly.
+
+# Sed substitution that helps us do robust quoting. It backslashifies
+# metacharacters that are still active within double-quoted strings.
+Xsed='/bin/sed -e 1s/^X//'
+sed_quote_subst='s/\([`"$\\]\)/\\\1/g'
+
+# Be Bourne compatible
+if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then
+ emulate sh
+ NULLCMD=:
+ # Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which
+ # is contrary to our usage. Disable this feature.
+ alias -g '${1+"$@"}'='"$@"'
+ setopt NO_GLOB_SUBST
+else
+ case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac
+fi
+BIN_SH=xpg4; export BIN_SH # for Tru64
+DUALCASE=1; export DUALCASE # for MKS sh
+
+# The HP-UX ksh and POSIX shell print the target directory to stdout
+# if CDPATH is set.
+(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
+
+relink_command="(cd /home/kalfa/src/telepathy-logger/tests; { test -z \"\${LIBRARY_PATH+set}\" || unset LIBRARY_PATH || { LIBRARY_PATH=; export LIBRARY_PATH; }; }; { test -z \"\${COMPILER_PATH+set}\" || unset COMPILER_PATH || { COMPILER_PATH=; export COMPILER_PATH; }; }; { test -z \"\${GCC_EXEC_PREFIX+set}\" || unset GCC_EXEC_PREFIX || { GCC_EXEC_PREFIX=; export GCC_EXEC_PREFIX; }; }; { test -z \"\${LD_RUN_PATH+set}\" || unset LD_RUN_PATH || { LD_RUN_PATH=; export LD_RUN_PATH; }; }; { test -z \"\${LD_LIBRARY_PATH+set}\" || unset LD_LIBRARY_PATH || { LD_LIBRARY_PATH=; export LD_LIBRARY_PATH; }; }; PATH=/usr/local/bin:/usr/bin:/bin:/usr/games:/sbin; export PATH; gcc -Wall -Werror -Wextra -Wno-missing-field-initializers -Wno-unused-parameter -D_POSIX_SOURCE -std=c99 -Wshadow -Wmissing-prototypes -Wmissing-declarations -Wstrict-prototypes -DORBIT2=1 -pthread -I/usr/include/gconf/2 -I/usr/include/orbit-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/libxml2 -I/usr/include/telepathy-1.0 -I ../telepathy-logger -I ../src -g -O2 -o \$progdir/\$file test-tpl-conf.o ../telepathy-logger/.libs/libtelepathy-logger.so /usr/lib/libgconf-2.so /usr/lib/libgio-2.0.so /usr/lib/libgmodule-2.0.so /usr/lib/libgobject-2.0.so /usr/lib/libglib-2.0.so /usr/lib/libxml2.so -ltelepathy-glib -pthread -Wl,-rpath -Wl,/home/kalfa/src/telepathy-logger/telepathy-logger/.libs)"
+
+# This environment variable determines our operation mode.
+if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then
+ # install mode needs the following variables:
+ generated_by_libtool_version='2.2.6b'
+ notinst_deplibs=' ../telepathy-logger/libtelepathy-logger.la'
+else
+ # When we are sourced in execute mode, $file and $ECHO are already set.
+ if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
+ ECHO="echo"
+ file="$0"
+ # Make sure echo works.
+ if test "X$1" = X--no-reexec; then
+ # Discard the --no-reexec flag, and continue.
+ shift
+ elif test "X`{ $ECHO '\t'; } 2>/dev/null`" = 'X\t'; then
+ # Yippee, $ECHO works!
+ :
+ else
+ # Restart under the correct shell, and then maybe $ECHO will work.
+ exec /bin/sh "$0" --no-reexec ${1+"$@"}
+ fi
+ fi
+
+ # Find the directory that this script lives in.
+ thisdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
+ test "x$thisdir" = "x$file" && thisdir=.
+
+ # Follow symbolic links until we get to the real thisdir.
+ file=`ls -ld "$file" | /bin/sed -n 's/.*-> //p'`
+ while test -n "$file"; do
+ destdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
+
+ # If there was a directory component, then change thisdir.
+ if test "x$destdir" != "x$file"; then
+ case "$destdir" in
+ [\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;;
+ *) thisdir="$thisdir/$destdir" ;;
+ esac
+ fi
+
+ file=`$ECHO "X$file" | $Xsed -e 's%^.*/%%'`
+ file=`ls -ld "$thisdir/$file" | /bin/sed -n 's/.*-> //p'`
+ done
+
+
+ # Usually 'no', except on cygwin/mingw when embedded into
+ # the cwrapper.
+ WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no
+ if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then
+ # special case for '.'
+ if test "$thisdir" = "."; then
+ thisdir=`pwd`
+ fi
+ # remove .libs from thisdir
+ case "$thisdir" in
+ *[\\/].libs ) thisdir=`$ECHO "X$thisdir" | $Xsed -e 's%[\\/][^\\/]*$%%'` ;;
+ .libs ) thisdir=. ;;
+ esac
+ fi
+
+ # Try to get the absolute directory name.
+ absdir=`cd "$thisdir" && pwd`
+ test -n "$absdir" && thisdir="$absdir"
+
+ program=lt-'test-tpl-conf'
+ progdir="$thisdir/.libs"
+
+ if test ! -f "$progdir/$program" ||
+ { file=`ls -1dt "$progdir/$program" "$progdir/../$program" 2>/dev/null | /bin/sed 1q`; \
+ test "X$file" != "X$progdir/$program"; }; then
+
+ file="$$-$program"
+
+ if test ! -d "$progdir"; then
+ mkdir "$progdir"
+ else
+ rm -f "$progdir/$file"
+ fi
+
+ # relink executable if necessary
+ if test -n "$relink_command"; then
+ if relink_command_output=`eval $relink_command 2>&1`; then :
+ else
+ echo "$relink_command_output" >&2
+ rm -f "$progdir/$file"
+ exit 1
+ fi
+ fi
+
+ mv -f "$progdir/$file" "$progdir/$program" 2>/dev/null ||
+ { rm -f "$progdir/$program";
+ mv -f "$progdir/$file" "$progdir/$program"; }
+ rm -f "$progdir/$file"
+ fi
+
+ if test -f "$progdir/$program"; then
+ if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
+ # Run the actual program with our arguments.
+
+ exec "$progdir/$program" ${1+"$@"}
+
+ $ECHO "$0: cannot exec $program $*" 1>&2
+ exit 1
+ fi
+ else
+ # The program doesn't exist.
+ $ECHO "$0: error: \`$progdir/$program' does not exist" 1>&2
+ $ECHO "This script is just a wrapper for $program." 1>&2
+ echo "See the libtool documentation for more information." 1>&2
+ exit 1
+ fi
+fi
diff --git a/tests/test-tpl-conf.c b/tests/test-tpl-conf.c
new file mode 100644
index 0000000..2319c34
--- /dev/null
+++ b/tests/test-tpl-conf.c
@@ -0,0 +1,30 @@
+#include <telepathy-logger/conf.h>
+
+int
+main (int argc, char **argv)
+{
+ TplConf *conf, *conf2;
+
+ g_type_init ();
+
+ conf = tpl_conf_dup ();
+
+ /* TplConf is a singleton, be sure both point to the same memory */
+ conf2 = tpl_conf_dup ();
+ g_assert (conf == conf2);
+
+ /* unref the second singleton pointer and check that the it is still
+ * valid: checking correct object ref-counting after each _dup() call */
+ g_object_unref (conf2);
+ g_assert (TPL_IS_CONF (conf));
+
+ /* it points to the same mem area, it should be still valid */
+ g_assert (TPL_IS_CONF (conf2));
+
+ /* proper disposal for the singleton when no references are present */
+ g_object_unref (conf);
+ g_assert (!TPL_IS_CONF (conf));
+
+ return 0;
+}
+
diff --git a/tests/test-tpl-log-entry.c b/tests/test-tpl-log-entry.c
new file mode 100644
index 0000000..b14d549
--- /dev/null
+++ b/tests/test-tpl-log-entry.c
@@ -0,0 +1,27 @@
+#include <telepathy-logger/conf.h>
+
+#define gconf_client_get_bool(obj,key,err) g_print ("%s", key)
+
+int main (int argc, char **argv)
+{
+ TplConf *conf, *conf2;
+ GError *error;
+
+ g_type_init ();
+
+ conf = tpl_conf_dup ();
+
+ /* TplConf is a singleton, be sure both point to the same memory */
+ conf2 = tpl_conf_dup ();
+ g_assert (conf == conf2);
+
+ /* unref the second singleton pointer and check that the it is still
+ * valid: checking correct object ref-counting after each _dup() call */
+ g_object_unref (conf2);
+ g_assert (TPL_IS_CONF (conf));
+
+ g_object_unref (conf);
+
+ return 0;
+}
+
diff --git a/tests/test-tpl-observer b/tests/test-tpl-observer
new file mode 100755
index 0000000..4aff27e
--- /dev/null
+++ b/tests/test-tpl-observer
@@ -0,0 +1,148 @@
+#! /bin/sh
+
+# test-tpl-observer - temporary wrapper script for .libs/test-tpl-observer
+# Generated by ltmain.sh (GNU libtool) 2.2.6b Debian-2.2.6b-2
+#
+# The test-tpl-observer program cannot be directly executed until all the libtool
+# libraries that it depends on are installed.
+#
+# This wrapper script should never be moved out of the build directory.
+# If it is, it will not operate correctly.
+
+# Sed substitution that helps us do robust quoting. It backslashifies
+# metacharacters that are still active within double-quoted strings.
+Xsed='/bin/sed -e 1s/^X//'
+sed_quote_subst='s/\([`"$\\]\)/\\\1/g'
+
+# Be Bourne compatible
+if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then
+ emulate sh
+ NULLCMD=:
+ # Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which
+ # is contrary to our usage. Disable this feature.
+ alias -g '${1+"$@"}'='"$@"'
+ setopt NO_GLOB_SUBST
+else
+ case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac
+fi
+BIN_SH=xpg4; export BIN_SH # for Tru64
+DUALCASE=1; export DUALCASE # for MKS sh
+
+# The HP-UX ksh and POSIX shell print the target directory to stdout
+# if CDPATH is set.
+(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
+
+relink_command="(cd /home/kalfa/src/telepathy-logger/tests; { test -z \"\${LIBRARY_PATH+set}\" || unset LIBRARY_PATH || { LIBRARY_PATH=; export LIBRARY_PATH; }; }; { test -z \"\${COMPILER_PATH+set}\" || unset COMPILER_PATH || { COMPILER_PATH=; export COMPILER_PATH; }; }; { test -z \"\${GCC_EXEC_PREFIX+set}\" || unset GCC_EXEC_PREFIX || { GCC_EXEC_PREFIX=; export GCC_EXEC_PREFIX; }; }; { test -z \"\${LD_RUN_PATH+set}\" || unset LD_RUN_PATH || { LD_RUN_PATH=; export LD_RUN_PATH; }; }; { test -z \"\${LD_LIBRARY_PATH+set}\" || unset LD_LIBRARY_PATH || { LD_LIBRARY_PATH=; export LD_LIBRARY_PATH; }; }; PATH=/usr/local/bin:/usr/bin:/bin:/usr/games:/sbin; export PATH; gcc -Wall -Werror -Wextra -Wno-missing-field-initializers -Wno-unused-parameter -D_POSIX_SOURCE -std=c99 -Wshadow -Wmissing-prototypes -Wmissing-declarations -Wstrict-prototypes -DORBIT2=1 -pthread -I/usr/include/gconf/2 -I/usr/include/orbit-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/libxml2 -I/usr/include/telepathy-1.0 -I ../telepathy-logger -I ../src -g -O2 -o \$progdir/\$file test-tpl-observer.o ../telepathy-logger/.libs/libtelepathy-logger.so /usr/lib/libgconf-2.so /usr/lib/libgio-2.0.so /usr/lib/libgmodule-2.0.so /usr/lib/libgobject-2.0.so /usr/lib/libglib-2.0.so /usr/lib/libxml2.so -ltelepathy-glib -pthread -Wl,-rpath -Wl,/home/kalfa/src/telepathy-logger/telepathy-logger/.libs)"
+
+# This environment variable determines our operation mode.
+if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then
+ # install mode needs the following variables:
+ generated_by_libtool_version='2.2.6b'
+ notinst_deplibs=' ../telepathy-logger/libtelepathy-logger.la'
+else
+ # When we are sourced in execute mode, $file and $ECHO are already set.
+ if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
+ ECHO="echo"
+ file="$0"
+ # Make sure echo works.
+ if test "X$1" = X--no-reexec; then
+ # Discard the --no-reexec flag, and continue.
+ shift
+ elif test "X`{ $ECHO '\t'; } 2>/dev/null`" = 'X\t'; then
+ # Yippee, $ECHO works!
+ :
+ else
+ # Restart under the correct shell, and then maybe $ECHO will work.
+ exec /bin/sh "$0" --no-reexec ${1+"$@"}
+ fi
+ fi
+
+ # Find the directory that this script lives in.
+ thisdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
+ test "x$thisdir" = "x$file" && thisdir=.
+
+ # Follow symbolic links until we get to the real thisdir.
+ file=`ls -ld "$file" | /bin/sed -n 's/.*-> //p'`
+ while test -n "$file"; do
+ destdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
+
+ # If there was a directory component, then change thisdir.
+ if test "x$destdir" != "x$file"; then
+ case "$destdir" in
+ [\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;;
+ *) thisdir="$thisdir/$destdir" ;;
+ esac
+ fi
+
+ file=`$ECHO "X$file" | $Xsed -e 's%^.*/%%'`
+ file=`ls -ld "$thisdir/$file" | /bin/sed -n 's/.*-> //p'`
+ done
+
+
+ # Usually 'no', except on cygwin/mingw when embedded into
+ # the cwrapper.
+ WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no
+ if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then
+ # special case for '.'
+ if test "$thisdir" = "."; then
+ thisdir=`pwd`
+ fi
+ # remove .libs from thisdir
+ case "$thisdir" in
+ *[\\/].libs ) thisdir=`$ECHO "X$thisdir" | $Xsed -e 's%[\\/][^\\/]*$%%'` ;;
+ .libs ) thisdir=. ;;
+ esac
+ fi
+
+ # Try to get the absolute directory name.
+ absdir=`cd "$thisdir" && pwd`
+ test -n "$absdir" && thisdir="$absdir"
+
+ program=lt-'test-tpl-observer'
+ progdir="$thisdir/.libs"
+
+ if test ! -f "$progdir/$program" ||
+ { file=`ls -1dt "$progdir/$program" "$progdir/../$program" 2>/dev/null | /bin/sed 1q`; \
+ test "X$file" != "X$progdir/$program"; }; then
+
+ file="$$-$program"
+
+ if test ! -d "$progdir"; then
+ mkdir "$progdir"
+ else
+ rm -f "$progdir/$file"
+ fi
+
+ # relink executable if necessary
+ if test -n "$relink_command"; then
+ if relink_command_output=`eval $relink_command 2>&1`; then :
+ else
+ echo "$relink_command_output" >&2
+ rm -f "$progdir/$file"
+ exit 1
+ fi
+ fi
+
+ mv -f "$progdir/$file" "$progdir/$program" 2>/dev/null ||
+ { rm -f "$progdir/$program";
+ mv -f "$progdir/$file" "$progdir/$program"; }
+ rm -f "$progdir/$file"
+ fi
+
+ if test -f "$progdir/$program"; then
+ if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
+ # Run the actual program with our arguments.
+
+ exec "$progdir/$program" ${1+"$@"}
+
+ $ECHO "$0: cannot exec $program $*" 1>&2
+ exit 1
+ fi
+ else
+ # The program doesn't exist.
+ $ECHO "$0: error: \`$progdir/$program' does not exist" 1>&2
+ $ECHO "This script is just a wrapper for $program." 1>&2
+ echo "See the libtool documentation for more information." 1>&2
+ exit 1
+ fi
+fi
diff --git a/tests/test-tpl-observer.c b/tests/test-tpl-observer.c
new file mode 100644
index 0000000..ea8db70
--- /dev/null
+++ b/tests/test-tpl-observer.c
@@ -0,0 +1,31 @@
+#include <telepathy-logger/observer.h>
+
+int
+main (int argc, char **argv)
+{
+ TplObserver *obs, *obs2;
+
+ g_type_init ();
+
+ obs = tpl_observer_new ();
+
+ /* TplObserver is a singleton, be sure both point to the same memory */
+ obs2 = tpl_observer_new ();
+ g_assert (obs == obs2);
+
+ /* unref the second singleton pointer and check that the it is still
+ * valid: checking correct object ref-counting after each _dup() call */
+ g_object_unref (obs2);
+ g_assert (TPL_IS_OBSERVER (obs));
+
+ /* it points to the same mem area, it should be still valid */
+ g_assert (TPL_IS_OBSERVER (obs2));
+
+ /* proper disposal for the singleton when no references are present */
+ g_object_unref (obs);
+ g_assert (!TPL_IS_OBSERVER (obs));
+
+
+ return 0;
+}
+
diff --git a/tests/tpl-channel-child.c b/tests/tpl-channel-child.c
new file mode 100644
index 0000000..3342663
--- /dev/null
+++ b/tests/tpl-channel-child.c
@@ -0,0 +1,665 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2009 Collabora Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Authors: Cosimo Alfarano <cosimo.alfarano@collabora.co.uk>
+ */
+
+/*
+ * This object acts as a Text Channel context, handling a automaton to
+ * set up all the needed information before connect to Text iface
+ * signals.
+ */
+
+#include "channel-text.h"
+
+#include <telepathy-logger/channel.h>
+
+#define GET_PRIV(obj) TPL_GET_PRIV (obj, TplChannelTest)
+struct _TplChannelTestPriv
+{
+};
+
+G_DEFINE_TYPE (TplChannelTest, tpl_channel_test, TPL_TYPE_CHANNEL)
+
+static void tpl_channel_test_dispose (GObject *obj)
+{
+ G_OBJECT_CLASS (tpl_channel_test_parent_class)->dispose (obj);
+}
+
+static void
+tpl_channel_test_finalize (GObject *obj)
+{
+ G_OBJECT_CLASS (tpl_channel_test_parent_class)->finalize (obj);
+}
+
+
+static void
+tpl_channel_test_class_init (TplChannelTestClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ TplChannelClass *tpl_chan_class = TPL_CHANNEL_CLASS (klass);
+
+ object_class->dispose = tpl_channel_test_dispose;
+ object_class->finalize = tpl_channel_test_finalize;
+
+ tpl_chan_class->call_when_ready = call_when_ready_wrapper;
+
+ g_type_class_add_private (object_class, sizeof (TplChannelTestPriv));
+}
+
+
+static void
+tpl_channel_test_init (TplChannelTest *self)
+{
+ TplChannelTestPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+ TPL_CHANNEL_TEST, TplChannelTestPriv);
+ self->priv = priv;
+}
+
+
+/**
+ * @conn: TpConnection instance owning the channel
+ * @object_path: the channel's DBus path
+ * @tp_chan_props: channel's immutable properties, obtained for example by
+ * %tp_channel_borrow_immutable_properties()
+ * @error: location of the GError, used in case a problem is raised while
+ * creating the channel
+ *
+ * Convenience function to create a new TPL Channel Text proxy. The returned
+ * #TplChannelTest is not guaranteed to be ready at the point of return. Use #TpChannel
+ * methods casting the #TplChannelTest instance to a TpChannel
+ *
+ * TplChannelTest instances are subclasses or the abstract TplChannel which is
+ * subclass of TpChannel.
+ *
+ * Returns: the TplChannelTest instance or %NULL in @object_path is not valid
+ */
+
+TplChannelTest *
+tpl_channel_test_new (TpConnection *conn,
+ const gchar *object_path,
+ GHashTable *tp_chan_props,
+ TpAccount *account,
+ GError **error)
+{
+ TpProxy *conn_proxy = TP_PROXY (conn);
+
+ /* Do what tpl_channel_new does + set TplChannelTest specific properties */
+
+ g_return_val_if_fail (TP_IS_CONNECTION (conn), NULL);
+ g_return_val_if_fail (TP_IS_ACCOUNT (account), NULL);
+ g_return_val_if_fail (!TPL_STR_EMPTY (object_path), NULL);
+ g_return_val_if_fail (tp_chan_props != NULL, NULL);
+
+ if (!tp_dbus_check_valid_object_path (object_path, error))
+ return NULL;
+
+ return g_object_new (TPL_CHANNEL_TEST,
+ /* TplChannel properties */
+ "account", account,
+ /* TpChannel properties */
+ "connection", conn,
+ "dbus-daemon", conn_proxy->dbus_daemon,
+ "bus-name", conn_proxy->bus_name,
+ "object-path", object_path,
+ "handle-type", (guint) TP_UNKNOWN_HANDLE_TYPE,
+ "channel-properties", tp_chan_props,
+ NULL);
+}
+
+
+TpContact *
+tpl_channel_test_get_remote_contact (TplChannelTest *self)
+{
+ TplChannelTestPriv *priv = GET_PRIV (self);
+
+ g_return_val_if_fail (TPL_IS_CHANNEL_TEXT (self), NULL);
+
+ return priv->remote_contact;
+}
+
+TpContact *
+tpl_channel_test_get_my_contact (TplChannelTest *self)
+{
+ TplChannelTestPriv *priv = GET_PRIV(self);
+
+ g_return_val_if_fail (TPL_IS_CHANNEL_TEXT (self), NULL);
+
+ return priv->my_contact;
+}
+
+gboolean
+tpl_channel_test_is_chatroom (TplChannelTest *self)
+{
+ TplChannelTestPriv *priv = GET_PRIV(self);
+
+ g_return_val_if_fail(TPL_IS_CHANNEL_TEXT (self), FALSE);
+
+ return priv->chatroom;
+}
+
+const gchar *
+tpl_channel_test_get_chatroom_id (TplChannelTest *self)
+{
+ TplChannelTestPriv *priv = GET_PRIV(self);
+
+ g_return_val_if_fail(TPL_IS_CHANNEL_TEXT (self), NULL);
+
+ return priv->chatroom_id;
+}
+
+void
+tpl_channel_test_set_remote_contact (TplChannelTest *self,
+ TpContact *data)
+{
+ TplChannelTestPriv *priv = GET_PRIV(self);
+
+ g_return_if_fail (TPL_IS_CHANNEL_TEXT (self));
+ g_return_if_fail (TP_IS_CONTACT (data));
+ g_return_if_fail (priv->remote_contact == NULL);
+
+ priv->remote_contact = data;
+ g_object_ref (data);
+}
+
+void
+tpl_channel_test_set_my_contact (TplChannelTest *self,
+ TpContact *data)
+{
+ TplChannelTestPriv *priv = GET_PRIV(self);
+
+ g_return_if_fail (TPL_IS_CHANNEL_TEXT (self));
+ g_return_if_fail (TP_IS_CONTACT (data));
+ g_return_if_fail (priv->my_contact == NULL);
+
+ priv->my_contact = data;
+ g_object_ref (data);
+}
+
+void
+tpl_channel_test_set_chatroom (TplChannelTest *self,
+ gboolean data)
+{
+ TplChannelTestPriv *priv = GET_PRIV(self);
+
+ g_return_if_fail (TPL_IS_CHANNEL_TEXT (self));
+
+ priv->chatroom = data;
+}
+
+
+void
+tpl_channel_test_set_chatroom_id (TplChannelTest *self,
+ const gchar *data)
+{
+ TplChannelTestPriv *priv = GET_PRIV(self);
+
+ g_return_if_fail (TPL_IS_CHANNEL_TEXT (self));
+ g_return_if_fail (!TPL_STR_EMPTY (data));
+ g_return_if_fail (priv->chatroom_id == NULL);
+ priv->chatroom_id = g_strdup (data);
+}
+
+static void
+call_when_ready_wrapper (TplChannel *tpl_chan,
+ GAsyncReadyCallback cb,
+ gpointer user_data)
+{
+ tpl_channel_test_call_when_ready( TPL_CHANNEL_TEXT (tpl_chan), cb,
+ user_data);
+}
+
+void
+tpl_channel_test_call_when_ready (TplChannelTest *self,
+ GAsyncReadyCallback cb, gpointer user_data)
+{
+ TplActionChain *actions;
+
+ /* first: connect signals, so none are lost
+ * second: prepare all TplChannel
+ * then: us TpContact to cache my contact and the remote one.
+ * If for any reason, the order is changed, it's need to check what objects
+ * are unreferenced by g_object_unref: after the order change, it might
+ * happend that an object still has to be created after the change */
+ actions = tpl_actionchain_new (G_OBJECT (self), cb, user_data);
+ tpl_actionchain_append (actions, pendingproc_connect_signals);
+ tpl_actionchain_append (actions, pendingproc_prepare_tpl_channel);
+ tpl_actionchain_append (actions, pendingproc_get_my_contact);
+ tpl_actionchain_append (actions, pendingproc_get_remote_handle_type);
+ /* start the queue consuming */
+ tpl_actionchain_continue (actions);
+}
+
+
+static void
+pendingproc_prepare_tpl_channel (TplActionChain *ctx)
+{
+ TplChannel *tpl_chan = TPL_CHANNEL (tpl_actionchain_get_object (ctx));
+
+ g_debug ("prepare tpl");
+ TPL_CHANNEL_GET_CLASS (tpl_chan)->call_when_ready_protected (tpl_chan,
+ got_tpl_chan_ready_cb, ctx);
+}
+
+
+static void
+got_tpl_chan_ready_cb (GObject *obj,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ TplActionChain *ctx = user_data;
+ g_debug ("PREPARE");
+
+ if (tpl_actionchain_finish (result) == TRUE)
+ tpl_actionchain_continue (ctx);
+ return;
+}
+
+
+static void
+pendingproc_get_chatroom_id (TplActionChain *ctx)
+{
+ TplChannelTest *tpl_text = tpl_actionchain_get_object (ctx);
+ TplChannel *tpl_chan = TPL_CHANNEL (tpl_text);
+ TpConnection *connection = tp_channel_borrow_connection (TP_CHANNEL (
+ tpl_chan));
+ TpHandle room_handle;
+ GArray *handles;
+
+ handles = g_array_new (FALSE, FALSE, sizeof (TpHandle));
+ room_handle = tp_channel_get_handle (TP_CHANNEL (tpl_chan), NULL);
+ g_array_append_val (handles, room_handle);
+
+ tpl_channel_test_set_chatroom (tpl_text, TRUE);
+ tp_cli_connection_call_inspect_handles (connection,
+ -1, TP_HANDLE_TYPE_ROOM, handles, _tpl_channel_test_get_chatroom_cb,
+ ctx, NULL, NULL);
+
+ g_array_unref (handles);
+}
+
+
+static void
+_tpl_channel_test_get_chatroom_cb (TpConnection *proxy,
+ const gchar **out_Identifiers,
+ const GError *error,
+ gpointer user_data,
+ GObject *weak_object)
+{
+ TplActionChain *ctx = user_data;
+ TplChannelTest *tpl_text = tpl_actionchain_get_object (ctx);
+
+ g_return_if_fail (TPL_IS_CHANNEL_TEXT (tpl_text));
+
+ if (error != NULL)
+ g_error ("retrieving chatroom identifier: %s", error->message);
+
+ g_debug ("Chatroom id: %s", *out_Identifiers);
+ tpl_channel_test_set_chatroom_id (tpl_text, *out_Identifiers);
+
+ tpl_actionchain_continue (ctx);
+}
+
+
+static void
+pendingproc_connect_signals (TplActionChain *ctx)
+{
+ TplChannelTest *tpl_text = tpl_actionchain_get_object (ctx);
+ GError *error = NULL;
+ TpChannel *channel = NULL;
+
+ g_debug ("CONNECT");
+
+ channel = TP_CHANNEL (TPL_CHANNEL (tpl_text));
+
+ tp_cli_channel_type_text_connect_to_received (channel,
+ _channel_on_received_signal_cb, tpl_text, NULL, NULL, &error);
+ if (error != NULL)
+ {
+ g_error ("received signal connect: %s", error->message);
+ g_clear_error (&error);
+ g_error_free (error);
+ error = NULL;
+ }
+
+ tp_cli_channel_type_text_connect_to_sent (channel,
+ _channel_on_sent_signal_cb, tpl_text, NULL, NULL, &error);
+ if (error != NULL)
+ {
+ g_error ("sent signal connect: %s", error->message);
+ g_clear_error (&error);
+ g_error_free (error);
+ error = NULL;
+ }
+
+ tp_cli_channel_type_text_connect_to_send_error (channel,
+ _channel_on_send_error_cb, tpl_text, NULL, NULL, &error);
+ if (error != NULL)
+ {
+ g_error ("send error signal connect: %s", error->message);
+ g_clear_error (&error);
+ g_error_free (error);
+ error = NULL;
+ }
+
+ tp_cli_channel_type_text_connect_to_lost_message (channel,
+ _channel_on_lost_message_cb, tpl_text, NULL, NULL, &error);
+ if (error != NULL)
+ {
+ g_error ("lost message signal connect: %s", error->message);
+ g_clear_error (&error);
+ g_error_free (error);
+ error = NULL;
+ }
+
+ tp_cli_channel_connect_to_closed (channel, _channel_on_closed_cb,
+ tpl_text, NULL, NULL, &error);
+
+ if (error != NULL)
+ {
+ g_error ("channel closed signal connect: %s", error->message);
+ g_clear_error (&error);
+ g_error_free (error);
+ error = NULL;
+ }
+
+ /* TODO connect to TpContacts' notify::presence-type */
+
+ tpl_actionchain_continue (ctx);
+}
+
+
+
+/* Signal's Callbacks */
+static void
+_channel_on_closed_cb (TpChannel *proxy,
+ gpointer user_data,
+ GObject *weak_object)
+{
+ TplChannelTest *tpl_text = TPL_CHANNEL_TEXT (user_data);
+ TplChannel *tpl_chan = TPL_CHANNEL (tpl_text);
+ gchar *chan_path;
+ TplObserver *observer = tpl_observer_new ();
+
+ /* set chan_path from the TpConnection's property */
+ g_object_get (G_OBJECT (tp_channel_borrow_connection (TP_CHANNEL (tpl_chan))),
+ "object-path", &chan_path, NULL);
+
+ if (!tpl_observer_unregister_channel (observer, tpl_chan))
+ g_warning ("Channel %s couldn't be unregistered correctly (BUG?)",
+ chan_path);
+
+ g_object_unref (observer);
+ g_free (chan_path);
+}
+
+
+static void
+_channel_on_lost_message_cb (TpChannel *proxy,
+ gpointer user_data,
+ GObject *weak_object)
+{
+ g_debug ("lost message signal catched. nothing logged");
+ // TODO log that the system lost a message
+}
+
+static void
+_channel_on_send_error_cb (TpChannel *proxy,
+ guint arg_Error,
+ guint arg_Timestamp,
+ guint arg_Type,
+ const gchar *arg_Text,
+ gpointer user_data,
+ GObject *weak_object)
+{
+ g_error ("unlogged event: "
+ "TP was unable to send the message: %s", arg_Text);
+ // TODO log that the system was unable to send the message
+}
+
+
+static void
+_channel_on_sent_signal_cb (TpChannel *proxy,
+ guint arg_Timestamp,
+ guint arg_Type,
+ const gchar *arg_Text,
+ gpointer user_data,
+ GObject *weak_object)
+{
+ GError *error = NULL;
+ TplChannelTest *tpl_text = TPL_CHANNEL_TEXT (user_data);
+ TpContact *remote = NULL;
+ TpContact *me;
+ TplContact *tpl_contact_sender;
+ TplContact *tpl_contact_receiver = NULL;
+ TplLogEntryText *log;
+ TplLogManager *logmanager;
+ gchar *chat_id;
+
+ g_return_if_fail (TPL_IS_CHANNEL_TEXT (tpl_text));
+
+ /* Initialize data for TplContact */
+ me = tpl_channel_test_get_my_contact (tpl_text);
+ tpl_contact_sender = tpl_contact_from_tp_contact (me);
+ tpl_contact_set_contact_type (tpl_contact_sender, TPL_CONTACT_USER);
+
+ if (tpl_channel_test_is_chatroom (tpl_text) == FALSE)
+ {
+ remote = tpl_channel_test_get_remote_contact (tpl_text);
+ if (remote == NULL)
+ g_error ("sending message: Remote TplContact=NULL on 1-1 Chat");
+ tpl_contact_receiver = tpl_contact_from_tp_contact (remote);
+ tpl_contact_set_contact_type (tpl_contact_receiver, TPL_CONTACT_USER);
+ }
+
+ g_message ("sent: %s (%s): %s",
+ tpl_contact_get_identifier (tpl_contact_sender),
+ tpl_contact_get_alias (tpl_contact_sender), arg_Text);
+
+ /* Initialise TplLogEntryText */
+ if (!tpl_channel_test_is_chatroom (tpl_text))
+ chat_id = g_strdup (tpl_contact_get_identifier (tpl_contact_receiver));
+ else
+ chat_id = g_strdup (tpl_channel_test_get_chatroom_id (tpl_text));
+
+ log = tpl_log_entry_text_new (arg_Timestamp, chat_id,
+ TPL_LOG_ENTRY_DIRECTION_OUT);
+ g_free (chat_id);
+
+ tpl_log_entry_text_set_timestamp (log, (time_t) arg_Timestamp);
+ tpl_log_entry_text_set_signal_type (log, TPL_LOG_ENTRY_TEXT_SIGNAL_SENT);
+ tpl_log_entry_text_set_sender (log, tpl_contact_sender);
+ tpl_log_entry_text_set_receiver (log, tpl_contact_receiver);
+ tpl_log_entry_text_set_message (log, arg_Text);
+ tpl_log_entry_text_set_message_type (log, arg_Type);
+ tpl_log_entry_text_set_tpl_channel_test (log, tpl_text);
+
+ /* Initialized LogStore and send the log entry */
+ tpl_log_entry_text_set_chatroom (log,
+ tpl_channel_test_is_chatroom (tpl_text));
+
+ logmanager = tpl_log_manager_dup_singleton ();
+ tpl_log_manager_add_message (logmanager, TPL_LOG_ENTRY (log), &error);
+
+ if (error != NULL)
+ {
+ g_error ("LogStore: %s", error->message);
+ g_clear_error (&error);
+ g_error_free (error);
+ }
+
+ if (tpl_contact_receiver)
+ g_object_unref (tpl_contact_receiver);
+ g_object_unref (tpl_contact_sender);
+ g_object_unref (logmanager);
+ g_object_unref (log);
+}
+
+static void
+_channel_on_received_signal_with_contact_cb (TpConnection *connection,
+ guint n_contacts,
+ TpContact *const *contacts,
+ guint n_failed,
+ const TpHandle *failed,
+ const GError *error,
+ gpointer user_data,
+ GObject *weak_object)
+{
+ TplLogEntryText *log = TPL_LOG_ENTRY_TEXT (user_data);
+ TplChannelTest *tpl_text;
+ TpContact *remote;
+
+ g_return_if_fail (TPL_IS_LOG_ENTRY_TEXT (log));
+
+ tpl_text = tpl_log_entry_text_get_tpl_channel_test (log);
+
+ if (error != NULL)
+ {
+ g_error ("Unrecoverable error retrieving remote contact "
+ "information: %s", error->message);
+ g_error ("Not able to log the received message: %s",
+ tpl_log_entry_text_get_message (log));
+ g_object_unref (log);
+ return;
+ }
+
+ if (n_failed > 0)
+ {
+ g_error ("%d invalid handle(s) passed to "
+ "tp_connection_get_contacts_by_handle()", n_failed);
+ g_error ("Not able to log the received message: %s",
+ tpl_log_entry_text_get_message (log));
+ g_object_unref (log);
+ return;
+ }
+
+ remote = contacts[0];
+ tpl_channel_test_set_remote_contact (tpl_text, remote);
+
+ keepon (log);
+}
+
+static void
+keepon (TplLogEntryText *log)
+{
+ TplChannelTest *tpl_text;
+ GError *e = NULL;
+ TplLogManager *logmanager;
+ TplContact *tpl_contact_sender;
+ TpContact *remote;
+ gchar *chat_id;
+
+ g_return_if_fail (TPL_IS_LOG_ENTRY_TEXT (log));
+
+ tpl_text = tpl_log_entry_text_get_tpl_channel_test (log);
+ remote = tpl_channel_test_get_remote_contact (tpl_text);
+
+ tpl_contact_sender = tpl_contact_from_tp_contact (remote);
+ tpl_contact_set_contact_type (tpl_contact_sender, TPL_CONTACT_USER);
+ tpl_log_entry_text_set_sender (log, tpl_contact_sender);
+
+ g_message ("recvd: %s (%s): %s",
+ tpl_contact_get_identifier (tpl_contact_sender),
+ tpl_contact_get_alias (tpl_contact_sender),
+ tpl_log_entry_text_get_message (log));
+
+ /* Initialise LogStore and store the message */
+
+ if (!tpl_channel_test_is_chatroom (tpl_text))
+ chat_id = g_strdup (tpl_contact_get_identifier (tpl_contact_sender));
+ else
+ chat_id = g_strdup (tpl_channel_test_get_chatroom_id (tpl_text));
+
+ tpl_log_entry_text_set_chat_id (log, chat_id);
+ tpl_log_entry_text_set_chatroom (log,
+ tpl_channel_test_is_chatroom (tpl_text));
+
+ logmanager = tpl_log_manager_dup_singleton ();
+ tpl_log_manager_add_message (logmanager, TPL_LOG_ENTRY (log), &e);
+ if (e != NULL)
+ {
+ g_error ("LogStore: %s", e->message);
+ g_clear_error (&e);
+ g_error_free (e);
+ }
+
+ g_object_unref (tpl_contact_sender);
+ g_object_unref (logmanager);
+ g_free (chat_id);
+}
+
+static void
+_channel_on_received_signal_cb (TpChannel *proxy,
+ guint arg_ID,
+ guint arg_Timestamp,
+ guint arg_Sender,
+ guint arg_Type,
+ guint arg_Flags,
+ const gchar *arg_Text,
+ gpointer user_data,
+ GObject *weak_object)
+{
+ TpHandle remote_handle = (TpHandle) arg_Sender;
+ TplChannelTest *tpl_text = TPL_CHANNEL_TEXT (user_data);
+ TplChannel *tpl_chan = TPL_CHANNEL (tpl_text);
+ TpConnection *tp_conn;
+ TpContact *me;
+ TplContact *tpl_contact_receiver;
+ TplLogEntryText *log;
+
+ /* TODO use the Message iface to check the delivery
+ notification and handle it correctly */
+ if (arg_Flags & TP_CHANNEL_TEXT_MESSAGE_FLAG_NON_TEXT_CONTENT)
+ {
+ g_debug ("Non text content flag set. "
+ "Probably a delivery notification for a sent message. "
+ "Ignoring");
+ return;
+ }
+
+ /* Initialize TplLogEntryText (part 1) */
+ log = tpl_log_entry_text_new (arg_Timestamp, NULL,
+ TPL_LOG_ENTRY_DIRECTION_IN);
+
+ tpl_log_entry_text_set_tpl_channel_test (log, tpl_text);
+ tpl_log_entry_text_set_message (log, arg_Text);
+ tpl_log_entry_text_set_message_type (log, arg_Type);
+ tpl_log_entry_text_set_signal_type (log, TPL_LOG_ENTRY_TEXT_SIGNAL_RECEIVED);
+
+ me = tpl_channel_test_get_my_contact (tpl_text);
+ tpl_contact_receiver = tpl_contact_from_tp_contact (me);
+ tpl_contact_set_contact_type (tpl_contact_receiver, TPL_CONTACT_USER);
+ tpl_log_entry_text_set_receiver (log, tpl_contact_receiver);
+
+ tpl_log_entry_text_set_timestamp (log, (time_t) arg_Timestamp);
+
+ tp_conn = tp_channel_borrow_connection (TP_CHANNEL (tpl_chan));
+ /* it's a chatroom and no contact has been pre-cached */
+ if (tpl_channel_test_get_remote_contact (tpl_text) == NULL)
+ tp_connection_get_contacts_by_handle (tp_conn, 1, &remote_handle,
+ TP_CONTACT_FEATURES_LEN, features,
+ _channel_on_received_signal_with_contact_cb, log, g_object_unref,
+ NULL);
+ else
+ keepon (log);
+
+ g_object_unref (tpl_contact_receiver);
+}
+
+/* End of Signal's Callbacks */
+
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am
new file mode 100644
index 0000000..9cbc256
--- /dev/null
+++ b/tests/twisted/Makefile.am
@@ -0,0 +1,130 @@
+TWISTED_TESTS = \
+ avatar-requirements.py \
+ caps_helper.py \
+ gateways.py \
+ text/initiate.py \
+ text/initiate-requestotron.py \
+ text/destroy.py \
+ text/ensure.py \
+ text/respawn.py \
+ text/send-error.py \
+ text/send-to-correct-resource.py \
+ text/test-chat-state.py \
+ text/test-text-delayed.py \
+ text/test-text-no-body.py \
+ text/test-text.py \
+ test-debug.py \
+ test-disco.py \
+ test-disco-no-reply.py \
+ test-fallback-socks5-proxy.py \
+ test-register.py \
+ test-set-status-idempotence.py \
+ test-location.py \
+ pubsub.py \
+ sidecars.py
+
+TESTS =
+
+TESTS_ENVIRONMENT = \
+ PYTHONPATH=@abs_top_srcdir@/tests/twisted:@abs_top_builddir@/tests/twisted
+
+check-local: check-coding-style check-twisted
+
+# set to 6 when using refdbg, to give Gabble time to exit
+CHECK_TWISTED_SLEEP=0
+
+check-twisted:
+ $(MAKE) -C tools
+ rm -f tools/core
+ rm -f tools/vgcore.*
+ rm -f tools/gabble-testing.log
+ rm -f tools/strace.log
+ if test -n "$$GABBLE_TEST_REFDBG"; then \
+ sleep=6; \
+ else \
+ sleep=$(CHECK_TWISTED_SLEEP); \
+ fi; \
+ sh $(srcdir)/tools/with-session-bus.sh \
+ --sleep=$$sleep \
+ --config-file=tools/tmp-session-bus.conf \
+ -- $(MAKE) check-TESTS \
+ TESTS="$(TWISTED_TESTS)" \
+ TESTS_ENVIRONMENT="$(TESTS_ENVIRONMENT) $(TEST_PYTHON) -u"
+ @if test -e tools/core; then\
+ echo "Core dump exists: tools/core";\
+ exit 1;\
+ fi
+
+if ENABLE_DEBUG
+DEBUGGING_PYBOOL = True
+else
+DEBUGGING_PYBOOL = False
+endif
+
+if ENABLE_PLUGINS
+PLUGINS_ENABLED_PYBOOL = True
+else
+PLUGINS_ENABLED_PYBOOL = False
+endif
+
+if ENABLE_CHANNEL_TYPE_CALL
+CHANNEL_TYPE_CALL_ENABLED_PYBOOL = True
+else
+CHANNEL_TYPE_CALL_ENABLED_PYBOOL = False
+endif
+
+config.py: Makefile
+ $(AM_V_GEN) { \
+ echo "PACKAGE_STRING = \"$(PACKAGE_STRING)\""; \
+ echo "DEBUGGING = $(DEBUGGING_PYBOOL)"; \
+ echo "PLUGINS_ENABLED = $(PLUGINS_ENABLED_PYBOOL)"; \
+ echo "CHANNEL_TYPE_CALL_ENABLED = $(CHANNEL_TYPE_CALL_ENABLED_PYBOOL)"; \
+ } > $@
+
+BUILT_SOURCES = config.py
+
+EXTRA_DIST = \
+ $(TWISTED_TESTS) \
+ bytestream.py \
+ constants.py \
+ gabbletest.py \
+ httptest.py \
+ servicetest.py \
+ jingle/jingletest.py \
+ jingle/jingletest2.py \
+ mucutil.py \
+ ns.py \
+ olpc/util.py \
+ tubes/muctubeutil.py \
+ tubes/tubetestutil.py \
+ search/search_helper.py \
+ file-transfer/file_transfer_helper.py
+
+noinst_PROGRAMS = \
+ telepathy-gabble-debug
+
+telepathy_gabble_debug_SOURCES = \
+ main-debug.c \
+ test-resolver.c \
+ test-resolver.h
+
+telepathy_gabble_debug_LDADD = \
+ $(top_builddir)/src/libgabble-convenience.la \
+ $(ALL_LIBS)
+
+telepathy_gabble_debug_LDFLAGS = -export-dynamic
+
+AM_CFLAGS = $(ERROR_CFLAGS) @DBUS_CFLAGS@ @GLIB_CFLAGS@ @WOCKY_CFLAGS@ \
+ @TP_GLIB_CFLAGS@ \
+ -I $(top_srcdir)/src -I $(top_builddir)/src \
+ -I $(top_srcdir)/lib -I $(top_builddir)/lib \
+ -I $(top_srcdir) -I $(top_builddir)
+ALL_LIBS = @DBUS_LIBS@ @GLIB_LIBS@ @WOCKY_LIBS@ @TP_GLIB_LIBS@
+
+CLEANFILES = gabble-[1-9]*.log *.pyc */*.pyc config.py
+
+check_misc_sources = $(TESTS)
+
+include $(top_srcdir)/tools/check-coding-style.mk
+
+SUBDIRS = tools
diff --git a/tests/twisted/bytestream.py b/tests/twisted/bytestream.py
new file mode 100644
index 0000000..54ef9e6
--- /dev/null
+++ b/tests/twisted/bytestream.py
@@ -0,0 +1,882 @@
+import base64
+import hashlib
+import sys
+import random
+import socket
+
+from twisted.internet.protocol import Factory, Protocol
+from twisted.internet import reactor
+from twisted.words.protocols.jabber.client import IQ
+from twisted.words.xish import xpath, domish
+from twisted.internet.error import CannotListenError
+
+from servicetest import Event, EventPattern
+from gabbletest import acknowledge_iq, make_result_iq, elem_iq, elem
+import ns
+
+def wait_events(q, expected, my_event):
+ tmp = expected + [my_event]
+ events = q.expect_many(*tmp)
+ return events[:-1], events[-1]
+
+def create_from_si_offer(stream, q, bytestream_cls, iq, initiator):
+ si_nodes = xpath.queryForNodes('/iq/si', iq)
+ assert si_nodes is not None
+ assert len(si_nodes) == 1
+ si = si_nodes[0]
+
+ feature = xpath.queryForNodes('/si/feature', si)[0]
+ x = xpath.queryForNodes('/feature/x', feature)[0]
+ assert x['type'] == 'form'
+ field = xpath.queryForNodes('/x/field', x)[0]
+ assert field['var'] == 'stream-method'
+ assert field['type'] == 'list-single'
+
+ bytestreams = []
+ for value in xpath.queryForNodes('/field/option/value', field):
+ bytestreams.append(str(value))
+
+ bytestream = bytestream_cls(stream, q, si['id'], initiator,
+ iq['to'], False)
+
+ bytestream.check_si_offer(iq, bytestreams)
+
+ return bytestream, si['profile']
+
+def is_ipv4(address):
+ try:
+ socket.inet_pton(socket.AF_INET, address)
+ except (ValueError, socket.error):
+ return False
+ return True
+
+class Bytestream(object):
+ def __init__(self, stream, q, sid, initiator, target, initiated):
+ self.stream = stream
+ self.q = q
+
+ self.stream_id = sid
+ self.initiator = initiator
+ self.target = target
+ self.initiated = initiated
+
+ def open_bytestream(self, expected_before=[], expected_after=[]):
+ raise NotImplemented
+
+ def send_data(self, data):
+ raise NotImplemented
+
+ def get_ns(self):
+ raise NotImplemented
+
+ def wait_bytestream_open(self):
+ raise NotImplemented
+
+ def get_data(self, size=0):
+ raise NotImplemented
+
+ def wait_bytestream_closed(self, expected=[]):
+ raise NotImplemented
+
+ def check_si_offer(self, iq, bytestreams):
+ assert self.get_ns() in bytestreams
+
+ def close(self):
+ raise NotImplemented
+
+##### XEP-0095: Stream Initiation #####
+
+ def _create_si_offer(self, profile, to=None):
+ assert self.initiated
+
+ iq = IQ(self.stream, 'set')
+ iq['from'] = self.initiator
+ if to is None:
+ iq['to'] = self.target
+ else:
+ iq['to'] = to
+ si = iq.addElement((ns.SI, 'si'))
+ si['id'] = self.stream_id
+ si['profile'] = profile
+ feature = si.addElement((ns.FEATURE_NEG, 'feature'))
+ x = feature.addElement((ns.X_DATA, 'x'))
+ x['type'] = 'form'
+ field = x.addElement((None, 'field'))
+ field['var'] = 'stream-method'
+ field['type'] = 'list-single'
+
+ return iq, si, field
+
+ def create_si_offer(self, profile, to=None):
+ iq, si, field = self._create_si_offer(profile, to)
+ option = field.addElement((None, 'option'))
+ value = option.addElement((None, 'value'))
+ value.addContent(self.get_ns())
+
+ return iq, si
+
+ def create_si_reply(self, iq, to=None):
+ result = make_result_iq(self.stream, iq)
+ result['from'] = iq['to']
+ if to is None:
+ result['to'] = self.initiator
+ else:
+ result['to'] = to
+ res_si = result.firstChildElement()
+ res_feature = res_si.addElement((ns.FEATURE_NEG, 'feature'))
+ res_x = res_feature.addElement((ns.X_DATA, 'x'))
+ res_x['type'] = 'submit'
+ res_field = res_x.addElement((None, 'field'))
+ res_field['var'] = 'stream-method'
+ res_value = res_field.addElement((None, 'value'))
+ res_value.addContent(self.get_ns())
+
+ return result, res_si
+
+ def check_si_reply(self, iq):
+ si = xpath.queryForNodes('/iq/si[@xmlns="%s"]' % ns.SI,
+ iq)[0]
+ value = xpath.queryForNodes('/si/feature/x/field/value', si)
+ assert len(value) == 1
+ proto = value[0]
+ assert str(proto) == self.get_ns()
+
+##### XEP-0065: SOCKS5 Bytestreams #####
+def listen_socks5(q):
+ for port in range(5000, 5100):
+ try:
+ reactor.listenTCP(port, S5BFactory(q.append))
+ except CannotListenError:
+ continue
+ else:
+ return port
+
+ assert False, "Can't find a free port"
+
+def announce_socks5_proxy(q, stream, disco_stanza):
+ reply = make_result_iq(stream, disco_stanza)
+ query = xpath.queryForNodes('/iq/query', reply)[0]
+ item = query.addElement((None, 'item'))
+ item['jid'] = 'proxy.localhost'
+ stream.send(reply)
+
+ # wait for proxy disco#info query
+ event = q.expect('stream-iq', to='proxy.localhost', query_ns=ns.DISCO_INFO,
+ iq_type='get')
+
+ reply = elem_iq(stream, 'result', from_='proxy.localhost', id=event.stanza['id'])(
+ elem(ns.DISCO_INFO, 'query')(
+ elem('identity', category='proxy', type='bytestreams', name='SOCKS5 Bytestreams')(),
+ elem('feature', var=ns.BYTESTREAMS)()))
+ stream.send(reply)
+
+ # Gabble asks for SOCKS5 info
+ event = q.expect('stream-iq', to='proxy.localhost', query_ns=ns.BYTESTREAMS,
+ iq_type='get')
+
+ port = listen_socks5(q)
+ reply = elem_iq(stream, 'result', id=event.stanza['id'], from_='proxy.localhost')(
+ elem(ns.BYTESTREAMS, 'query')(
+ elem('streamhost', jid='proxy.localhost', host='127.0.0.1', port=str(port))()))
+ stream.send(reply)
+
+class BytestreamS5B(Bytestream):
+ def __init__(self, stream, q, sid, initiator, target, initiated):
+ Bytestream.__init__(self, stream, q, sid, initiator, target, initiated)
+
+ # hosts that will be announced when sending S5B open IQ
+ self.hosts = [
+ # Not working streamhost
+ ('invalid.invalid', 'invalid.invalid'),
+ # Working streamhost
+ (self.initiator, '127.0.0.1'),
+ # This works too but should not be tried as Gabble should just
+ # connect to the previous one
+ ('Not me', '127.0.0.1')]
+
+ def get_ns(self):
+ return ns.BYTESTREAMS
+
+ def _send_socks5_init(self, port):
+ iq = IQ(self.stream, 'set')
+ iq['to'] = self.target
+ iq['from'] = self.initiator
+ query = iq.addElement((ns.BYTESTREAMS, 'query'))
+ query['sid'] = self.stream_id
+ query['mode'] = 'tcp'
+ for jid, host in self.hosts:
+ streamhost = query.addElement('streamhost')
+ streamhost['jid'] = jid
+ streamhost['host'] = host
+ streamhost['port'] = str(port)
+ self.stream.send(iq)
+
+ def _wait_auth_request(self):
+ event = self.q.expect('s5b-data-received')
+ assert event.data == '\x05\x01\x00' # version 5, 1 auth method, no auth
+ self.transport = event.transport
+
+ def _send_auth_reply(self):
+ self.transport.write('\x05\x00') # version 5, no auth
+
+ def _compute_hash_domain(self):
+ # sha-1(sid + initiator + target)
+ unhashed_domain = self.stream_id + self.initiator + self.target
+ return hashlib.sha1(unhashed_domain).hexdigest()
+
+ def _wait_connect_cmd(self):
+ event = self.q.expect('s5b-data-received', transport=self.transport)
+ # version 5, connect, reserved, domain type
+ expected_connect = '\x05\x01\x00\x03'
+ expected_connect += chr(40) # len (SHA-1)
+ expected_connect += self._compute_hash_domain()
+ expected_connect += '\x00\x00' # port
+ assert event.data == expected_connect
+
+ def _send_connect_reply(self):
+ connect_reply = '\x05\x00\x00\x03'
+ connect_reply += chr(40) # len (SHA-1)
+ connect_reply += self._compute_hash_domain()
+ connect_reply += '\x00\x00' # port
+ self.transport.write(connect_reply)
+
+ def _check_s5b_reply(self, iq):
+ streamhost = xpath.queryForNodes('/iq/query/streamhost-used', iq)[0]
+ assert streamhost['jid'] == self.initiator
+
+ def _socks5_expect_connection(self, expected_before, expected_after):
+ events_before, _ = wait_events(self.q, expected_before,
+ EventPattern('s5b-connected'))
+
+ self._wait_auth_request()
+ self._send_auth_reply()
+ self._wait_connect_cmd()
+ self._send_connect_reply()
+
+ # wait for S5B IQ reply
+ events_after, e = wait_events(self.q, expected_after,
+ EventPattern('stream-iq', iq_type='result', to=self.initiator))
+
+ self._check_s5b_reply(e.stanza)
+
+ return events_before, events_after
+
+ def open_bytestream(self, expected_before=[], expected_after=[]):
+ port = listen_socks5(self.q)
+
+ self._send_socks5_init(port)
+ return self._socks5_expect_connection(expected_before, expected_after)
+
+ def send_data(self, data):
+ self.transport.write(data)
+
+ def _expect_socks5_init(self):
+ event = self.q.expect('stream-iq', iq_type='set')
+ iq = event.stanza
+ query = xpath.queryForNodes('/iq/query', iq)[0]
+ assert query.uri == ns.BYTESTREAMS
+
+ mode = query['mode']
+ sid = query['sid']
+ hosts = []
+
+ for streamhost in xpath.queryForNodes('/query/streamhost', query):
+ hosts.append((streamhost['jid'], streamhost['host'], int(streamhost['port'])))
+ return iq['id'], mode, sid, hosts
+
+ def _send_auth_cmd(self):
+ #version 5, 1 auth method, no auth
+ self.transport.write('\x05\x01\x00')
+
+ def _wait_auth_reply(self):
+ event = self.q.expect('s5b-data-received')
+ assert event.data == '\x05\x00' # version 5, no auth
+
+ def _send_connect_cmd(self):
+ # version 5, connect, reserved, domain type
+ connect = '\x05\x01\x00\x03'
+ connect += chr(40) # len (SHA-1)
+ connect += self._compute_hash_domain()
+ connect += '\x00\x00' # port
+ self.transport.write(connect)
+
+ def _wait_connect_reply(self):
+ event = self.q.expect('s5b-data-received')
+ # version 5, succeed, reserved, domain type
+ expected_reply = '\x05\x00\x00\x03'
+ expected_reply += chr(40) # len (SHA-1)
+ expected_reply += self._compute_hash_domain()
+ expected_reply += '\x00\x00' # port
+ assert event.data == expected_reply
+
+ def _socks5_connect(self, host, port):
+ reactor.connectTCP(host, port, S5BFactory(self.q.append))
+
+ event = self.q.expect('s5b-connected')
+ self.transport = event.transport
+
+ self._send_auth_cmd()
+ self._wait_auth_reply()
+ self._send_connect_cmd()
+ self._wait_connect_reply()
+ return True
+
+ def _send_socks5_reply(self, id, stream_used):
+ result = IQ(self.stream, 'result')
+ result['id'] = id
+ result['from'] = self.target
+ result['to'] = self.initiator
+ query = result.addElement((ns.BYTESTREAMS, 'query'))
+ streamhost_used = query.addElement((None, 'streamhost-used'))
+ streamhost_used['jid'] = stream_used
+ result.send()
+
+ def wait_bytestream_open(self):
+ id, mode, sid, hosts = self._expect_socks5_init()
+
+ assert mode == 'tcp'
+ assert sid == self.stream_id
+
+ stream_host_found = False
+
+ for jid, host, port in hosts:
+ if not is_ipv4(host):
+ continue
+
+ if jid == self.initiator:
+ stream_host_found = True
+ if self._socks5_connect(host, port):
+ self._send_socks5_reply(id, jid)
+ else:
+ # Connection failed
+ self.send_not_found(id)
+ break
+ assert stream_host_found
+
+ def get_data(self, size=0):
+ binary = ''
+ received = False
+ while not received:
+ e = self.q.expect('s5b-data-received', transport=self.transport)
+ binary += e.data
+
+ if len(binary) >= size or size == 0:
+ received = True
+
+ return binary
+
+ def wait_bytestream_closed(self, expected=[]):
+ events, _ = wait_events(self.q, expected,
+ EventPattern('s5b-connection-lost'))
+ return events
+
+ def check_error_stanza(self, iq):
+ error = xpath.queryForNodes('/iq/error', iq)[0]
+ assert error['code'] == '404'
+ assert error['type'] == 'cancel'
+
+ def send_not_found(self, id):
+ iq = IQ(self.stream, 'error')
+ iq['to'] = self.initiator
+ iq['from'] = self.target
+ iq['id'] = id
+ error = iq.addElement(('', 'error'))
+ error['type'] = 'cancel'
+ error['code'] = '404'
+ self.stream.send(iq)
+
+ def close(self):
+ self.transport.loseConnection()
+
+class BytestreamS5BPidgin(BytestreamS5B):
+ """Simulate buggy S5B implementation (as Pidgin's one)"""
+ def _send_connect_reply(self):
+ # version 5, ok, reserved, domain type
+ connect_reply = '\x05\x00\x00\x03'
+ # I'm Pidgin, why should I respect SOCKS5 XEP?
+ domain = '127.0.0.1'
+ connect_reply += chr(len(domain))
+ connect_reply += domain
+ connect_reply += '\x00\x00' # port
+ self.transport.write(connect_reply)
+
+class BytestreamS5BCannotConnect(BytestreamS5B):
+ """SOCKS5 bytestream not working because target can't connect
+ to initiator."""
+ def __init__(self, stream, q, sid, initiator, target, initiated):
+ BytestreamS5B.__init__(self, stream, q, sid, initiator, target, initiated)
+
+ self.hosts = [('invalid.invalid', 'invalid.invalid')]
+
+ def open_bytestream(self, expected_before=[], expected_after=[]):
+ self._send_socks5_init(12345)
+
+ events_before, iq_event = wait_events(self.q, expected_before,
+ EventPattern('stream-iq', iq_type='error', to=self.initiator))
+
+ self.check_error_stanza(iq_event.stanza)
+
+ return events_before, []
+
+ def _socks5_connect(self, host, port):
+ # Pretend we can't connect to it
+ return False
+
+class BytestreamS5BWrongHash(BytestreamS5B):
+ """Connection is closed because target sends the wrong hash"""
+ def __init__(self, stream, q, sid, initiator, target, initiated):
+ BytestreamS5B.__init__(self, stream, q, sid, initiator, target, initiated)
+
+ self.hosts = [(self.initiator, '127.0.0.1')]
+
+ def _send_connect_cmd(self):
+ # version 5, connect, reserved, domain type
+ connect = '\x05\x01\x00\x03'
+ # send wrong hash as domain
+ domain = 'this is wrong'
+ connect += chr(len(domain))
+ connect += domain
+ connect += '\x00\x00' # port
+ self.transport.write(connect)
+
+ def _socks5_connect(self, host, port):
+ reactor.connectTCP(host, port, S5BFactory(self.q.append))
+
+ event = self.q.expect('s5b-connected')
+ self.transport = event.transport
+
+ self._send_auth_cmd()
+ self._wait_auth_reply()
+ self._send_connect_cmd()
+
+ # Gabble disconnects the connection because we sent a wrong hash
+ self.q.expect('s5b-connection-lost')
+ return False
+
+ def _socks5_expect_connection(self, expected_before, expected_after):
+ events_before, _ = wait_events(self.q, expected_before,
+ EventPattern('s5b-connected'))
+
+ self._wait_auth_request()
+ self._send_auth_reply()
+ self._wait_connect_cmd()
+
+ # pretend the hash was wrong and close the transport
+ self.transport.loseConnection()
+
+ iq_event = self.q.expect('stream-iq', iq_type='error', to=self.initiator)
+ self.check_error_stanza(iq_event.stanza)
+
+ return events_before, []
+
+class BytestreamS5BRelay(BytestreamS5B):
+ """Direct connection doesn't work so we use a relay"""
+ def __init__(self, stream, q, sid, initiator, target, initiated):
+ BytestreamS5B.__init__(self, stream, q, sid, initiator, target, initiated)
+
+ self.hosts = [(self.initiator, 'invalid.invalid'),
+ ('proxy.localhost', '127.0.0.1')]
+
+ # This is the only thing we need to check to test the Target side as the
+ # protocol is similar from this side.
+ def _check_s5b_reply(self, iq):
+ streamhost = xpath.queryForNodes('/iq/query/streamhost-used', iq)[0]
+ assert streamhost['jid'] == 'proxy.localhost'
+
+ def wait_bytestream_open(self):
+ # The only difference of using a relay on the Target side is to
+ # connect to another streamhost.
+ id, mode, sid, hosts = self._expect_socks5_init()
+
+ assert mode == 'tcp'
+ assert sid == self.stream_id
+
+ proxy_found = False
+
+ for jid, host, port in hosts:
+ if jid != self.initiator:
+ proxy_found = True
+ # connect to the (fake) relay
+ if self._socks5_connect(host, port):
+ self._send_socks5_reply(id, jid)
+ else:
+ assert False
+ break
+ assert proxy_found
+
+ # The initiator (Gabble) is now supposed to connect to the proxy too
+ self._wait_connect_to_proxy()
+
+ def _wait_connect_to_proxy(self):
+ e = self.q.expect('s5b-connected')
+ self.transport = e.transport
+
+ self._wait_auth_request()
+ self._send_auth_reply()
+ self._wait_connect_cmd()
+ self._send_connect_reply()
+
+ self._wait_activation_iq()
+
+ def _wait_activation_iq(self):
+ e = self.q.expect('stream-iq', iq_type='set', to='proxy.localhost',
+ query_ns=ns.BYTESTREAMS)
+
+ query = xpath.queryForNodes('/iq/query', e.stanza)[0]
+ assert query['sid'] == self.stream_id
+ activate = xpath.queryForNodes('/iq/query/activate', e.stanza)[0]
+ assert str(activate) == self.target
+
+ self._reply_activation_iq(e.stanza)
+
+ def _reply_activation_iq(self, iq):
+ reply = make_result_iq(self.stream, iq)
+ reply.send()
+
+ def _socks5_connect(self, host, port):
+ # No point to emulate the proxy. Just pretend the Target properly
+ # connects, auth and requests connection
+ return True
+
+ def wait_bytestream_closed(self, expected=[]):
+ if expected == []:
+ return []
+
+ return self.q.expect_many(*expected)
+
+
+class BytestreamS5BRelayBugged(BytestreamS5BRelay):
+ """Simulate bugged ejabberd (< 2.0.2) proxy sending wrong CONNECT reply"""
+ def _send_connect_reply(self):
+ # send a 6 bytes wrong reply
+ connect_reply = '\x05\x00\x00\x00\x00\x00'
+ self.transport.write(connect_reply)
+
+class S5BProtocol(Protocol):
+ def connectionMade(self):
+ self.factory.event_func(Event('s5b-connected',
+ transport=self.transport))
+
+ def dataReceived(self, data):
+ self.factory.event_func(Event('s5b-data-received', data=data,
+ transport=self.transport))
+
+class S5BFactory(Factory):
+ protocol = S5BProtocol
+
+ def __init__(self, event_func):
+ self.event_func = event_func
+
+ def buildProtocol(self, addr):
+ protocol = Factory.buildProtocol(self, addr)
+ return protocol
+
+ def startedConnecting(self, connector):
+ self.event_func(Event('s5b-started-connecting', connector=connector))
+
+ def clientConnectionLost(self, connector, reason):
+ self.event_func(Event('s5b-connection-lost', connector=connector,
+ reason=reason))
+
+ def clientConnectionFailed(self, connector, reason):
+ self.event_func(Event('s5b-connection-failed', reason=reason))
+
+def expect_socks5_reply(q):
+ event = q.expect('stream-iq', iq_type='result')
+ iq = event.stanza
+ query = xpath.queryForNodes('/iq/query', iq)[0]
+ assert query.uri == ns.BYTESTREAMS
+ streamhost_used = xpath.queryForNodes('/query/streamhost-used', query)[0]
+ return streamhost_used
+
+##### XEP-0047: In-Band Bytestreams (IBB) #####
+
+class BytestreamIBB(Bytestream):
+ def __init__(self, stream, q, sid, initiator, target, initiated):
+ Bytestream.__init__(self, stream, q, sid, initiator, target, initiated)
+
+ self.seq = 0
+ self.checked = False
+
+ def get_ns(self):
+ return ns.IBB
+
+ def check_si_reply(self, iq):
+ self.checked = True
+
+ def open_bytestream(self, expected_before=[], expected_after=[]):
+ # open IBB bytestream
+ iq = IQ(self.stream, 'set')
+ iq['to'] = self.target
+ iq['from'] = self.initiator
+ open = iq.addElement((ns.IBB, 'open'))
+ open['sid'] = self.stream_id
+ # set a ridiculously small block size to stress test IBB buffering
+ open['block-size'] = '1'
+
+ assert self.checked
+
+ events_before = self.q.expect_many(*expected_before)
+ self.stream.send(iq)
+ events_after = self.q.expect_many(*expected_after)
+
+ return events_before, events_after
+
+ def _send(self, from_, to, data):
+ raise NotImplemented
+
+ def send_data(self, data):
+ if self.initiated:
+ from_ = self.initiator
+ to = self.target
+ else:
+ from_ = self.target
+ to = self.initiator
+
+ self._send(from_, to, data)
+ self.seq += 1
+
+ def wait_bytestream_open(self):
+ # Wait IBB open iq
+ event = self.q.expect('stream-iq', iq_type='set')
+ open = xpath.queryForNodes('/iq/open', event.stanza)[0]
+ assert open.uri == ns.IBB
+ assert open['sid'] == self.stream_id
+
+ # open IBB bytestream
+ acknowledge_iq(self.stream, event.stanza)
+
+ def get_data(self, size=0):
+ # wait for IBB stanza. Gabble always uses IQ
+
+ binary = ''
+ received = False
+ while not received:
+ ibb_event = self.q.expect('stream-iq', query_ns=ns.IBB)
+
+ data_nodes = xpath.queryForNodes('/iq/data[@xmlns="%s"]' % ns.IBB,
+ ibb_event.stanza)
+ assert data_nodes is not None
+ assert len(data_nodes) == 1
+ ibb_data = data_nodes[0]
+ binary += base64.b64decode(str(ibb_data))
+
+ assert ibb_data['sid'] == self.stream_id
+
+ # ack the IQ
+ result = make_result_iq(self.stream, ibb_event.stanza)
+ result.send()
+
+ if len(binary) >= size or size == 0:
+ received = True
+
+ return binary
+
+ def wait_bytestream_closed(self, expected=[]):
+ events, close_event = wait_events(self.q, expected,
+ EventPattern('stream-iq', iq_type='set', query_name='close', query_ns=ns.IBB))
+
+ # sender finish to send the file and so close the bytestream
+ acknowledge_iq(self.stream, close_event.stanza)
+ return events
+
+ def close(self):
+ if self.initiated:
+ from_ = self.initiator
+ to = self.target
+ else:
+ from_ = self.target
+ to = self.initiator
+
+ iq = elem_iq(self.stream, 'set', from_=from_, to=to, id=str(id))(
+ elem('close', xmlns=ns.IBB, sid=self.stream_id)())
+
+ self.stream.send(iq)
+
+class BytestreamIBBMsg(BytestreamIBB):
+ def _send(self, from_, to, data):
+ message = domish.Element(('jabber:client', 'message'))
+ message['to'] = to
+ message['from'] = from_
+ data_node = message.addElement((ns.IBB, 'data'))
+ data_node['sid'] = self.stream_id
+ data_node['seq'] = str(self.seq)
+ data_node.addContent(base64.b64encode(data))
+ self.stream.send(message)
+
+ def _wait_data_event(self):
+ ibb_event = self.q.expect('stream-message')
+
+ data_nodes = xpath.queryForNodes('/message/data[@xmlns="%s"]' % ns.IBB,
+ ibb_event.stanza)
+ assert data_nodes is not None
+ assert len(data_nodes) == 1
+ ibb_data = data_nodes[0]
+ assert ibb_data['sid'] == self.stream_id
+ return str(ibb_data), ibb_data['sid']
+
+class BytestreamIBBIQ(BytestreamIBB):
+ def _send(self, from_, to, data):
+ id = random.randint(0, sys.maxint)
+
+ iq = elem_iq(self.stream, 'set', from_=from_, to=to, id=str(id))(
+ elem('data', xmlns=ns.IBB, sid=self.stream_id, seq=str(self.seq))(
+ (unicode(base64.b64encode(data)))))
+
+ self.stream.send(iq)
+
+##### SI Fallback (Gabble specific extension) #####
+class BytestreamSIFallback(Bytestream):
+ """Abstract class used for all the SI fallback scenarios"""
+ def __init__(self, stream, q, sid, initiator, target, initiated):
+ Bytestream.__init__(self, stream, q, sid, initiator, target, initiated)
+
+ self.socks5 = BytestreamS5B(stream, q, sid, initiator, target,
+ initiated)
+
+ self.ibb = BytestreamIBBMsg(stream, q, sid, initiator, target,
+ initiated)
+
+ def create_si_offer(self, profile, to=None):
+ iq, si, field = self._create_si_offer(profile, to)
+
+ # add SOCKS5
+ option = field.addElement((None, 'option'))
+ value = option.addElement((None, 'value'))
+ value.addContent(self.socks5.get_ns())
+ # add IBB
+ option = field.addElement((None, 'option'))
+ value = option.addElement((None, 'value'))
+ value.addContent(self.ibb.get_ns())
+
+ si_multiple = si.addElement((ns.SI_MULTIPLE, 'si-multiple'))
+
+ return iq, si
+
+ def check_si_reply(self, iq):
+ value = xpath.queryForNodes(
+ '/iq/si[@xmlns="%s"]/si-multiple[@xmlns="%s"]/value' %
+ (ns.SI, ns.SI_MULTIPLE), iq)
+ assert len(value) == 2
+ assert str(value[0]) == self.socks5.get_ns()
+ assert str(value[1]) == self.ibb.get_ns()
+
+ def create_si_reply(self, iq, to=None):
+ result = make_result_iq(self.stream, iq)
+ result['from'] = iq['to']
+ if to is None:
+ result['to'] = self.initiator
+ else:
+ result['to'] = to
+ res_si = result.firstChildElement()
+ si_multiple = res_si.addElement((ns.SI_MULTIPLE, 'si-multiple'))
+ # add SOCKS5
+ res_value = si_multiple.addElement((None, 'value'))
+ res_value.addContent(self.socks5.get_ns())
+ # add IBB
+ res_value = si_multiple.addElement((None, 'value'))
+ res_value.addContent(self.ibb.get_ns())
+
+ return result, res_si
+
+ def open_bytestream(self, expected_before=[], expected_after=[]):
+ # first propose to peer to connect using SOCKS5
+ # We set an invalid IP so that won't work
+ self.socks5._send_socks5_init([
+ # Not working streamhost
+ (self.initiator, 'invalid.invalid', 12345),
+ ])
+
+ events_before, iq_event = wait_events(self.q, expected_before,
+ EventPattern('stream-iq', iq_type='error', to=self.initiator))
+
+ self.socks5.check_error_stanza(iq_event.stanza)
+
+ # socks5 failed, let's try IBB
+ _, events_after = self.ibb.open_bytestream([], expected_after)
+
+ return events_before, events_after
+
+ def send_data(self, data):
+ self.used.send_data(data)
+
+ def get_data(self, size=0):
+ return self.used.get_data(size)
+
+ def wait_bytestream_closed(self, expected=[]):
+ return self.used.wait_bytestream_closed(expected)
+
+ def check_si_offer(self, iq, bytestreams):
+ assert self.socks5.get_ns() in bytestreams
+ assert self.ibb.get_ns() in bytestreams
+
+ # check if si-multiple is supported
+ si_multiple = xpath.queryForNodes(
+ '/iq/si[@xmlns="%s"]/si-multiple[@xmlns="%s"]'
+ % (ns.SI, ns.SI_MULTIPLE), iq)
+ assert si_multiple is not None
+
+ def close(self):
+ return self.used.close()
+
+class BytestreamSIFallbackS5CannotConnect(BytestreamSIFallback):
+ """Try to use SOCKS5 and fallback to IBB because the target can't connect
+ to the receiver."""
+ def __init__(self, stream, q, sid, initiator, target, initiated):
+ BytestreamSIFallback.__init__(self, stream, q, sid, initiator, target, initiated)
+
+ self.socks5 = BytestreamS5BCannotConnect(stream, q, sid, initiator, target,
+ initiated)
+
+ self.used = self.ibb
+
+ def open_bytestream(self, expected_before=[], expected_after=[]):
+ # First propose to peer to connect using SOCKS5
+ # That won't work as target can't connect
+ events_before, _ = self.socks5.open_bytestream(expected_before)
+
+ # socks5 failed, let's try IBB
+ _, events_after = self.ibb.open_bytestream([], expected_after)
+
+ return events_before, events_after
+
+ def wait_bytestream_open(self):
+ # Gabble tries SOCKS5 first
+ self.socks5.wait_bytestream_open()
+
+ # Gabble now tries IBB
+ self.ibb.wait_bytestream_open()
+
+ def check_si_reply (self, iq):
+ self.ibb.check_si_reply (iq)
+
+class BytestreamSIFallbackS5WrongHash(BytestreamSIFallback):
+ """Try to use SOCKS5 and fallback to IBB because target sent the wrong hash
+ as domain in the CONNECT command."""
+ def __init__(self, stream, q, sid, initiator, target, initiated):
+ BytestreamSIFallback.__init__(self, stream, q, sid, initiator, target, initiated)
+
+ self.socks5 = BytestreamS5BWrongHash(stream, q, sid, initiator, target,
+ initiated)
+
+ self.used = self.ibb
+
+ def open_bytestream(self, expected_before=[], expected_after=[]):
+ # SOCKS5 won't work because we'll pretend the hash was wrong and
+ # close the connection
+ events_before, _ = self.socks5.open_bytestream(expected_before)
+
+ # socks5 failed, let's try IBB
+ _, events_after = self.ibb.open_bytestream([], expected_after)
+ return events_before, events_after
+
+ def wait_bytestream_open(self):
+ # BytestreamS5BWrongHash will send a wrong hash so Gabble will
+ # disconnect the connection
+ self.socks5.wait_bytestream_open()
+
+ # Gabble now tries IBB
+ self.ibb.wait_bytestream_open()
+
+ def check_si_reply (self, iq):
+ self.ibb.check_si_reply (iq)
diff --git a/tests/twisted/constants.py b/tests/twisted/constants.py
new file mode 100644
index 0000000..f80e7dd
--- /dev/null
+++ b/tests/twisted/constants.py
@@ -0,0 +1,306 @@
+"""
+Some handy constants for other tests to share and enjoy.
+"""
+
+from dbus import PROPERTIES_IFACE
+
+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_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_TYPE_CALL = CHANNEL + ".Type.Call.DRAFT"
+CHANNEL_TYPE_CONTACT_LIST = CHANNEL + ".Type.ContactList"
+CHANNEL_TYPE_CONTACT_SEARCH = CHANNEL + ".Type.ContactSearch.DRAFT2"
+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"
+
+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_VIDEO = CHANNEL_TYPE_CALL + '.InitialVideo'
+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_STREAM_TRANSPORT_RAW_UDP = 0
+CALL_STREAM_TRANSPORT_ICE = 1
+CALL_STREAM_TRANSPORT_GOOGLE = 2
+
+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_EARLY_MEDIA = 1
+CALL_DISPOSITION_INITIAL = 2
+
+CALL_SENDING_STATE_NONE = 0
+CALL_SENDING_STATE_PENDING_SEND = 1
+CALL_SENDING_STATE_SENDING = 2
+
+CONN = "org.freedesktop.Telepathy.Connection"
+CONN_IFACE_AVATARS = CONN + '.Interface.Avatars'
+CONN_IFACE_CAPS = CONN + '.Interface.Capabilities'
+CONN_IFACE_CONTACTS = CONN + '.Interface.Contacts'
+CONN_IFACE_CONTACT_CAPS = CONN + '.Interface.ContactCapabilities'
+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'
+
+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'
+
+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'
+
+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
+
+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'
+
+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
diff --git a/tests/twisted/gabbletest.py b/tests/twisted/gabbletest.py
new file mode 100644
index 0000000..d6e4c34
--- /dev/null
+++ b/tests/twisted/gabbletest.py
@@ -0,0 +1,606 @@
+
+"""
+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 constants as cs
+import servicetest
+from servicetest import (
+ assertEquals, assertLength, assertContains, wrap_channel, EventPattern, call_async
+ )
+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"]
+ to = iq.getAttribute('to')
+ if to is not None:
+ result["from"] = to
+ 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, 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):
+ 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
+ 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 JabberAuthenticator(xmlstream.Authenticator):
+ "Trivial XML stream authenticator that accepts one username/digest pair."
+
+ def __init__(self, username, password, resource=None):
+ self.username = username
+ self.password = password
+ self.resource = resource
+ 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 = '%x' % random.randint(1, sys.maxint)
+
+ 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 = 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]))
+
+ 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, resource=None):
+ xmlstream.Authenticator.__init__(self)
+ self.username = username
+ self.password = password
+ self.resource = resource
+ 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):
+ 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_XMPP_BIND, 'bind'))
+ jid = bind.addElement('jid', content=('test@localhost/%s' % 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")
+ event.iq_id = iq.getAttribute("id")
+ 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")
+ else:
+ event.query = None
+
+ 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'
+ 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
+
+class JabberXmlStream(BaseXmlStream):
+ version = (0, 9)
+
+class XmppXmlStream(BaseXmlStream):
+ version = (1, 0)
+
+class GoogleXmlStream(BaseXmlStream):
+ version = (1, 0)
+
+ def _cb_disco_iq(self, iq):
+ if iq.getAttribute('to') == 'localhost':
+ nodes = xpath.queryForNodes(
+ "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']",
+ iq)
+ query = nodes[0]
+ feature = query.addElement('feature')
+ feature['var'] = ns.GOOGLE_ROSTER
+ feature = query.addElement('feature')
+ feature['var'] = ns.GOOGLE_JINGLE_INFO
+
+ iq['type'] = 'result'
+ iq['from'] = 'localhost'
+ self.send(iq)
+
+def make_connection(bus, event_func, params=None):
+ # Gabble accepts a resource in 'account', but the value of 'resource'
+ # overrides it if there is one.
+ account = 'test@localhost/%s' % re.sub(r'.*tests/twisted/', '', sys.argv[0])
+ default_params = {
+ 'account': account,
+ 'password': 'pass',
+ 'resource': 'Resource',
+ 'server': 'localhost',
+ 'port': dbus.UInt32(4242),
+ 'fallback-socks5-proxies': dbus.Array([], signature='s'),
+ }
+
+ if params:
+ default_params.update(params)
+
+ return servicetest.make_connection(bus, event_func, 'gabble', 'jabber',
+ default_params)
+
+def make_stream(event_func, authenticator=None, protocol=None, port=4242, resource=None):
+ # set up Jabber server
+
+ if authenticator is None:
+ authenticator = XmppAuthenticator('test', 'pass', resource=resource)
+
+ if protocol is None:
+ protocol = XmppXmlStream
+
+ stream = protocol(event_func, authenticator)
+ factory = twisted.internet.protocol.Factory()
+ factory.protocol = lambda *args: stream
+ port = reactor.listenTCP(port, factory)
+ return (stream, port)
+
+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):
+ # 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()
+
+ 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)
+ resource = params.get('resource') if params is not None else None
+ (stream, port) = make_stream(queue.append, protocol=protocol,
+ authenticator=authenticator, resource=resource)
+
+ error = None
+
+ try:
+ fun(queue, bus, conn, stream)
+ except Exception, e:
+ traceback.print_exc()
+ error = e
+
+ 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)))
+
+ # Does the Connection object still exist?
+ if not bus.name_has_owner(conn.object.bus_name):
+ # Connection has already been disconnected and destroyed
+ return
+
+ try:
+ if conn.GetStatus() == cs.CONN_STATUS_CONNECTED:
+ # Connection is connected, properly disconnect it
+ disconnect_conn(queue, conn, stream)
+ else:
+ # Connection is not connected, call Disconnect() to destroy it
+ conn.Disconnect()
+ except dbus.DBusException, e:
+ pass
+
+def exec_test(fun, params=None, protocol=None, timeout=None,
+ authenticator=None):
+ reactor.callWhenRunning(
+ exec_test_deferred, fun, params, protocol, timeout, authenticator)
+ 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)
+ result.addChild(current_vcard)
+ stream.send(result)
+
+def expect_and_handle_set_vcard(q, stream, check=None):
+ 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, **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
+
+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
+
+def expect_list_channel(q, bus, conn, name, contacts, lp_contacts=[],
+ rp_contacts=[]):
+ return expect_contact_list_channel(q, bus, conn, cs.HT_LIST, name,
+ contacts, lp_contacts=lp_contacts, rp_contacts=rp_contacts)
+
+def expect_group_channel(q, bus, conn, name, contacts, lp_contacts=[],
+ rp_contacts=[]):
+ return expect_contact_list_channel(q, bus, conn, cs.HT_GROUP, name,
+ contacts, lp_contacts=lp_contacts, rp_contacts=rp_contacts)
+
+def expect_contact_list_channel(q, bus, conn, ht, name, contacts,
+ lp_contacts=[], rp_contacts=[]):
+ """
+ Expects NewChannel and NewChannels signals for the
+ contact list with handle type 'ht' and ID 'name', and checks that its
+ members, lp members and rp members are exactly 'contacts', 'lp_contacts'
+ and 'rp_contacts'.
+ Returns a proxy for the channel.
+ """
+
+ old_signal, new_signal = q.expect_many(
+ EventPattern('dbus-signal', signal='NewChannel'),
+ EventPattern('dbus-signal', signal='NewChannels'),
+ )
+
+ path, type, handle_type, handle, suppress_handler = old_signal.args
+
+ assertEquals(cs.CHANNEL_TYPE_CONTACT_LIST, type)
+ assertEquals(name, conn.InspectHandles(handle_type, [handle])[0])
+
+ chan = wrap_channel(bus.get_object(conn.bus_name, path),
+ cs.CHANNEL_TYPE_CONTACT_LIST)
+ members = chan.Group.GetMembers()
+
+ assertEquals(sorted(contacts),
+ sorted(conn.InspectHandles(cs.HT_CONTACT, members)))
+
+ lp_handles = conn.RequestHandles(cs.HT_CONTACT, lp_contacts)
+ rp_handles = conn.RequestHandles(cs.HT_CONTACT, rp_contacts)
+
+ # NB. comma: we're unpacking args. Thython!
+ info, = new_signal.args
+ assertLength(1, info) # one channel
+ path_, emitted_props = info[0]
+
+ assertEquals(path_, path)
+
+ assertEquals(cs.CHANNEL_TYPE_CONTACT_LIST, emitted_props[cs.CHANNEL_TYPE])
+ assertEquals(ht, emitted_props[cs.TARGET_HANDLE_TYPE])
+ assertEquals(handle, emitted_props[cs.TARGET_HANDLE])
+
+ channel_props = chan.Properties.GetAll(cs.CHANNEL)
+ assertEquals(handle, channel_props.get('TargetHandle'))
+ assertEquals(ht, channel_props.get('TargetHandleType'))
+ assertEquals(cs.CHANNEL_TYPE_CONTACT_LIST, channel_props.get('ChannelType'))
+ assertContains(cs.CHANNEL_IFACE_GROUP, channel_props.get('Interfaces'))
+ assertEquals(name, channel_props['TargetID'])
+ assertEquals(False, channel_props['Requested'])
+ assertEquals('', channel_props['InitiatorID'])
+ assertEquals(0, channel_props['InitiatorHandle'])
+
+ group_props = chan.Properties.GetAll(cs.CHANNEL_IFACE_GROUP)
+ assertContains('HandleOwners', group_props)
+ assertContains('Members', group_props)
+ assertEquals(members, group_props['Members'])
+ assertContains('LocalPendingMembers', group_props)
+ actual_lp_handles = [x[0] for x in group_props['LocalPendingMembers']]
+ assertEquals(sorted(lp_handles), sorted(actual_lp_handles))
+ assertContains('RemotePendingMembers', group_props)
+ assertEquals(sorted(rp_handles), sorted(group_props['RemotePendingMembers']))
+ assertContains('GroupFlags', group_props)
+
+ return chan
diff --git a/tests/twisted/gateways.py b/tests/twisted/gateways.py
new file mode 100644
index 0000000..bce152e
--- /dev/null
+++ b/tests/twisted/gateways.py
@@ -0,0 +1,94 @@
+"""
+Test the gateways plugin
+"""
+
+import dbus
+from twisted.words.xish import domish, xpath
+
+from servicetest import (
+ sync_dbus, call_async, EventPattern, assertEquals, assertContains,
+ )
+from gabbletest import exec_test, send_error_reply, acknowledge_iq, sync_stream
+import constants as cs
+import ns
+from config import PLUGINS_ENABLED
+
+PLUGIN_IFACE = "org.freedesktop.Telepathy.Gabble.Plugin.Gateways"
+
+if not PLUGINS_ENABLED:
+ print "NOTE: built without --enable-plugins, not testing plugins"
+ raise SystemExit(77)
+
+def test_success(q, gateways_iface, stream):
+ call_async(q, gateways_iface, 'Register',
+ 'talkd.example.com', '1970', 's3kr1t')
+ e = q.expect('stream-iq', iq_type='set', query_name='query',
+ query_ns=ns.REGISTER, to='talkd.example.com')
+ assertEquals('1970', xpath.queryForString('/query/username', e.query))
+ assertEquals('s3kr1t', xpath.queryForString('/query/password', e.query))
+ acknowledge_iq(stream, e.stanza)
+ q.expect('dbus-return', method='Register')
+
+def test_conflict(q, gateways_iface, stream):
+ call_async(q, gateways_iface, 'Register',
+ 'sip.example.com', '8675309', 'jenny')
+ e = q.expect('stream-iq', iq_type='set', query_name='query',
+ query_ns=ns.REGISTER, to='sip.example.com')
+ assertEquals('8675309', xpath.queryForString('/query/username', e.query))
+ assertEquals('jenny', xpath.queryForString('/query/password', e.query))
+ error = domish.Element((None, 'error'))
+ error['type'] = 'cancel'
+ error['code'] = '409'
+ error.addElement((ns.STANZA, 'conflict'))
+ send_error_reply(stream, e.stanza, error)
+ q.expect('dbus-error', method='Register', name=cs.REGISTRATION_EXISTS)
+
+def test_not_acceptable(q, gateways_iface, stream):
+ call_async(q, gateways_iface, 'Register',
+ 'fully-captcha-enabled.example.com', 'lalala', 'stoats')
+ e = q.expect('stream-iq', iq_type='set', query_name='query',
+ query_ns=ns.REGISTER, to='fully-captcha-enabled.example.com')
+ assertEquals('lalala', xpath.queryForString('/query/username', e.query))
+ assertEquals('stoats', xpath.queryForString('/query/password', e.query))
+ error = domish.Element((None, 'error'))
+ error['type'] = 'modify'
+ error['code'] = '406'
+ error.addElement((ns.STANZA, 'not-acceptable'))
+ send_error_reply(stream, e.stanza, error)
+ q.expect('dbus-error', method='Register', name=cs.NOT_AVAILABLE)
+
+def test(q, bus, conn, stream):
+ # Request a sidecar thate we support before we're connected; it should just
+ # wait around until we're connected.
+ call_async(q, conn.Future, 'EnsureSidecar', PLUGIN_IFACE)
+
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ # Now we're connected, the call we made earlier should return.
+ path, props = q.expect('dbus-return', method='EnsureSidecar').value
+ # This sidecar doesn't even implement get_immutable_properties; it
+ # should just get the empty dict filled in for it.
+ assertEquals({}, props)
+
+ gateways_iface = dbus.Interface(bus.get_object(conn.bus_name, path),
+ PLUGIN_IFACE)
+
+ test_success(q, gateways_iface, stream)
+ test_conflict(q, gateways_iface, stream)
+ test_not_acceptable(q, gateways_iface, stream)
+
+ call_async(q, conn, 'Disconnect')
+
+ q.expect_many(
+ EventPattern('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_DISCONNECTED, cs.CSR_REQUESTED]),
+ EventPattern('stream-closed'),
+ )
+
+ stream.sendFooter()
+ q.expect('dbus-return', method='Disconnect')
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/httptest.py b/tests/twisted/httptest.py
new file mode 100644
index 0000000..ce9040c
--- /dev/null
+++ b/tests/twisted/httptest.py
@@ -0,0 +1,31 @@
+
+from twisted.web import http
+from twisted.internet import reactor
+
+from servicetest import Event
+
+class Request(http.Request):
+ def process(self):
+ self.queue.append(Event('http-request',
+ method=self.method, path=self.path, request=self))
+
+class HTTPChannel(http.HTTPChannel):
+ def requestFactory(self, *misc):
+ request = Request(*misc)
+ request.queue = self.queue
+ return request
+
+class HTTPFactory(http.HTTPFactory):
+ protocol = HTTPChannel
+
+ def __init__(self, queue):
+ self.queue = queue
+
+ def buildProtocol(self, addr):
+ protocol = http.HTTPFactory.buildProtocol(self, addr)
+ protocol.queue = self.queue
+ return protocol
+
+def listen_http(q, port=0):
+ return reactor.listenTCP(port, HTTPFactory(q))
+
diff --git a/tests/twisted/main-debug.c b/tests/twisted/main-debug.c
new file mode 100644
index 0000000..d7569c2
--- /dev/null
+++ b/tests/twisted/main-debug.c
@@ -0,0 +1,73 @@
+/*
+ * main.c - entry point for telepathy-gabble-debug used by tests
+ * Copyright (C) 2008 Collabora Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdlib.h>
+
+#include "gabble.h"
+#include "connection.h"
+#include "vcard-manager.h"
+#include "jingle-factory.h"
+#include "jingle-session.h"
+
+#include "test-resolver.h"
+
+#include <dbus/dbus.h>
+
+int
+main (int argc,
+ char **argv)
+{
+ int ret = 1;
+ GResolver *kludged;
+
+ gabble_init ();
+
+ /* needed for test-disco-no-reply.py */
+ gabble_connection_set_disco_reply_timeout (3);
+ /* needed for test-avatar-async.py */
+ gabble_vcard_manager_set_suspend_reply_timeout (3);
+ gabble_vcard_manager_set_default_request_timeout (3);
+
+ /* hook up the fake DNS resolver that lets us divert A and SRV queries *
+ * into our local cache before asking the real DNS */
+ kludged = g_object_new (TEST_TYPE_RESOLVER, NULL);
+ g_resolver_set_default (kludged);
+ g_object_unref (kludged);
+
+ test_resolver_add_A (TEST_RESOLVER (kludged),
+ "resolves-to-5.4.3.2", "5.4.3.2");
+ test_resolver_add_A (TEST_RESOLVER (kludged),
+ "resolves-to-1.2.3.4", "1.2.3.4");
+ test_resolver_add_A (TEST_RESOLVER (kludged),
+ "localhost", "127.0.0.1");
+ test_resolver_add_A (TEST_RESOLVER (kludged),
+ "stun.telepathy.im", "6.7.8.9");
+
+ gabble_jingle_factory_set_test_mode ();
+
+ ret = gabble_main (argc, argv);
+
+ /* Hack, remove the ref g_resolver has on this object, atm there is no way to
+ * unset a custom resolver */
+ g_object_unref (kludged);
+
+ dbus_shutdown ();
+
+ return ret;
+}
diff --git a/tests/twisted/mucutil.py b/tests/twisted/mucutil.py
new file mode 100644
index 0000000..3c30ea7
--- /dev/null
+++ b/tests/twisted/mucutil.py
@@ -0,0 +1,70 @@
+"""
+Utility functions for tests that need to interact with MUCs.
+"""
+
+import dbus
+
+from servicetest import call_async, wrap_channel, EventPattern
+from gabbletest import make_muc_presence, request_muc_handle
+
+import constants as cs
+import ns
+
+def join_muc(q, bus, conn, stream, muc, request=None,
+ also_capture=[], role='participant'):
+ """
+ Joins 'muc', returning the muc's handle, a proxy object for the channel,
+ its path and its immutable properties just after the CreateChannel event
+ has fired. The room contains one other member.
+ """
+ if request is None:
+ request = {
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_ROOM,
+ cs.TARGET_ID: muc,
+ }
+
+ muc_handle = request_muc_handle(q, conn, stream, muc)
+
+ requests = dbus.Interface(conn, cs.CONN_IFACE_REQUESTS)
+ call_async(q, requests, 'CreateChannel',
+ dbus.Dictionary(request, signature='sv'))
+
+ q.expect('stream-presence', to='%s/test' % muc)
+
+ # Send presence for other member of room.
+ stream.send(make_muc_presence('owner', 'moderator', muc, 'bob'))
+
+ # Send presence for own membership of room.
+ stream.send(make_muc_presence('none', role, muc, 'test'))
+
+ captured = q.expect_many(
+ EventPattern('dbus-return', method='CreateChannel'),
+ *also_capture)
+ path, props = captured[0].value
+ chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text',
+ ['Messages'])
+
+ return (muc_handle, chan, path, props) + tuple(captured[1:])
+
+def join_muc_and_check(q, bus, conn, stream, muc, request=None):
+ """
+ Like join_muc(), but also checks the NewChannels and NewChannel signals and
+ the Members property, and returns both members' handles.
+ """
+ muc_handle, chan, path, props = \
+ join_muc(q, bus, conn, stream, muc, request=request)
+
+ q.expect('dbus-signal', signal='NewChannels', args=[[(path, props)]])
+ q.expect('dbus-signal', signal='NewChannel',
+ args=[path, cs.CHANNEL_TYPE_TEXT, cs.HT_ROOM, muc_handle, True])
+
+ test_handle, bob_handle = conn.RequestHandles(cs.HT_CONTACT,
+ ['%s/test' % muc, '%s/bob' % muc])
+
+ members = chan.Get(cs.CHANNEL_IFACE_GROUP, 'Members',
+ dbus_interface=cs.PROPERTIES_IFACE)
+ assert set(members) == set([test_handle, bob_handle]), \
+ (members, (test_handle, bob_handle))
+
+ return (muc_handle, chan, test_handle, bob_handle)
diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py
new file mode 100644
index 0000000..2191259
--- /dev/null
+++ b/tests/twisted/ns.py
@@ -0,0 +1,61 @@
+AMP = "http://jabber.org/protocol/amp"
+BYTESTREAMS = 'http://jabber.org/protocol/bytestreams'
+CHAT_STATES = 'http://jabber.org/protocol/chatstates'
+CAPS = "http://jabber.org/protocol/caps"
+DISCO_INFO = "http://jabber.org/protocol/disco#info"
+DISCO_ITEMS = "http://jabber.org/protocol/disco#items"
+FEATURE_NEG = 'http://jabber.org/protocol/feature-neg'
+FILE_TRANSFER = 'http://jabber.org/protocol/si/profile/file-transfer'
+GEOLOC = 'http://jabber.org/protocol/geoloc'
+GOOGLE_FEAT_SESSION = 'http://www.google.com/xmpp/protocol/session'
+GOOGLE_FEAT_VOICE = 'http://www.google.com/xmpp/protocol/voice/v1'
+GOOGLE_FEAT_VIDEO = 'http://www.google.com/xmpp/protocol/video/v1'
+GOOGLE_JINGLE_INFO = 'google:jingleinfo'
+GOOGLE_P2P = "http://www.google.com/transport/p2p"
+GOOGLE_ROSTER = 'google:roster'
+GOOGLE_SESSION = "http://www.google.com/session"
+GOOGLE_SESSION_PHONE = "http://www.google.com/session/phone"
+GOOGLE_SESSION_VIDEO = "http://www.google.com/session/video"
+IBB = 'http://jabber.org/protocol/ibb'
+JINGLE_015 = "http://jabber.org/protocol/jingle"
+JINGLE_015_AUDIO = "http://jabber.org/protocol/jingle/description/audio"
+JINGLE_015_VIDEO = "http://jabber.org/protocol/jingle/description/video"
+JINGLE = "urn:xmpp:jingle:1"
+JINGLE_RTP = "urn:xmpp:jingle:apps:rtp:1"
+JINGLE_RTP_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"
+JINGLE_RTP_VIDEO = "urn:xmpp:jingle:apps:rtp:video"
+JINGLE_RTP_ERRORS = "urn:xmpp:jingle:apps:rtp:errors:1"
+JINGLE_RTP_INFO_1 = "urn:xmpp:jingle:apps:rtp:info:1"
+JINGLE_TRANSPORT_ICEUDP = "urn:xmpp:jingle:transports:ice-udp:1"
+JINGLE_TRANSPORT_RAWUDP = "urn:xmpp:jingle:transports:raw-udp:1"
+MUC = 'http://jabber.org/protocol/muc'
+MUC_BYTESTREAM = 'http://telepathy.freedesktop.org/xmpp/protocol/muc-bytestream'
+MUC_OWNER = '%s#owner' % MUC
+MUC_USER = '%s#user' % MUC
+NICK = "http://jabber.org/protocol/nick"
+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"
+PUBSUB_EVENT = "%s#event" % PUBSUB
+REGISTER = "jabber:iq:register"
+ROSTER = "jabber:iq:roster"
+SEARCH = 'jabber:iq:search'
+SI = 'http://jabber.org/protocol/si'
+SI_MULTIPLE = 'http://telepathy.freedesktop.org/xmpp/si-multiple'
+STANZA = "urn:ietf:params:xml:ns:xmpp-stanzas"
+STREAMS = "urn:ietf:params:xml:ns:xmpp-streams"
+TEMPPRES = "urn:xmpp:temppres:0"
+TUBES = 'http://telepathy.freedesktop.org/xmpp/tubes'
+VCARD_TEMP = 'vcard-temp'
+VCARD_TEMP_UPDATE = 'vcard-temp:x:update'
+X_DATA = 'jabber:x:data'
+XML = 'http://www.w3.org/XML/1998/namespace'
+X_OOB = 'jabber:x:oob'
diff --git a/tests/twisted/pubsub.py b/tests/twisted/pubsub.py
new file mode 100644
index 0000000..70de708
--- /dev/null
+++ b/tests/twisted/pubsub.py
@@ -0,0 +1,88 @@
+"""Send malformed pubsub notifications to be sure that Gabble isn't confused about those"""
+from gabbletest import exec_test, elem, sync_stream
+
+import constants as cs
+import ns
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ # event node without NS
+ message = elem('message', from_='bob@foo.com')(
+ elem('event')(
+ elem('items', node=ns.GEOLOC)(
+ elem('item', id='12345')(
+ elem(ns.GEOLOC, 'geoloc')(
+ elem ('country') (u'France'))))))
+ stream.send(message)
+
+ # event node with a wrong NS
+ message = elem('message', from_='bob@foo.com')(
+ elem('badger', 'event')(
+ elem('items', node=ns.GEOLOC)(
+ elem('item', id='12345')(
+ elem(ns.GEOLOC, 'geoloc')(
+ elem ('country') (u'France'))))))
+ stream.send(message)
+
+ # event node without 'from'
+ message = elem('message')(
+ elem((ns.PUBSUB_EVENT), 'event')(
+ elem('items', node=ns.GEOLOC)(
+ elem('item', id='12345')(
+ elem(ns.GEOLOC, 'geoloc')(
+ elem ('country') (u'France'))))))
+ stream.send(message)
+
+ # event node with an invalid 'from'
+ message = elem('message', from_='aaaa')(
+ elem((ns.PUBSUB_EVENT), 'event')(
+ elem('items', node=ns.GEOLOC)(
+ elem('item', id='12345')(
+ elem(ns.GEOLOC, 'geoloc')(
+ elem ('country') (u'France'))))))
+ stream.send(message)
+
+ # no items node
+ message = elem('message', from_='bob@foo.com')(
+ elem((ns.PUBSUB_EVENT), 'event')())
+ stream.send(message)
+
+ # no item node
+ message = elem('message', from_='bob@foo.com')(
+ elem((ns.PUBSUB_EVENT), 'event')(
+ elem('items', node=ns.GEOLOC)()))
+ stream.send(message)
+
+ # item node doesn't have any child
+ message = elem('message', from_='bob@foo.com')(
+ elem((ns.PUBSUB_EVENT), 'event')(
+ elem('items', node=ns.GEOLOC)(
+ elem('item', id='12345')())))
+ stream.send(message)
+
+ # the child of the item node doesn't have a NS
+ message = elem('message', from_='bob@foo.com')(
+ elem((ns.PUBSUB_EVENT), 'event')(
+ elem('items', node=ns.GEOLOC)(
+ elem('item', id='12345')(
+ elem('geoloc')(
+ elem ('country') (u'France'))))))
+ stream.send(message)
+
+ # valid but unknown pubsub notification
+ message = elem('message', from_='bob@foo.com')(
+ elem((ns.PUBSUB_EVENT), 'event')(
+ elem('items', node='http://www.badger.com')(
+ elem('item', id='12345')(
+ elem('http://www.badger.com', 'badger')(
+ elem ('mushroom') (u'snake'))))))
+ stream.send(message)
+
+ sync_stream(q, stream)
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/servicetest.py b/tests/twisted/servicetest.py
new file mode 100644
index 0000000..0634678
--- /dev/null
+++ b/tests/twisted/servicetest.py
@@ -0,0 +1,510 @@
+
+"""
+Infrastructure code for testing connection managers.
+"""
+
+from twisted.internet import glib2reactor
+from twisted.internet.protocol import Protocol, Factory, ClientFactory
+glib2reactor.install()
+import sys
+
+import pprint
+import unittest
+
+import dbus.glib
+
+from twisted.internet import reactor
+
+import constants as cs
+
+tp_name_prefix = 'org.freedesktop.Telepathy'
+tp_path_prefix = '/org/freedesktop/Telepathy'
+
+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 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 __repr__(self):
+ properties = dict(self.properties)
+
+ if self.predicate:
+ 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(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.forbidden_events = set()
+
+ if timeout is None:
+ self.timeout = 5
+ else:
+ self.timeout = timeout
+
+ def log(self, s):
+ if self.verbose:
+ print s
+
+ def log_event(self, event):
+ if self.verbose:
+ 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):
+ print "forbidden event occurred:"
+ for x in format_event(event):
+ print x
+ assert False
+
+ def expect(self, type, **kw):
+ pattern = EventPattern(type, **kw)
+
+ while True:
+ event = self.wait()
+ self.log_event(event)
+ self._check_forbidden(event)
+
+ if pattern.match(event):
+ self.log('handled')
+ self.log('')
+ return event
+
+ self.log('not handled')
+ self.log('')
+
+ def expect_many(self, *patterns):
+ ret = [None] * len(patterns)
+
+ while None in ret:
+ try:
+ event = self.wait()
+ 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.log_event(event)
+ self._check_forbidden(event)
+
+ for i, pattern in enumerate(patterns):
+ if ret[i] is None and pattern.match(event):
+ self.log('handled')
+ 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()
+ self.log_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_expect_many2(self):
+ # Test that events are only matched against patterns that haven't yet
+ # been matched. This tests a regression.
+ queue = TestEventQueue([Event('foo', x=1), Event('foo', x=2)])
+ foo1, foo2 = queue.expect_many(
+ EventPattern('foo'),
+ EventPattern('foo'))
+ assert foo1.type == 'foo' and foo1.x == 1
+ assert foo2.type == 'foo' and foo2.x == 2
+
+ 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()])
+
+ 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.handle_event(Event('dbus-return', method=method,
+ value=unwrap(ret)))
+
+ def error_func(err):
+ test.handle_event(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
+ # 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, '/')
+ call_async(
+ q, dbus.Interface(root_object, 'org.freedesktop.DBus.Peer'), 'Ping')
+ q.expect('dbus-return', method='Ping')
+
+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),
+ ('Location', cs.CONN_IFACE_LOCATION),
+ ('Future', tp_name_prefix + '.Connection.FUTURE'),
+ ]))
+
+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)
+ cm_iface = dbus.Interface(cm, tp_name_prefix + '.ConnectionManager')
+
+ connection_name, connection_path = cm_iface.RequestConnection(
+ proto, params)
+ conn = wrap_connection(bus.get_object(connection_name, connection_path))
+
+ bus.add_signal_receiver(
+ lambda *args, **kw:
+ event_func(
+ Event('dbus-signal',
+ path=unwrap(kw['path']),
+ signal=kw['member'], args=map(unwrap, args),
+ interface=kw['interface'])),
+ None, # signal name
+ None, # interface
+ None,
+ 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
+
+# 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.handle_event(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.handle_event(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.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)
+
+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 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 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
+
+if __name__ == '__main__':
+ unittest.main()
+
diff --git a/tests/twisted/test-debug.py b/tests/twisted/test-debug.py
new file mode 100644
index 0000000..2614eb3
--- /dev/null
+++ b/tests/twisted/test-debug.py
@@ -0,0 +1,55 @@
+
+"""
+Test the debug message interface.
+"""
+
+import dbus
+
+from gabbletest import exec_test
+import constants as cs
+from config import DEBUGGING
+
+if not DEBUGGING:
+ print " -- Not testing debugger, built with --disable-debug"
+ raise SystemExit(77)
+
+path = '/org/freedesktop/Telepathy/debug'
+iface = 'org.freedesktop.Telepathy.Debug'
+
+def test(q, bus, conn, stream):
+ messages = []
+
+ def new_message(timestamp, domain, level, string):
+ messages.append((timestamp, domain, level, string))
+
+ debug = bus.get_object(conn.bus_name, path)
+ debug_iface = dbus.Interface(debug, iface)
+ debug_iface.connect_to_signal('NewDebugMessage', new_message)
+ props_iface = dbus.Interface(debug, cs.PROPERTIES_IFACE)
+
+ assert len(debug_iface.GetMessages()) > 0
+
+ # Turn signalling on and generate some messages.
+
+ assert len(messages) == 0
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+ assert props_iface.Get(iface, 'Enabled') == False
+ props_iface.Set(iface, 'Enabled', True)
+
+ conn.RequestChannel(
+ cs.CHANNEL_TYPE_TEXT, cs.HT_CONTACT, conn.GetSelfHandle(), True)
+ q.expect('dbus-signal', signal='NewChannel')
+ assert len(messages) > 0
+
+ # Turn signalling off and check we have no new messages.
+
+ props_iface.Set(iface, 'Enabled', False)
+ snapshot = list(messages)
+
+ assert snapshot == messages
+
+if __name__ == '__main__':
+ exec_test(test)
+
diff --git a/tests/twisted/text/destroy.py b/tests/twisted/text/destroy.py
new file mode 100644
index 0000000..8fcb918
--- /dev/null
+++ b/tests/twisted/text/destroy.py
@@ -0,0 +1,125 @@
+"""
+Test text channel not being recreated because although there were still
+pending messages, we destroyed it with extreme prejudice.
+"""
+
+import dbus
+
+from twisted.words.xish import domish
+
+from gabbletest import exec_test
+from servicetest import call_async, EventPattern
+import constants as cs
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ self_handle = conn.GetSelfHandle()
+
+ jid = 'foo@bar.com'
+ call_async(q, conn, 'RequestHandles', 1, [jid])
+
+ event = q.expect('dbus-return', method='RequestHandles')
+ foo_handle = event.value[0][0]
+
+ call_async(q, conn, 'RequestChannel',
+ cs.CHANNEL_TYPE_TEXT, cs.HT_CONTACT, foo_handle, True)
+
+ ret, old_sig, new_sig = q.expect_many(
+ EventPattern('dbus-return', method='RequestChannel'),
+ EventPattern('dbus-signal', signal='NewChannel'),
+ EventPattern('dbus-signal', signal='NewChannels'),
+ )
+
+ text_chan = bus.get_object(conn.bus_name, ret.value[0])
+ chan_iface = dbus.Interface(text_chan, cs.CHANNEL)
+ text_iface = dbus.Interface(text_chan, cs.CHANNEL_TYPE_TEXT)
+ destroyable_iface = dbus.Interface(text_chan, cs.CHANNEL_IFACE_DESTROYABLE)
+
+ assert old_sig.args[0] == ret.value[0]
+ assert old_sig.args[1] == cs.CHANNEL_TYPE_TEXT
+ assert old_sig.args[2] == cs.HT_CONTACT
+ assert old_sig.args[3] == foo_handle
+ assert old_sig.args[4] == True # suppress handler
+
+ assert len(new_sig.args) == 1
+ assert len(new_sig.args[0]) == 1 # one channel
+ assert len(new_sig.args[0][0]) == 2 # two struct members
+ assert new_sig.args[0][0][0] == ret.value[0]
+ emitted_props = new_sig.args[0][0][1]
+ assert emitted_props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_TEXT
+ assert emitted_props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT
+ assert emitted_props[cs.TARGET_HANDLE] == foo_handle
+ assert emitted_props[cs.TARGET_ID] == jid
+ assert emitted_props[cs.REQUESTED] == True
+ assert emitted_props[cs.INITIATOR_HANDLE] == self_handle
+ assert emitted_props[cs.INITIATOR_ID] == 'test@localhost'
+
+ channel_props = text_chan.GetAll(
+ cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE)
+ assert channel_props['TargetID'] == jid, (channel_props['TargetID'], jid)
+ assert channel_props['Requested'] == True
+ assert channel_props['InitiatorHandle'] == self_handle,\
+ (channel_props['InitiatorHandle'], self_handle)
+ assert channel_props['InitiatorID'] == 'test@localhost',\
+ channel_props['InitiatorID']
+
+ text_iface.Send(0, 'hey')
+
+ event = q.expect('stream-message')
+
+ elem = event.stanza
+ assert elem.name == 'message'
+ assert elem['type'] == 'chat'
+ body = list(event.stanza.elements())[0]
+ assert body.name == 'body'
+ assert body.children[0] == u'hey'
+
+ # <message type="chat"><body>hello</body</message>
+ m = domish.Element((None, 'message'))
+ m['from'] = 'foo@bar.com/Pidgin'
+ m['type'] = 'chat'
+ m.addElement('body', content='hello')
+ stream.send(m)
+
+ event = q.expect('dbus-signal', signal='Received')
+
+ hello_message_id = event.args[0]
+ hello_message_time = event.args[1]
+ assert event.args[2] == foo_handle
+ # message type: normal
+ assert event.args[3] == 0
+ # flags: none
+ assert event.args[4] == 0
+ # body
+ assert event.args[5] == 'hello'
+
+ messages = text_chan.ListPendingMessages(False,
+ dbus_interface=cs.CHANNEL_TYPE_TEXT)
+ assert messages == \
+ [(hello_message_id, hello_message_time, foo_handle,
+ 0, 0, 'hello')], messages
+
+ # destroy the channel without acking the message; it does not come back
+
+ call_async(q, destroyable_iface, 'Destroy')
+
+ event = q.expect('dbus-signal', signal='Closed')
+ assert event.path == text_chan.object_path,\
+ (event.path, text_chan.object_path)
+
+ event = q.expect('dbus-return', method='Destroy')
+
+ # assert that it stays dead
+
+ try:
+ chan_iface.GetChannelType()
+ except dbus.DBusException:
+ pass
+ else:
+ raise AssertionError("Why won't it die?")
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/text/ensure.py b/tests/twisted/text/ensure.py
new file mode 100644
index 0000000..5fdf4bf
--- /dev/null
+++ b/tests/twisted/text/ensure.py
@@ -0,0 +1,171 @@
+"""
+Test text channel initiated by me, using Requests.EnsureChannel
+"""
+
+import dbus
+
+from gabbletest import exec_test
+from servicetest import call_async, EventPattern
+import constants as cs
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ self_handle = conn.GetSelfHandle()
+
+ jids = ['foo@bar.com', 'truc@cafe.fr']
+ call_async(q, conn, 'RequestHandles', 1, jids)
+
+ event = q.expect('dbus-return', method='RequestHandles')
+ handles = event.value[0]
+
+ properties = conn.GetAll(
+ cs.CONN_IFACE_REQUESTS, dbus_interface=cs.PROPERTIES_IFACE)
+ assert properties.get('Channels') == [], properties['Channels']
+ assert ({cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ },
+ [cs.TARGET_HANDLE, cs.TARGET_ID],
+ ) in properties.get('RequestableChannelClasses'),\
+ properties['RequestableChannelClasses']
+
+ test_ensure_ensure(q, conn, self_handle, jids[0], handles[0])
+ test_request_ensure(q, conn, self_handle, jids[1], handles[1])
+
+def test_ensure_ensure(q, conn, self_handle, jid, handle):
+ """
+ Test ensuring a non-existant channel twice. The first call should succeed
+ with Yours=True; the subsequent call should succeed with Yours=False
+ """
+
+ # Check that Ensuring a channel that doesn't exist succeeds
+ call_async(q, conn.Requests, 'EnsureChannel', request_props (handle))
+
+ ret, old_sig, new_sig = q.expect_many(
+ EventPattern('dbus-return', method='EnsureChannel'),
+ EventPattern('dbus-signal', signal='NewChannel'),
+ EventPattern('dbus-signal', signal='NewChannels'),
+ )
+
+ assert len(ret.value) == 3
+ yours, path, emitted_props = ret.value
+
+ # The channel was created in response to the call, and we were the only
+ # requestor, so we should get Yours=True
+ assert yours, ret.value
+
+ check_props(emitted_props, self_handle, handle, jid)
+
+ assert len(old_sig.args) == 5
+ old_path, old_ct, old_ht, old_h, old_sh = old_sig.args
+
+ assert old_path == path
+ assert old_ct == cs.CHANNEL_TYPE_TEXT
+ assert old_ht == cs.HT_CONTACT
+ assert old_h == handle
+ assert old_sh == True # suppress handler
+
+ assert len(new_sig.args) == 1
+ assert len(new_sig.args[0]) == 1 # one channel
+ assert len(new_sig.args[0][0]) == 2 # two struct members
+ assert new_sig.args[0][0][0] == path
+ assert new_sig.args[0][0][1] == emitted_props
+
+ properties = conn.GetAll(
+ cs.CONN_IFACE_REQUESTS, dbus_interface=dbus.PROPERTIES_IFACE)
+
+ assert new_sig.args[0][0] in properties['Channels'], \
+ (new_sig.args[0][0], properties['Channels'])
+
+
+ # Now try Ensuring a channel which already exists
+ call_async(q, conn.Requests, 'EnsureChannel', request_props(handle))
+ ret_ = q.expect('dbus-return', method='EnsureChannel')
+
+ assert len(ret_.value) == 3
+ yours_, path_, emitted_props_ = ret_.value
+
+ # Someone's already responsible for this channel, so we should get
+ # Yours=False
+ assert not yours_, ret_.value
+ assert path == path_, (path, path_)
+ assert emitted_props == emitted_props_, (emitted_props, emitted_props_)
+
+
+def test_request_ensure(q, conn, self_handle, jid, handle):
+ """
+ Test Creating a non-existant channel, then Ensuring the same channel.
+ The call to Ensure should succeed with Yours=False.
+ """
+
+ call_async(q, conn.Requests, 'CreateChannel', request_props(handle))
+
+ ret, old_sig, new_sig = q.expect_many(
+ EventPattern('dbus-return', method='CreateChannel'),
+ EventPattern('dbus-signal', signal='NewChannel'),
+ EventPattern('dbus-signal', signal='NewChannels'),
+ )
+
+ assert len(ret.value) == 2
+ path, emitted_props = ret.value
+
+ check_props(emitted_props, self_handle, handle, jid)
+
+ assert len(old_sig.args) == 5
+ old_path, old_ct, old_ht, old_h, old_sh = old_sig.args
+
+ assert old_path == path
+ assert old_ct == cs.CHANNEL_TYPE_TEXT
+ assert old_ht == cs.HT_CONTACT
+ assert old_h == handle
+ assert old_sh == True # suppress handler
+
+ assert len(new_sig.args) == 1
+ assert len(new_sig.args[0]) == 1 # one channel
+ assert len(new_sig.args[0][0]) == 2 # two struct members
+ assert new_sig.args[0][0][0] == path
+ assert new_sig.args[0][0][1] == emitted_props
+
+ properties = conn.GetAll(
+ cs.CONN_IFACE_REQUESTS, dbus_interface=dbus.PROPERTIES_IFACE)
+
+ assert new_sig.args[0][0] in properties['Channels'], \
+ (new_sig.args[0][0], properties['Channels'])
+
+
+ # Now try Ensuring that same channel.
+ call_async(q, conn.Requests, 'EnsureChannel', request_props(handle))
+ ret_ = q.expect('dbus-return', method='EnsureChannel')
+
+ assert len(ret_.value) == 3
+ yours_, path_, emitted_props_ = ret_.value
+
+ # Someone's already responsible for this channel, so we should get
+ # Yours=False
+ assert not yours_, ret_.value
+ assert path == path_, (path, path_)
+ assert emitted_props == emitted_props_, (emitted_props, emitted_props_)
+
+
+def check_props(props, self_handle, handle, jid):
+ assert props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_TEXT
+ assert props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT
+ assert props[cs.TARGET_HANDLE] == handle
+ assert props[cs.TARGET_ID] == jid
+ assert props[cs.REQUESTED] == True
+ assert props[cs.INITIATOR_HANDLE] == self_handle
+ assert props[cs.INITIATOR_ID] == 'test@localhost'
+
+
+def request_props(handle):
+ return { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: handle,
+ }
+
+
+if __name__ == '__main__':
+ exec_test(test)
+
diff --git a/tests/twisted/text/initiate-requestotron.py b/tests/twisted/text/initiate-requestotron.py
new file mode 100644
index 0000000..75ef7c0
--- /dev/null
+++ b/tests/twisted/text/initiate-requestotron.py
@@ -0,0 +1,72 @@
+"""
+Test text channel initiated by me, using Requests.
+"""
+
+import dbus
+
+from gabbletest import exec_test
+from servicetest import call_async, EventPattern
+import constants as cs
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ self_handle = conn.GetSelfHandle()
+
+ jid = 'foo@bar.com'
+ foo_handle = conn.RequestHandles(cs.HT_CONTACT, [jid])[0]
+
+ properties = conn.GetAll(
+ cs.CONN_IFACE_REQUESTS, dbus_interface=dbus.PROPERTIES_IFACE)
+ assert properties.get('Channels') == [], properties['Channels']
+ assert ({cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ },
+ [cs.TARGET_HANDLE, cs.TARGET_ID],
+ ) in properties.get('RequestableChannelClasses'),\
+ properties['RequestableChannelClasses']
+
+ call_async(q, conn.Requests, 'CreateChannel',
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: foo_handle,
+ })
+
+ ret, old_sig, new_sig = q.expect_many(
+ EventPattern('dbus-return', method='CreateChannel'),
+ EventPattern('dbus-signal', signal='NewChannel'),
+ EventPattern('dbus-signal', signal='NewChannels'),
+ )
+
+ assert len(ret.value) == 2
+ emitted_props = ret.value[1]
+ assert emitted_props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_TEXT
+ assert emitted_props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT
+ assert emitted_props[cs.TARGET_HANDLE] == foo_handle
+ assert emitted_props[cs.TARGET_ID] == jid
+ assert emitted_props[cs.REQUESTED] == True
+ assert emitted_props[cs.INITIATOR_HANDLE] == self_handle
+ assert emitted_props[cs.INITIATOR_ID] == 'test@localhost'
+
+ assert old_sig.args[0] == ret.value[0]
+ assert old_sig.args[1] == cs.CHANNEL_TYPE_TEXT
+ assert old_sig.args[2] == cs.HT_CONTACT
+ assert old_sig.args[3] == foo_handle
+ assert old_sig.args[4] == True # suppress handler
+
+ assert len(new_sig.args) == 1
+ assert len(new_sig.args[0]) == 1 # one channel
+ assert len(new_sig.args[0][0]) == 2 # two struct members
+ assert new_sig.args[0][0][0] == ret.value[0]
+ assert new_sig.args[0][0][1] == ret.value[1]
+
+ properties = conn.GetAll(
+ cs.CONN_IFACE_REQUESTS, dbus_interface=dbus.PROPERTIES_IFACE)
+
+ assert new_sig.args[0][0] in properties['Channels'], \
+ (new_sig.args[0][0], properties['Channels'])
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/text/initiate.py b/tests/twisted/text/initiate.py
new file mode 100644
index 0000000..dae48e5
--- /dev/null
+++ b/tests/twisted/text/initiate.py
@@ -0,0 +1,93 @@
+"""
+Test text channel initiated by me.
+"""
+
+import dbus
+
+from twisted.words.xish import domish
+
+from gabbletest import exec_test
+from servicetest import call_async, EventPattern
+import constants as cs
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ self_handle = conn.GetSelfHandle()
+
+ jid = 'foo@bar.com'
+ call_async(q, conn, 'RequestHandles', cs.HT_CONTACT, [jid])
+
+ event = q.expect('dbus-return', method='RequestHandles')
+ foo_handle = event.value[0][0]
+
+ call_async(q, conn, 'RequestChannel',
+ cs.CHANNEL_TYPE_TEXT, cs.HT_CONTACT, foo_handle, True)
+
+ ret, sig = q.expect_many(
+ EventPattern('dbus-return', method='RequestChannel'),
+ EventPattern('dbus-signal', signal='NewChannel'),
+ )
+
+ text_chan = bus.get_object(conn.bus_name, ret.value[0])
+
+ assert sig.args[0] == ret.value[0], \
+ (sig.args[0], ret.value[0])
+ assert sig.args[1] == cs.CHANNEL_TYPE_TEXT, sig.args[1]
+ # check that handle type == contact handle
+ assert sig.args[2] == 1, sig.args[1]
+ assert sig.args[3] == foo_handle, (sig.args[3], foo_handle)
+ assert sig.args[4] == True # suppress handler
+
+ # Exercise basic Channel Properties from spec 0.17.7
+ channel_props = text_chan.GetAll(
+ cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE)
+ assert channel_props.get('TargetHandle') == foo_handle,\
+ (channel_props.get('TargetHandle'), foo_handle)
+ assert channel_props.get('TargetHandleType') == 1,\
+ channel_props.get('TargetHandleType')
+ assert channel_props.get('ChannelType') == \
+ cs.CHANNEL_TYPE_TEXT,\
+ channel_props.get('ChannelType')
+ assert cs.CHANNEL_IFACE_CHAT_STATE in \
+ channel_props.get('Interfaces', ()), \
+ channel_props.get('Interfaces')
+ assert channel_props['TargetID'] == jid,\
+ (channel_props['TargetID'], jid)
+ assert channel_props['Requested'] == True
+ assert channel_props['InitiatorHandle'] == self_handle,\
+ (channel_props['InitiatorHandle'], self_handle)
+ assert channel_props['InitiatorID'] == 'test@localhost',\
+ channel_props['InitiatorID']
+
+ dbus.Interface(text_chan, cs.CHANNEL_TYPE_TEXT).Send(0, 'hey')
+
+ event = q.expect('stream-message')
+
+ elem = event.stanza
+ assert elem.name == 'message'
+ assert elem['type'] == 'chat'
+ body = list(event.stanza.elements())[0]
+ assert body.name == 'body'
+ assert body.children[0] == u'hey'
+
+ # <message type="chat"><body>hello</body</message>
+ m = domish.Element((None, 'message'))
+ m['from'] = 'foo@bar.com/Pidgin'
+ m['type'] = 'chat'
+ m.addElement('body', content='hello')
+ stream.send(m)
+
+ event = q.expect('dbus-signal', signal='Received')
+
+ # message type: normal
+ assert event.args[3] == 0
+ # flags: none
+ assert event.args[4] == 0
+ # body
+ assert event.args[5] == 'hello'
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/text/respawn.py b/tests/twisted/text/respawn.py
new file mode 100644
index 0000000..55ceb11
--- /dev/null
+++ b/tests/twisted/text/respawn.py
@@ -0,0 +1,172 @@
+"""
+Test text channel being recreated because there are still pending messages.
+"""
+
+import dbus
+
+from twisted.words.xish import domish
+
+from gabbletest import exec_test
+from servicetest import call_async, EventPattern
+import constants as cs
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ self_handle = conn.GetSelfHandle()
+
+ jid = 'foo@bar.com'
+ foo_handle = conn.RequestHandles(cs.HT_CONTACT, [jid])[0]
+
+ call_async(q, conn, 'RequestChannel',
+ cs.CHANNEL_TYPE_TEXT, cs.HT_CONTACT, foo_handle, True)
+
+ ret, old_sig, new_sig = q.expect_many(
+ EventPattern('dbus-return', method='RequestChannel'),
+ EventPattern('dbus-signal', signal='NewChannel'),
+ EventPattern('dbus-signal', signal='NewChannels'),
+ )
+
+ text_chan = bus.get_object(conn.bus_name, ret.value[0])
+ chan_iface = dbus.Interface(text_chan, cs.CHANNEL)
+ text_iface = dbus.Interface(text_chan, cs.CHANNEL_TYPE_TEXT)
+
+ assert old_sig.args[0] == ret.value[0]
+ assert old_sig.args[1] == cs.CHANNEL_TYPE_TEXT
+ assert old_sig.args[2] == cs.HT_CONTACT
+ assert old_sig.args[3] == foo_handle
+ assert old_sig.args[4] == True # suppress handler
+
+ assert len(new_sig.args) == 1
+ assert len(new_sig.args[0]) == 1 # one channel
+ assert len(new_sig.args[0][0]) == 2 # two struct members
+ assert new_sig.args[0][0][0] == ret.value[0]
+ emitted_props = new_sig.args[0][0][1]
+ assert emitted_props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_TEXT
+ assert emitted_props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT
+ assert emitted_props[cs.TARGET_HANDLE] == foo_handle
+ assert emitted_props[cs.TARGET_ID] == jid
+ assert emitted_props[cs.REQUESTED] == True
+ assert emitted_props[cs.INITIATOR_HANDLE] == self_handle
+ assert emitted_props[cs.INITIATOR_ID] == 'test@localhost'
+
+ channel_props = text_chan.GetAll(
+ cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE)
+ assert channel_props['TargetID'] == jid,\
+ (channel_props['TargetID'], jid)
+ assert channel_props['Requested'] == True
+ assert channel_props['InitiatorHandle'] == self_handle,\
+ (channel_props['InitiatorHandle'], self_handle)
+ assert channel_props['InitiatorID'] == 'test@localhost',\
+ channel_props['InitiatorID']
+
+ text_iface.Send(0, 'hey')
+
+ event = q.expect('stream-message')
+
+ elem = event.stanza
+ assert elem.name == 'message'
+ assert elem['type'] == 'chat'
+ body = list(event.stanza.elements())[0]
+ assert body.name == 'body'
+ assert body.children[0] == u'hey'
+
+ # <message type="chat"><body>hello</body</message>
+ m = domish.Element((None, 'message'))
+ m['from'] = 'foo@bar.com/Pidgin'
+ m['type'] = 'chat'
+ m.addElement('body', content='hello')
+ stream.send(m)
+
+ event = q.expect('dbus-signal', signal='Received')
+
+ hello_message_id = event.args[0]
+ hello_message_time = event.args[1]
+ assert event.args[2] == foo_handle
+ # message type: normal
+ assert event.args[3] == 0
+ # flags: none
+ assert event.args[4] == 0
+ # body
+ assert event.args[5] == 'hello'
+
+ messages = text_chan.ListPendingMessages(False,
+ dbus_interface=cs.CHANNEL_TYPE_TEXT)
+ assert messages == \
+ [(hello_message_id, hello_message_time, foo_handle,
+ 0, 0, 'hello')], messages
+
+ # close the channel without acking the message; it comes back
+
+ call_async(q, chan_iface, 'Close')
+
+ old, new = q.expect_many(
+ EventPattern('dbus-signal', signal='Closed'),
+ EventPattern('dbus-signal', signal='ChannelClosed'),
+ )
+ assert old.path == text_chan.object_path,\
+ (old.path, text_chan.object_path)
+ assert new.args[0] == text_chan.object_path,\
+ (new.args[0], text_chan.object_path)
+
+ event = q.expect('dbus-signal', signal='NewChannel')
+ assert event.args[0] == text_chan.object_path
+ assert event.args[1] == cs.CHANNEL_TYPE_TEXT
+ assert event.args[2] == cs.HT_CONTACT
+ assert event.args[3] == foo_handle
+ assert event.args[4] == False # suppress handler
+
+ event = q.expect('dbus-return', method='Close')
+
+ # it now behaves as if the message had initiated it
+
+ channel_props = text_chan.GetAll(
+ cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE)
+ assert channel_props['TargetID'] == jid,\
+ (channel_props['TargetID'], jid)
+ assert channel_props['Requested'] == False
+ assert channel_props['InitiatorHandle'] == foo_handle,\
+ (channel_props['InitiatorHandle'], foo_handle)
+ assert channel_props['InitiatorID'] == 'foo@bar.com',\
+ channel_props['InitiatorID']
+
+ # the message is still there
+
+ messages = text_chan.ListPendingMessages(False,
+ dbus_interface=cs.CHANNEL_TYPE_TEXT)
+ assert messages == \
+ [(hello_message_id, hello_message_time, foo_handle,
+ 0, 8, 'hello')], messages
+
+ # acknowledge it
+
+ text_chan.AcknowledgePendingMessages([hello_message_id],
+ dbus_interface=cs.CHANNEL_TYPE_TEXT)
+
+ messages = text_chan.ListPendingMessages(False,
+ dbus_interface=cs.CHANNEL_TYPE_TEXT)
+ assert messages == []
+
+ # close the channel again
+
+ call_async(q, chan_iface, 'Close')
+
+ event = q.expect('dbus-signal', signal='Closed')
+ assert event.path == text_chan.object_path,\
+ (event.path, text_chan.object_path)
+
+ event = q.expect('dbus-return', method='Close')
+
+ # assert that it stays dead this time!
+
+ try:
+ chan_iface.GetChannelType()
+ except dbus.DBusException:
+ pass
+ else:
+ raise AssertionError("Why won't it die?")
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/text/send-error.py b/tests/twisted/text/send-error.py
new file mode 100644
index 0000000..039223c
--- /dev/null
+++ b/tests/twisted/text/send-error.py
@@ -0,0 +1,184 @@
+"""
+Test that an incoming <message><error/></> for a contact gives both a SendError
+and a delivery report on a 1-1 text channel to that contact.
+"""
+
+from twisted.words.xish import domish
+
+from gabbletest import exec_test
+from servicetest import call_async, EventPattern
+import constants as cs
+import ns
+
+def test_temporary_error(q, bus, conn, stream):
+ self_handle = conn.GetSelfHandle()
+
+ jid = 'foo@bar.com'
+ call_async(q, conn, 'RequestHandles', 1, [jid])
+
+ event = q.expect('dbus-return', method='RequestHandles')
+ foo_handle = event.value[0][0]
+
+ path = conn.Requests.CreateChannel(
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: foo_handle,
+ })[0]
+ text_chan = bus.get_object(conn.bus_name, path)
+
+ # <message from='foo@bar.com' type='error'>
+ # <body>what is up, my good sir?</body>
+ # <error type='wait'>
+ # <resource-constraint xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ # </error>
+ # </message>
+ message_body = 'what is up, my good sir?'
+
+ m = domish.Element((None, 'message'))
+ m['from'] = 'foo@bar.com'
+ m['id'] = '1845a1a9-f7bc-4a2e-a885-633aadc81e1b'
+ m['type'] = 'error'
+ m.addElement('body', content=message_body)
+
+ e = domish.Element((None, 'error'))
+ e['type'] = 'wait'
+ e.addElement((ns.STANZA, 'resource-constraint'))
+
+ m.addChild(e)
+
+ stream.send(m)
+
+ send_error, received, message_received = q.expect_many(
+ EventPattern('dbus-signal', signal='SendError'),
+ EventPattern('dbus-signal', signal='Received'),
+ EventPattern('dbus-signal', signal='MessageReceived'),
+ )
+
+ expected_send_error = 4 # Too_Long
+
+ assert send_error.args[0] == expected_send_error, send_error.args
+ # FIXME: It doesn't look like it's possible to know what the original
+ # message type is, given that the type attribute of <message> is 'error'
+ # for error reports.
+ #assert send_error.args[2] == 0, send_error.args
+ assert send_error.args[3] == message_body, send_error.args
+
+ assert received.args[2] == foo_handle, (received.args, foo_handle)
+ assert received.args[3] == 4, received.args # Channel_Text_Message_Type_Delivery_Report
+ assert received.args[4] == 2, received.args # Channel_Text_Message_Flag_Non_Text_Content
+ assert received.args[5] == '', received.args
+
+ delivery_report = message_received.args[0]
+ assert len(delivery_report) == 1, delivery_report
+ header = delivery_report[0]
+ assert header['message-sender'] == foo_handle, header
+ assert header['message-type'] == 4, header # Channel_Text_Message_Type_Delivery_Report
+ assert header['delivery-status'] == 2, header # Delivery_Status_Temporarily_Failed
+ assert header['delivery-token'] == '1845a1a9-f7bc-4a2e-a885-633aadc81e1b',\
+ header
+ assert header['delivery-error'] == expected_send_error, header
+
+ delivery_echo = header['delivery-echo']
+ assert len(delivery_echo) == 2, delivery_echo
+
+ assert delivery_echo[0]['message-sender'] == self_handle, delivery_echo
+ assert delivery_echo[0]['message-token'] == \
+ '1845a1a9-f7bc-4a2e-a885-633aadc81e1b', delivery_echo
+ # FIXME: see above
+ #assert delivery_echo[0]['message-type'] == 0, delivery_echo
+
+ assert delivery_echo[1]['content-type'] == "text/plain", delivery_echo
+ assert delivery_echo[1]['content'] == message_body, delivery_echo
+
+
+def test_permanent_error(q, bus, conn, stream):
+ self_handle = conn.GetSelfHandle()
+
+ jid = 'wee@ninja.jp'
+ call_async(q, conn, 'RequestHandles', 1, [jid])
+
+ event = q.expect('dbus-return', method='RequestHandles')
+ ninja_handle = event.value[0][0]
+
+ path = conn.Requests.CreateChannel(
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: ninja_handle,
+ })[0]
+ text_chan = bus.get_object(conn.bus_name, path)
+
+ # <message from='wee@ninja.jp' type='error'>
+ # <body>hello? is there anyone there?</body>
+ # <error type='cancel'>
+ # <item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ # </error>
+ # </message>
+ message_body = 'hello? is there anyone there?'
+
+ m = domish.Element((None, 'message'))
+ m['from'] = 'wee@ninja.jp'
+ m['type'] = 'error'
+ m.addElement('body', content=message_body)
+
+ e = domish.Element((None, 'error'))
+ e['type'] = 'cancel'
+ e.addElement((ns.STANZA, 'item-not-found'))
+
+ m.addChild(e)
+
+ stream.send(m)
+
+ send_error, received, message_received = q.expect_many(
+ EventPattern('dbus-signal', signal='SendError'),
+ EventPattern('dbus-signal', signal='Received'),
+ EventPattern('dbus-signal', signal='MessageReceived'),
+ )
+
+ expected_send_error = 2 # Invalid_Contact
+
+ assert send_error.args[0] == expected_send_error, send_error.args
+ # FIXME: It doesn't look like it's possible to know what the original
+ # message type is, given that the type attribute of <message> is 'error'
+ # for error reports.
+ #assert send_error.args[2] == 0, send_error.args
+ assert send_error.args[3] == message_body, send_error.args
+
+ assert received.args[2] == ninja_handle, (received.args, ninja_handle)
+ assert received.args[3] == 4, received.args # Channel_Text_Message_Type_Delivery_Report
+ assert received.args[4] == 2, received.args # Channel_Text_Message_Flag_Non_Text_Content
+ assert received.args[5] == '', received.args
+
+ delivery_report = message_received.args[0]
+ assert len(delivery_report) == 1, delivery_report
+ header = delivery_report[0]
+ assert header['message-sender'] == ninja_handle, header
+ assert header['message-type'] == 4, header # Channel_Text_Message_Type_Delivery_Report
+ assert header['delivery-status'] == 3, header # Delivery_Status_Permanently_Failed
+ # the error has no ID, therefore its Telepathy rendition has no
+ # delivery-token
+ assert 'delivery-token' not in header, header
+ assert header['delivery-error'] == expected_send_error, header
+
+ delivery_echo = header['delivery-echo']
+ assert len(delivery_echo) == 2, delivery_echo
+
+ assert delivery_echo[0]['message-sender'] == self_handle, delivery_echo
+ # the error has no ID, therefore the echo's Telepathy rendition has no
+ # message-token
+ assert 'message-token' not in delivery_echo[0], delivery_echo
+ # FIXME: see above
+ #assert delivery_echo[0]['message-type'] == 0, delivery_echo
+
+ assert delivery_echo[1]['content-type'] == "text/plain", delivery_echo
+ assert delivery_echo[1]['content'] == message_body, delivery_echo
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ test_temporary_error(q, bus, conn, stream)
+ test_permanent_error(q, bus, conn, stream)
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/text/send-to-correct-resource.py b/tests/twisted/text/send-to-correct-resource.py
new file mode 100644
index 0000000..4668d8a
--- /dev/null
+++ b/tests/twisted/text/send-to-correct-resource.py
@@ -0,0 +1,79 @@
+"""
+Regression test for https://bugs.freedesktop.org/show_bug.cgi?id=22369.
+"""
+
+from twisted.words.xish import domish
+
+from servicetest import wrap_channel
+from gabbletest import exec_test
+import constants as cs
+import ns
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ # <wjt> I need a random name generator
+ # <fledermaus> Macro-Variable Spin Gel
+ contact = 'macro-variable.spin.gel@example.com'
+ contact_a = '%s/n810' % contact
+ contact_b = '%s/laptop' % contact
+
+ path, _ = conn.Requests.CreateChannel({
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_ID: contact,
+ })
+ chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text')
+
+ # When we start a conversation, Gabble should send to the bare JID.
+ chan.Text.Send(0, 'hey, you around?')
+ q.expect('stream-message', to=contact)
+
+ # A particular resource replies.
+ m = domish.Element((None, 'message'))
+ m['from'] = contact_a
+ m['type'] = 'chat'
+ m.addElement('body', content="i'm on a beach at Gran Canaria!")
+ stream.send(m)
+
+ q.expect('dbus-signal', signal='Received')
+
+ # Now that we got a reply from a particular resource, Gabble should reply
+ # there.
+ chan.Text.Send(0, 'nice')
+ q.expect('stream-message', to=contact_a)
+
+ # Now another resource messages us
+ m = domish.Element((None, 'message'))
+ m['from'] = contact_b
+ m['type'] = 'chat'
+ m.addElement('body', content="I brought my laptop to the Empathy hackfest")
+ stream.send(m)
+
+ q.expect('dbus-signal', signal='Received')
+
+ # Gabble should have updated the resource it's sending to.
+ chan.Text.Send(0, "don't get sand in the keyboard")
+ e = q.expect('stream-message', to=contact_b)
+
+ # But actually that resource has gone offline:
+ m = e.stanza
+ m['from'] = contact_b
+ m['type'] = 'error'
+ del m['to']
+
+ err = m.addElement((None, 'error'))
+ err['type'] = 'cancel'
+ err.addElement((ns.STANZA, 'item-not-found'))
+
+ stream.send(m)
+ q.expect('dbus-signal', signal='SendError')
+
+ # So as a result, Gabble should send the next message to the bare JID.
+ chan.Text.Send(0, "... i guess my warning was too late")
+ q.expect('stream-message', to=contact)
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/text/test-chat-state.py b/tests/twisted/text/test-chat-state.py
new file mode 100644
index 0000000..289f692
--- /dev/null
+++ b/tests/twisted/text/test-chat-state.py
@@ -0,0 +1,303 @@
+# coding=utf-8
+"""
+Test that chat state notifications are correctly sent and received on text
+channels.
+"""
+
+from twisted.words.xish import domish
+
+from servicetest import assertEquals, assertLength, wrap_channel, EventPattern
+from gabbletest import exec_test, make_result_iq, sync_stream, make_presence
+import constants as cs
+import ns
+
+def check_state_notification(elem, name, allow_body=False):
+ assertEquals('message', elem.name)
+ assertEquals('chat', elem['type'])
+
+ children = list(elem.elements())
+ notification = [x for x in children if x.uri == ns.CHAT_STATES][0]
+ assert notification.name == name, notification.toXml()
+
+ if not allow_body:
+ assert len(children) == 1, elem.toXml()
+
+def make_message(jid, body=None, state=None):
+ m = domish.Element((None, 'message'))
+ m['from'] = jid
+ m['type'] = 'chat'
+
+ if state is not None:
+ m.addElement((ns.CHAT_STATES, state))
+
+ if body is not None:
+ m.addElement('body', content=body)
+
+ return m
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ self_handle = conn.GetSelfHandle()
+
+ jid = 'foo@bar.com'
+ full_jid = 'foo@bar.com/Foo'
+ foo_handle = conn.RequestHandles(cs.HT_CONTACT, [jid])[0]
+
+ path = conn.Requests.CreateChannel(
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: foo_handle,
+ })[0]
+ chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text',
+ ['ChatState', 'Destroyable'])
+
+ presence = make_presence(full_jid, status='hello',
+ caps={
+ 'node': 'http://telepathy.freedesktop.org/homeopathy',
+ 'ver' : '0.1',
+ })
+ stream.send(presence)
+
+ version_event = q.expect('stream-iq', to=full_jid,
+ query_ns=ns.DISCO_INFO,
+ query_node='http://telepathy.freedesktop.org/homeopathy#0.1')
+
+ result = make_result_iq(stream, version_event.stanza)
+ query = result.firstChildElement()
+ feature = query.addElement('feature')
+ feature['var'] = ns.CHAT_STATES
+ stream.send(result)
+
+ sync_stream(q, stream)
+
+ # Receiving chat states:
+
+ # Composing...
+ stream.send(make_message(full_jid, state='composing'))
+
+ changed = q.expect('dbus-signal', signal='ChatStateChanged')
+ handle, state = changed.args
+ assertEquals(foo_handle, handle)
+ assertEquals(cs.CHAT_STATE_COMPOSING, state)
+
+ # Message!
+ stream.send(make_message(full_jid, body='hello', state='active'))
+
+ changed = q.expect('dbus-signal', signal='ChatStateChanged')
+ handle, state = changed.args
+ assertEquals(foo_handle, handle)
+ assertEquals(cs.CHAT_STATE_ACTIVE, state)
+
+ # Sending chat states:
+
+ # Composing...
+ chan.ChatState.SetChatState(cs.CHAT_STATE_COMPOSING)
+
+ stream_message = q.expect('stream-message')
+ check_state_notification(stream_message.stanza, 'composing')
+
+ # XEP 0085:
+ # every content message SHOULD contain an <active/> notification.
+ chan.Text.Send(0, 'hi.')
+
+ stream_message = q.expect('stream-message')
+ elem = stream_message.stanza
+ assertEquals('chat', elem['type'])
+
+ check_state_notification(elem, 'active', allow_body=True)
+
+ def is_body(e):
+ if e.name == 'body':
+ assert e.children[0] == u'hi.', e.toXml()
+ return True
+ return False
+
+ assert len([x for x in elem.elements() if is_body(x)]) == 1, elem.toXml()
+
+ # Close the channel without acking the received message. The peer should
+ # get a <gone/> notification, and the channel should respawn.
+ chan.Close()
+
+ gone, _, _ = q.expect_many(
+ EventPattern('stream-message'),
+ EventPattern('dbus-signal', signal='Closed'),
+ EventPattern('dbus-signal', signal='NewChannel'),
+ )
+ check_state_notification(gone.stanza, 'gone')
+
+ # Reusing the proxy object because we happen to know it'll be at the same
+ # path...
+
+ # Destroy the channel. The peer shouldn't get a <gone/> notification, since
+ # we already said we were gone and haven't sent them any messages to the
+ # contrary.
+ es = [EventPattern('stream-message')]
+ q.forbid_events(es)
+
+ chan.Destroyable.Destroy()
+ sync_stream(q, stream)
+
+ # Make the channel anew.
+ path = conn.Requests.CreateChannel(
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: foo_handle,
+ })[0]
+ chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text',
+ ['ChatState', 'Destroyable'])
+
+ # Close it immediately; the peer should again not get a <gone/>
+ # notification, since we haven't sent any notifications on that channel.
+ chan.Close()
+ sync_stream(q, stream)
+ q.unforbid_events(es)
+
+ # XEP-0085 §5.1 defines how to negotiate support for chat states with a
+ # contact in the absence of capabilities. This is useful when talking to
+ # invisible contacts, for example.
+
+ # First, if we receive a message from a contact, containing an <active/>
+ # notification, they support chat states, so we should send them.
+
+ jid = 'i@example.com'
+ full_jid = jid + '/GTalk'
+
+ path = conn.Requests.CreateChannel(
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_ID: jid,
+ })[0]
+ chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text',
+ ['ChatState'])
+
+ stream.send(make_message(full_jid, body='i am invisible', state='active'))
+
+ changed = q.expect('dbus-signal', signal='ChatStateChanged')
+ assertEquals(cs.CHAT_STATE_ACTIVE, changed.args[1])
+
+ # We've seen them send a chat state notification, so we should send them
+ # notifications when the UI tells us to.
+ chan.ChatState.SetChatState(cs.CHAT_STATE_COMPOSING)
+ stream_message = q.expect('stream-message', to=full_jid)
+ check_state_notification(stream_message.stanza, 'composing')
+
+ chan.Text.Send(0, 'very convincing')
+ stream_message = q.expect('stream-message', to=full_jid)
+ check_state_notification(stream_message.stanza, 'active', allow_body=True)
+
+ # Now, test the case where we start the negotiation, and the contact
+ # turns out to support chat state notifications.
+
+ jid = 'c@example.com'
+ full_jid = jid + '/GTalk'
+ path = conn.Requests.CreateChannel(
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_ID: jid,
+ })[0]
+ chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text',
+ ['ChatState'])
+
+ # We shouldn't send any notifications until we actually send a message.
+ e = EventPattern('stream-message', to=jid)
+ q.forbid_events([e])
+ for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE,
+ cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]:
+ chan.ChatState.SetChatState(i)
+ sync_stream(q, stream)
+ q.unforbid_events([e])
+
+ # When we send a message, say we're active.
+ chan.Text.Send(0, 'is anyone there?')
+ stream_message = q.expect('stream-message', to=jid)
+ check_state_notification(stream_message.stanza, 'active', allow_body=True)
+
+ # We get a notification back from our contact.
+ stream.send(make_message(full_jid, state='composing'))
+
+ changed = q.expect('dbus-signal', signal='ChatStateChanged')
+ _, state = changed.args
+ assertEquals(cs.CHAT_STATE_COMPOSING, state)
+
+ # So now we know they support notification, so should send notifications.
+ chan.ChatState.SetChatState(cs.CHAT_STATE_COMPOSING)
+
+ # This doesn't check whether we're sending to the bare jid, or the
+ # jid+resource. In fact, the notification is sent to the bare jid, because
+ # we only update which jid we send to when we actually receive a message,
+ # not when we receive a notification. wjt thinks this is less surprising
+ # than the alternative:
+ #
+ # • I'm talking to you on my N900, and signed in on my laptop;
+ # • I enter one character in a tab to you on my laptop, and then delete
+ # it;
+ # • Now your messages to me appear on my laptop (until I send you another
+ # one from my N900)!
+ stream_message = q.expect('stream-message')
+ check_state_notification(stream_message.stanza, 'composing')
+
+ # But! Now they start messaging us from a different client, which *doesn't*
+ # support notifications.
+ other_jid = jid + '/Library'
+ stream.send(make_message(other_jid, body='grr, library computers'))
+ q.expect('dbus-signal', signal='Received')
+
+ # Okay, we should stop sending typing notifications.
+ e = EventPattern('stream-message', to=other_jid)
+ q.forbid_events([e])
+ for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE,
+ cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]:
+ chan.ChatState.SetChatState(i)
+ sync_stream(q, stream)
+ q.unforbid_events([e])
+
+ # Now, test the case where we start the negotiation, and the contact
+ # does not support chat state notifications
+
+ jid = 'twitterbot@example.com'
+ full_jid = jid + '/Nonsense'
+ path = conn.Requests.CreateChannel(
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_ID: jid,
+ })[0]
+ chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text',
+ ['ChatState'])
+
+ # We shouldn't send any notifications until we actually send a message.
+ e = EventPattern('stream-message', to=jid)
+ q.forbid_events([e])
+ for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE,
+ cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]:
+ chan.ChatState.SetChatState(i)
+ sync_stream(q, stream)
+ q.unforbid_events([e])
+
+ # When we send a message, say we're active.
+ chan.Text.Send(0, '#n900 #maemo #zomg #woo #yay http://bit.ly/n900')
+ stream_message = q.expect('stream-message', to=jid)
+ check_state_notification(stream_message.stanza, 'active', allow_body=True)
+
+ # They reply without a chat state.
+ stream.send(make_message(full_jid, body="posted."))
+ q.expect('dbus-signal', signal='Received')
+
+ # Okay, we shouldn't send any more.
+ e = EventPattern('stream-message', to=other_jid)
+ q.forbid_events([e])
+ for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE,
+ cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]:
+ chan.ChatState.SetChatState(i)
+ sync_stream(q, stream)
+ q.unforbid_events([e])
+
+ chan.Text.Send(0, '@stephenfry simmer down')
+ message = q.expect('stream-message')
+ states = [x for x in message.stanza.elements() if x.uri == ns.CHAT_STATES]
+ assertLength(0, states)
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/text/test-text-delayed.py b/tests/twisted/text/test-text-delayed.py
new file mode 100644
index 0000000..1a652cf
--- /dev/null
+++ b/tests/twisted/text/test-text-delayed.py
@@ -0,0 +1,56 @@
+
+"""
+Test receiving delayed (offline) messages on a text channel.
+"""
+
+import datetime
+
+from twisted.words.xish import domish
+
+from gabbletest import exec_test
+from servicetest import EventPattern
+import constants as cs
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ m = domish.Element((None, 'message'))
+ m['from'] = 'foo@bar.com'
+ m['type'] = 'chat'
+ m.addElement('body', content='hello')
+
+ # add timestamp information
+ x = m.addElement(('jabber:x:delay', 'x'))
+ x['stamp'] = '20070517T16:15:01'
+
+ stream.send(m)
+
+ event = q.expect('dbus-signal', signal='NewChannel')
+ assert event.args[1] == cs.CHANNEL_TYPE_TEXT
+ assert event.args[2] == cs.HT_CONTACT
+ jid = conn.InspectHandles(cs.HT_CONTACT, [event.args[3]])[0]
+ assert jid == 'foo@bar.com'
+
+ received, message_received = q.expect_many(
+ EventPattern('dbus-signal', signal='Received'),
+ EventPattern('dbus-signal', signal='MessageReceived'),
+ )
+
+ assert (str(datetime.datetime.utcfromtimestamp(received.args[1]))
+ == '2007-05-17 16:15:01')
+ assert received.args[5] == 'hello'
+
+ message = message_received.args[0]
+ header = message[0]
+ message_sent_timestamp = header['message-sent']
+ assert str(datetime.datetime.utcfromtimestamp(message_sent_timestamp)
+ == '2007-05-17 16:15:01'), header
+ message_received_timestamp = header['message-received']
+ assert message_received_timestamp > message_sent_timestamp, header
+
+ assert message[1]['content'] == 'hello', message
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/text/test-text-no-body.py b/tests/twisted/text/test-text-no-body.py
new file mode 100644
index 0000000..09ae6f4
--- /dev/null
+++ b/tests/twisted/text/test-text-no-body.py
@@ -0,0 +1,40 @@
+
+"""
+Test that <message>s with a chat state notification but no body don't create a
+new text channel.
+"""
+
+from twisted.words.xish import domish
+
+from gabbletest import exec_test
+import constants as cs
+import ns
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ # message without body
+ m = domish.Element((None, 'message'))
+ m['from'] = 'alice@foo.com'
+ m['type'] = 'chat'
+ m.addElement((ns.CHAT_STATES, 'composing'))
+ stream.send(m)
+
+ # message with body
+ m = domish.Element((None, 'message'))
+ m['from'] = 'bob@foo.com'
+ m['type'] = 'chat'
+ m.addElement((ns.CHAT_STATES, 'active'))
+ m.addElement('body', content='hello')
+ stream.send(m)
+
+ # first message should be from Bob, not Alice
+ event = q.expect('dbus-signal', signal='NewChannel')
+ assert event.args[1] == cs.CHANNEL_TYPE_TEXT
+ jid = conn.InspectHandles(cs.HT_CONTACT, [event.args[3]])[0]
+ assert jid == 'bob@foo.com'
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/text/test-text.py b/tests/twisted/text/test-text.py
new file mode 100644
index 0000000..e685429
--- /dev/null
+++ b/tests/twisted/text/test-text.py
@@ -0,0 +1,177 @@
+
+"""
+Test text channel.
+"""
+
+import dbus
+
+from twisted.words.xish import domish
+
+from gabbletest import exec_test
+from servicetest import EventPattern, wrap_channel, assertNotEquals
+import constants as cs
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ id = '1845a1a9-f7bc-4a2e-a885-633aadc81e1b'
+
+ # <message type="chat"><body>hello</body</message>
+ m = domish.Element((None, 'message'))
+ m['from'] = 'foo@bar.com/Pidgin'
+ m['id'] = id
+ m['type'] = 'chat'
+ m.addElement('body', content='hello')
+ stream.send(m)
+
+ event = q.expect('dbus-signal', signal='NewChannel')
+ text_chan = wrap_channel(
+ bus.get_object(conn.bus_name, event.args[0]), 'Text', ['Messages'])
+ assert event.args[1] == cs.CHANNEL_TYPE_TEXT
+ assert event.args[2] == cs.HT_CONTACT
+ foo_at_bar_dot_com_handle = event.args[3]
+ jid = conn.InspectHandles(1, [foo_at_bar_dot_com_handle])[0]
+ assert jid == 'foo@bar.com'
+ assert event.args[4] == False # suppress handler
+
+ # Exercise basic Channel Properties from spec 0.17.7
+ channel_props = text_chan.Properties.GetAll(cs.CHANNEL)
+ assert channel_props.get('TargetHandle') == event.args[3],\
+ (channel_props.get('TargetHandle'), event.args[3])
+ assert channel_props.get('TargetHandleType') == cs.HT_CONTACT,\
+ channel_props.get('TargetHandleType')
+ assert channel_props.get('ChannelType') == \
+ cs.CHANNEL_TYPE_TEXT,\
+ channel_props.get('ChannelType')
+ assert cs.CHANNEL_IFACE_CHAT_STATE in \
+ channel_props.get('Interfaces', ()), \
+ channel_props.get('Interfaces')
+ assert cs.CHANNEL_IFACE_MESSAGES in \
+ channel_props.get('Interfaces', ()), \
+ channel_props.get('Interfaces')
+ assert channel_props['TargetID'] == jid,\
+ (channel_props['TargetID'], jid)
+ assert channel_props['Requested'] == False
+ assert channel_props['InitiatorHandle'] == event.args[3],\
+ (channel_props['InitiatorHandle'], event.args[3])
+ assert channel_props['InitiatorID'] == jid,\
+ (channel_props['InitiatorID'], jid)
+
+ received, message_received = q.expect_many(
+ EventPattern('dbus-signal', signal='Received'),
+ EventPattern('dbus-signal', signal='MessageReceived'),
+ )
+
+ # Check that C.T.Text.Received looks right
+ # message type: normal
+ assert received.args[3] == 0
+ # flags: none
+ assert received.args[4] == 0
+ # body
+ assert received.args[5] == 'hello'
+
+
+ # Check that C.I.Messages.MessageReceived looks right.
+ message = message_received.args[0]
+
+ # message should have two parts: the header and one content part
+ assert len(message) == 2, message
+ header, body = message
+
+ assert header['message-sender'] == foo_at_bar_dot_com_handle, header
+ # the spec says that message-type "MAY be omitted for normal chat
+ # messages."
+ assert 'message-type' not in header or header['message-type'] == 0, header
+
+ # This looks wrong, but is correct. We don't know if our contacts generate
+ # message id='' attributes which are unique enough for our requirements, so
+ # we should not use them as the message-token for incoming messages.
+ assertNotEquals(id, header['message-token'])
+
+ assert body['content-type'] == 'text/plain', body
+ assert body['content'] == 'hello', body
+
+ # Remove the message from the pending message queue, and check that
+ # PendingMessagesRemoved fires.
+ message_id = header['pending-message-id']
+
+ text_chan.Text.AcknowledgePendingMessages([message_id])
+
+ removed = q.expect('dbus-signal', signal='PendingMessagesRemoved')
+
+ removed_ids = removed.args[0]
+ assert len(removed_ids) == 1, removed_ids
+ assert removed_ids[0] == message_id, (removed_ids, message_id)
+
+ # Send a Notice using the Messages API
+ greeting = [
+ dbus.Dictionary({ 'message-type': 2, # Notice
+ }, signature='sv'),
+ { 'content-type': 'text/plain',
+ 'content': u"what up",
+ }
+ ]
+
+ sent_token = text_chan.Messages.SendMessage(greeting, dbus.UInt32(0))
+
+ stream_message, sent, message_sent = q.expect_many(
+ EventPattern('stream-message'),
+ EventPattern('dbus-signal', signal='Sent'),
+ EventPattern('dbus-signal', signal='MessageSent'),
+ )
+
+ elem = stream_message.stanza
+ assert elem.name == 'message'
+ assert elem['type'] == 'normal'
+ body = list(stream_message.stanza.elements())[0]
+ assert body.name == 'body'
+ assert body.children[0] == u'what up'
+
+ sent_message = message_sent.args[0]
+ assert len(sent_message) == 2, sent_message
+ header = sent_message[0]
+ assert header['message-type'] == 2, header # Notice
+ assert header['message-token'] == sent_token, header
+ body = sent_message[1]
+ assert body['content-type'] == 'text/plain', body
+ assert body['content'] == u'what up', body
+
+ assert message_sent.args[2] == sent_token
+
+ assert sent.args[1] == 2, sent.args # Notice
+ assert sent.args[2] == u'what up', sent.args
+
+
+ # Send a message using Channel.Type.Text API
+ text_chan.Text.Send(0, 'goodbye')
+
+ stream_message, sent, message_sent = q.expect_many(
+ EventPattern('stream-message'),
+ EventPattern('dbus-signal', signal='Sent'),
+ EventPattern('dbus-signal', signal='MessageSent'),
+ )
+
+ elem = stream_message.stanza
+ assert elem.name == 'message'
+ assert elem['type'] == 'chat'
+ body = list(stream_message.stanza.elements())[0]
+ assert body.name == 'body'
+ assert body.children[0] == u'goodbye'
+
+ sent_message = message_sent.args[0]
+ assert len(sent_message) == 2, sent_message
+ header = sent_message[0]
+ # the spec says that message-type "MAY be omitted for normal chat
+ # messages."
+ assert 'message-type' not in header or header['message-type'] == 0, header
+ body = sent_message[1]
+ assert body['content-type'] == 'text/plain', body
+ assert body['content'] == u'goodbye', body
+
+ assert sent.args[1] == 0, sent.args # message type normal
+ assert sent.args[2] == u'goodbye', sent.args
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/tools/Makefile.am b/tests/twisted/tools/Makefile.am
new file mode 100644
index 0000000..efcff37
--- /dev/null
+++ b/tests/twisted/tools/Makefile.am
@@ -0,0 +1,33 @@
+exec-with-log.sh: exec-with-log.sh.in
+ $(AM_V_GEN)sed -e "s|[@]abs_top_builddir[@]|@abs_top_builddir@|g" \
+ -e "s|[@]abs_top_srcdir[@]|@abs_top_srcdir@|g" $< > $@
+ @chmod +x $@
+
+%.conf: %.conf.in
+ $(AM_V_GEN)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
+ $(AM_V_GEN)sed -e "s|[@]abs_top_builddir[@]|@abs_top_builddir@|g" $< > $@
+
+# D-Bus service file for testing
+service_in_files = gabble.service.in
+service_files = org.freedesktop.Telepathy.ConnectionManager.gabble.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) exec-with-log.sh
+
+EXTRA_DIST = \
+ $(service_in_files) \
+ $(conf_in_files) \
+ exec-with-log.sh.in \
+ with-session-bus.sh
+
+CLEANFILES = \
+ $(BUILT_SOURCES) \
+ gabble-testing.log
diff --git a/tests/twisted/tools/exec-with-log.sh b/tests/twisted/tools/exec-with-log.sh
new file mode 100755
index 0000000..d443fb8
--- /dev/null
+++ b/tests/twisted/tools/exec-with-log.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+cd "/home/kalfa/moblin/src/telepathy-gabble/tests/twisted/tools"
+
+export GABBLE_DEBUG=all LM_DEBUG=net GIBBER_DEBUG=all WOCKY_DEBUG=all
+export GABBLE_TIMING=1
+export GABBLE_PLUGIN_DIR="/home/kalfa/moblin/src/telepathy-gabble/plugins/.libs"
+ulimit -c unlimited
+exec >> gabble-testing.log 2>&1
+
+if test -n "$GABBLE_TEST_VALGRIND"; then
+ export G_DEBUG=${G_DEBUG:+"${G_DEBUG},"}gc-friendly
+ export 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=/home/kalfa/moblin/src/telepathy-gabble/tests/suppressions/tp-glib.supp"
+ GABBLE_WRAPPER="$GABBLE_WRAPPER --suppressions=/home/kalfa/moblin/src/telepathy-gabble/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"
+fi
+
+export G_DEBUG=fatal-warnings,fatal-criticals" ${G_DEBUG}"
+exec /home/kalfa/moblin/src/telepathy-gabble/libtool --mode=execute $GABBLE_WRAPPER ../telepathy-gabble-debug
diff --git a/tests/twisted/tools/exec-with-log.sh.in b/tests/twisted/tools/exec-with-log.sh.in
new file mode 100644
index 0000000..7fb1758
--- /dev/null
+++ b/tests/twisted/tools/exec-with-log.sh.in
@@ -0,0 +1,33 @@
+#!/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 GABBLE_PLUGIN_DIR="@abs_top_builddir@/plugins/.libs"
+ulimit -c unlimited
+exec >> gabble-testing.log 2>&1
+
+if test -n "$GABBLE_TEST_VALGRIND"; then
+ export G_DEBUG=${G_DEBUG:+"${G_DEBUG},"}gc-friendly
+ export 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"
+fi
+
+export G_DEBUG=fatal-warnings,fatal-criticals" ${G_DEBUG}"
+exec @abs_top_builddir@/libtool --mode=execute $GABBLE_WRAPPER ../telepathy-gabble-debug
diff --git a/tests/twisted/tools/failure-helper.sh b/tests/twisted/tools/failure-helper.sh
new file mode 100644
index 0000000..d3e427c
--- /dev/null
+++ b/tests/twisted/tools/failure-helper.sh
@@ -0,0 +1,105 @@
+#!/bin/sh
+#
+# Run a given test repeatedly until it fails at least once and passes at least
+# once. Places logs from the test and from the service being tested in the
+# given directory, and generates diffs between the fail and pass cases.
+
+error()
+{
+ echo "$@" >&2
+ exit 1
+}
+
+abspath()
+{
+ if echo "$1" | grep -q "^/"; then
+ echo "$1"
+ else
+ echo "$PWD/$1"
+ fi
+}
+
+stripdiff()
+{
+ a=`mktemp`
+ b=`mktemp`
+ python ../../tools/log-strip.py < "$1" > "$a"
+ python ../../tools/log-strip.py < "$2" > "$b"
+ diff -U40 "$a" "$b"
+ rm "$a" "$b"
+}
+
+prog=gabble
+test_name="$1"
+log_dir="$2"
+
+usage="usage: $0 test-name log-directory"
+test -n "$test_name" || error "$usage"
+test -n "$log_dir" || error "$usage"
+
+cd `dirname $0`/..
+test -f "servicetest.py" || error "can't find servicetest.py"
+test -f "$test_name" || error "can't find that test"
+
+if ! test -d "$log_dir"; then
+ if ! test -e "$log_dir"; then
+ mkdir "$log_dir"
+ else
+ error "not a directory: $log_dir"
+ fi
+fi
+
+log_dir=`abspath "$log_dir"`
+test_pass_log="$log_dir/test-pass.log"
+test_fail_log="$log_dir/test-fail.log"
+prog_pass_log="$log_dir/$prog-pass.log"
+prog_fail_log="$log_dir/$prog-fail.log"
+
+if test -e "$test_pass_log" -a -e "$prog_pass_log"; then
+ echo "using existing pass"
+ got_pass=true
+else
+ got_pass=false
+fi
+
+if test -e "$test_fail_log" -a -e "$prog_fail_log"; then
+ echo "using existing fail"
+ got_fail=true
+else
+ got_fail=false
+fi
+
+run=1
+
+while test "$got_pass" != true -o "$got_fail" != true; do
+ echo -n "run $run: "
+
+ CHECK_TWISTED_VERBOSE=1 make check-twisted TWISTED_TESTS="$test_name" \
+ > "$log_dir/test.log" 2>&1
+ ret=$?
+
+ if test $ret -eq 0; then
+ echo "pass"
+ else
+ echo "fail"
+ fi
+
+ if test $ret -eq 0 -a "$got_pass" != true; then
+ mv "$log_dir/test.log" "$test_pass_log"
+ cp "tools/$prog-testing.log" "$prog_pass_log"
+ got_pass=true
+ elif test $ret -ne 0 -a "$got_fail" != true; then
+ mv "$log_dir/test.log" "$test_fail_log"
+ cp "tools/$prog-testing.log" "$prog_fail_log"
+ got_fail=true
+ else
+ rm "$log_dir/test.log"
+ fi
+
+ run=`expr $run + 1`
+done
+
+stripdiff "$test_pass_log" "$test_fail_log" > "$log_dir/test-log.diff"
+stripdiff "$prog_pass_log" "$prog_fail_log" > "$log_dir/$prog-log.diff"
+
+echo done
diff --git a/tests/twisted/tools/gabble.service.in b/tests/twisted/tools/gabble.service.in
new file mode 100644
index 0000000..6845c59
--- /dev/null
+++ b/tests/twisted/tools/gabble.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.Telepathy.ConnectionManager.gabble
+Exec=@abs_top_builddir@/tests/twisted/tools/exec-with-log.sh
diff --git a/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service
new file mode 100644
index 0000000..877cea2
--- /dev/null
+++ b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.Telepathy.ConnectionManager.gabble
+Exec=/home/kalfa/moblin/src/telepathy-gabble/tests/twisted/tools/exec-with-log.sh
diff --git a/tests/twisted/tools/tmp-session-bus.conf b/tests/twisted/tools/tmp-session-bus.conf
new file mode 100644
index 0000000..828a33e
--- /dev/null
+++ b/tests/twisted/tools/tmp-session-bus.conf
@@ -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>/home/kalfa/moblin/src/telepathy-gabble/tests/twisted/tools</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/tools/tmp-session-bus.conf.in b/tests/twisted/tools/tmp-session-bus.conf.in
new file mode 100644
index 0000000..84d8d65
--- /dev/null
+++ b/tests/twisted/tools/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/twisted/tools</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/tools/with-session-bus.sh b/tests/twisted/tools/with-session-bus.sh
new file mode 100644
index 0000000..9fc0be1
--- /dev/null
+++ b/tests/twisted/tools/with-session-bus.sh
@@ -0,0 +1,96 @@
+#!/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"
+sleep=0
+
+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
+ --sleep=*)
+ sleep="$1"
+ sleep="${sleep#--sleep=}"
+ shift
+ ;;
+ --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
+# Break glass in case of emergency.
+#dbus-monitor &
+
+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=$?
+
+if test $sleep != 0; then
+ sleep $sleep
+fi
+
+trap - INT HUP TERM
+cleanup
+
+exit $e