From f08161df2b0e9f46271a5bb3ba5a15694642b169 Mon Sep 17 00:00:00 2001 From: Cosimo Alfarano Date: Mon, 1 Feb 2010 15:38:10 +0000 Subject: imported tests from telepathy-gabble * test folder imported from telepathy-gabble, twisted/ still not working but cleaned-up of clearly useless tests. it still needs a further clean-up and adaptation in order to be used. * basic C tests written * autotools (configure and Makefile .au) updated for Python and Twisted checking. * autotools (configure and Makefile .au) updated for DBus and DBus-glib basic requirement --- INSTALL | 97 ++- Makefile.am | 2 +- configure.ac | 27 +- tests/Makefile.am | 48 ++ tests/README | 86 ++ tests/suppressions/Makefile.am | 2 + tests/suppressions/tpl.supp | 250 ++++++ tests/test-basic-connect.sh | 2 + tests/test-tpl-channel.c | 29 + tests/test-tpl-conf | 148 ++++ tests/test-tpl-conf.c | 30 + tests/test-tpl-log-entry.c | 27 + tests/test-tpl-observer | 148 ++++ tests/test-tpl-observer.c | 31 + tests/tpl-channel-child.c | 665 ++++++++++++++++ tests/twisted/Makefile.am | 130 +++ tests/twisted/bytestream.py | 882 +++++++++++++++++++++ tests/twisted/constants.py | 306 +++++++ tests/twisted/gabbletest.py | 606 ++++++++++++++ tests/twisted/gateways.py | 94 +++ tests/twisted/httptest.py | 31 + tests/twisted/main-debug.c | 73 ++ tests/twisted/mucutil.py | 70 ++ tests/twisted/ns.py | 61 ++ tests/twisted/pubsub.py | 88 ++ tests/twisted/servicetest.py | 510 ++++++++++++ tests/twisted/test-debug.py | 55 ++ tests/twisted/text/destroy.py | 125 +++ tests/twisted/text/ensure.py | 171 ++++ tests/twisted/text/initiate-requestotron.py | 72 ++ tests/twisted/text/initiate.py | 93 +++ tests/twisted/text/respawn.py | 172 ++++ tests/twisted/text/send-error.py | 184 +++++ tests/twisted/text/send-to-correct-resource.py | 79 ++ tests/twisted/text/test-chat-state.py | 303 +++++++ tests/twisted/text/test-text-delayed.py | 56 ++ tests/twisted/text/test-text-no-body.py | 40 + tests/twisted/text/test-text.py | 177 +++++ tests/twisted/tools/Makefile.am | 33 + tests/twisted/tools/exec-with-log.sh | 33 + tests/twisted/tools/exec-with-log.sh.in | 33 + tests/twisted/tools/failure-helper.sh | 105 +++ tests/twisted/tools/gabble.service.in | 3 + ...ktop.Telepathy.ConnectionManager.gabble.service | 3 + tests/twisted/tools/tmp-session-bus.conf | 30 + tests/twisted/tools/tmp-session-bus.conf.in | 30 + tests/twisted/tools/with-session-bus.sh | 96 +++ 47 files changed, 6316 insertions(+), 20 deletions(-) create mode 100644 tests/Makefile.am create mode 100644 tests/README create mode 100644 tests/suppressions/Makefile.am create mode 100644 tests/suppressions/tpl.supp create mode 100644 tests/test-basic-connect.sh create mode 100644 tests/test-tpl-channel.c create mode 100755 tests/test-tpl-conf create mode 100644 tests/test-tpl-conf.c create mode 100644 tests/test-tpl-log-entry.c create mode 100755 tests/test-tpl-observer create mode 100644 tests/test-tpl-observer.c create mode 100644 tests/tpl-channel-child.c create mode 100644 tests/twisted/Makefile.am create mode 100644 tests/twisted/bytestream.py create mode 100644 tests/twisted/constants.py create mode 100644 tests/twisted/gabbletest.py create mode 100644 tests/twisted/gateways.py create mode 100644 tests/twisted/httptest.py create mode 100644 tests/twisted/main-debug.c create mode 100644 tests/twisted/mucutil.py create mode 100644 tests/twisted/ns.py create mode 100644 tests/twisted/pubsub.py create mode 100644 tests/twisted/servicetest.py create mode 100644 tests/twisted/test-debug.py create mode 100644 tests/twisted/text/destroy.py create mode 100644 tests/twisted/text/ensure.py create mode 100644 tests/twisted/text/initiate-requestotron.py create mode 100644 tests/twisted/text/initiate.py create mode 100644 tests/twisted/text/respawn.py create mode 100644 tests/twisted/text/send-error.py create mode 100644 tests/twisted/text/send-to-correct-resource.py create mode 100644 tests/twisted/text/test-chat-state.py create mode 100644 tests/twisted/text/test-text-delayed.py create mode 100644 tests/twisted/text/test-text-no-body.py create mode 100644 tests/twisted/text/test-text.py create mode 100644 tests/twisted/tools/Makefile.am create mode 100755 tests/twisted/tools/exec-with-log.sh create mode 100644 tests/twisted/tools/exec-with-log.sh.in create mode 100644 tests/twisted/tools/failure-helper.sh create mode 100644 tests/twisted/tools/gabble.service.in create mode 100644 tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service create mode 100644 tests/twisted/tools/tmp-session-bus.conf create mode 100644 tests/twisted/tools/tmp-session-bus.conf.in create mode 100644 tests/twisted/tools/with-session-bus.sh 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 + +#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 + +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 + +#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 + +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 + */ + +/* + * 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 + +#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'' + >>> elem('foo', x='1')().toXml() + u"" + >>> elem('foo', x='1')(u'hello').toXml() + u"hello" + >>> elem('foo', x='1')(u'hello', + ... elem('http://foo.org', 'bar', y='2')(u'bye')).toXml() + u"hellobye" + """ + + 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 + + # 4a1... + 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 + +#include "gabble.h" +#include "connection.h" +#include "vcard-manager.h" +#include "jingle-factory.h" +#include "jingle-session.h" + +#include "test-resolver.h" + +#include + +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' + + # hello + 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' + + # hello + 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' + + # hello + 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 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) + + # + # what is up, my good sir? + # + # + # + # + 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 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) + + # + # hello? is there anyone there? + # + # + # + # + 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 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]) + + # I need a random name generator + # 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 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 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 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 + # 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 + # 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 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' + + # hello + 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 @@ + + + + + + session + + unix:tmpdir=/tmp + + /home/kalfa/moblin/src/telepathy-gabble/tests/twisted/tools + + + + + + + + + + + + + + + + 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 @@ + + + + + + session + + unix:tmpdir=/tmp + + @abs_top_builddir@/tests/twisted/tools + + + + + + + + + + + + + + + + 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. +# +# 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-.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 -- cgit v1.2.1