a5f0139a10
As reported by Canonical, syncing fails if data items contain text which is not correct UTF-8 in one of the fields that SyncEvolution logs in the command line output (like SUMMARY of a calendar event). That is because the byte string coming from the item is passed unchecked to the D-Bus implementation for transmission via D-Bus. But D-Bus strings must be correct UTF-8, so depending on the D-Bus library in use, one gets a segfault (GIO D-Bus, due to an unchecked NULL pointer access) or an "out of memory" error (libdbus, which checks for NULL). What the D-Bus bindings now do is checking the string in advance (to avoid error messages inside the D-Bus implementations) and then replacing all invalid bytes with question marks. The rest of the string is preserved. Handling this inside the D-Bus binding layer is not the "correct" solution (which would be to check for UTF-8 in the higher layers were such bad data might get into SyncEvolution), but it is the less intrusive and more complete one. Changing the bindings such that all strings must be declared explicitly as "UTF-8 string" would have been a way to find all places where such checks are missing, but it turned out to be too complex and requiring too many changes.
9885 lines
461 KiB
Python
Executable file
9885 lines
461 KiB
Python
Executable file
#! /usr/bin/python -u
|
|
# -*- coding: utf-8 -*-
|
|
# vim: set fileencoding=utf-8 :#
|
|
#
|
|
# Copyright (C) 2009 Intel Corporation
|
|
#
|
|
# 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) version 3.
|
|
#
|
|
# 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 Street, Fifth Floor, Boston, MA
|
|
# 02110-1301 USA
|
|
|
|
import random
|
|
import unittest
|
|
import subprocess
|
|
import time
|
|
import os
|
|
import errno
|
|
import signal
|
|
import shutil
|
|
import copy
|
|
import heapq
|
|
import string
|
|
import difflib
|
|
import traceback
|
|
import ConfigParser
|
|
import io
|
|
import inspect
|
|
import gzip
|
|
import httplib
|
|
import socket
|
|
import stat
|
|
|
|
import dbus
|
|
from dbus.mainloop.glib import DBusGMainLoop
|
|
import dbus.service
|
|
import sys
|
|
import traceback
|
|
import re
|
|
import atexit
|
|
import base64
|
|
|
|
# introduced in python-gobject 2.16, not available
|
|
# on all Linux distros => make it optional
|
|
glib = None
|
|
try:
|
|
import glib
|
|
except ImportError:
|
|
try:
|
|
from gi.repository import GLib as glib
|
|
except ImportError:
|
|
pass
|
|
|
|
gobject = None
|
|
try:
|
|
import gobject
|
|
except ImportError:
|
|
try:
|
|
from gi.repository import GObject as gobject
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
DBusGMainLoop(set_as_default=True)
|
|
|
|
debugger = os.environ.get("TEST_DBUS_GDB", None) and "gdb" or ""
|
|
# Default verbosity is DEBUG.
|
|
if os.environ.get("TEST_DBUS_QUIET", False):
|
|
level = 0
|
|
else:
|
|
level = 3
|
|
|
|
usingDLT = os.environ.get("TEST_DBUS_DLT", False)
|
|
if usingDLT:
|
|
logging = '--no-syslog --stdout --dlt'
|
|
else:
|
|
logging = '--no-syslog --stdout'
|
|
server = ("syncevo-dbus-server %s --verbosity=%d --dbus-verbosity=%d" % (logging, level, level)).split()
|
|
|
|
# primarily for XDG files, but also other temporary files
|
|
xdg_root = "temp-test-dbus"
|
|
configName = "dbus_unittest"
|
|
|
|
def usingValgrind():
|
|
return 'valgrind' in os.environ.get("TEST_DBUS_PREFIX", "")
|
|
|
|
def which(program):
|
|
'''find absolute path to program (simple file name, no path) in PATH env variable'''
|
|
def isExe(fpath):
|
|
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
|
|
|
for path in os.environ["PATH"].split(os.pathsep):
|
|
exeFile = os.path.join(path, program)
|
|
if isExe(exeFile):
|
|
return os.path.abspath(exeFile)
|
|
|
|
return None
|
|
|
|
def listall(dirs, exclude=[], includedata=False):
|
|
'''returns list of all dirs and files in the given dirs, excluding entries matching one
|
|
of the regular expressions'''
|
|
|
|
result = {}
|
|
def append(dirname, entry):
|
|
fullname = os.path.join(dirname, entry)
|
|
for pattern in exclude:
|
|
if re.match(pattern, fullname):
|
|
return
|
|
s = os.stat(fullname)
|
|
if includedata and stat.S_ISREG(s.st_mode):
|
|
data = open(fullname).read()
|
|
if filter(lambda x: x not in string.printable, data):
|
|
data = ' '.join(['%02x'%ord(x) for x in data])
|
|
result[fullname] = (s.st_mtime, data)
|
|
else:
|
|
result[fullname] = s.st_mtime
|
|
for dir in dirs:
|
|
for dirname, dirnames, filenames in os.walk(dir):
|
|
for subdirname in dirnames:
|
|
append(dirname, subdirname)
|
|
for filename in filenames:
|
|
append(dirname, filename)
|
|
return result
|
|
|
|
def GrepNotifications(dbuslog):
|
|
'''finds all Notify calls and returns their parameters as list of line lists'''
|
|
return re.findall(r'^method call .* dest=.* .*interface=org.freedesktop.Notifications; member=Notify\n((?:^ .*\n)*)',
|
|
dbuslog,
|
|
re.MULTILINE)
|
|
|
|
# See notification-daemon.py for a stand-alone version.
|
|
#
|
|
# Embedded here to avoid issues with setting up the environment
|
|
# in such a way that the stand-alone version can be run
|
|
# properly.
|
|
class Notifications (dbus.service.Object):
|
|
'''fake org.freedesktop.Notifications implementation,'''
|
|
'''used when there is none already registered on the session bus'''
|
|
|
|
@dbus.service.method(dbus_interface='org.freedesktop.Notifications', in_signature='', out_signature='ssss')
|
|
def GetServerInformation(self):
|
|
return ('test-dbus', 'SyncEvolution', '0.1', '1.1')
|
|
|
|
@dbus.service.method(dbus_interface='org.freedesktop.Notifications', in_signature='', out_signature='as')
|
|
def GetCapabilities(self):
|
|
return ['actions', 'body', 'body-hyperlinks', 'body-markup', 'icon-static', 'sound']
|
|
|
|
@dbus.service.method(dbus_interface='org.freedesktop.Notifications', in_signature='susssasa{sv}i', out_signature='u')
|
|
def Notify(self, app, replaces, icon, summary, body, actions, hints, expire):
|
|
return random.randint(1,100)
|
|
|
|
# fork before connecting to the D-Bus daemon
|
|
child = os.fork()
|
|
if child == 0:
|
|
bus = dbus.SessionBus()
|
|
loop = gobject.MainLoop()
|
|
name = dbus.service.BusName("org.freedesktop.Notifications", bus)
|
|
# start dummy notification daemon, if possible;
|
|
# if it fails, ignore (probably already one running)
|
|
notifications = Notifications(bus, "/org/freedesktop/Notifications")
|
|
loop.run()
|
|
sys.exit(0)
|
|
|
|
# testing continues in parent process
|
|
atexit.register(os.kill, child, 9)
|
|
bus = dbus.SessionBus()
|
|
loop = gobject.MainLoop()
|
|
|
|
# Override the default Connection.send_message_with_reply() with a
|
|
# version that uses a very large timeout. That saves us the trouble of
|
|
# putting a timeout=<large value> whenever we call a D-Bus method. We
|
|
# don't want the default timeout because we may be debugging
|
|
# interactively, which can slow down call processing longer than the
|
|
# default timeout.
|
|
#
|
|
# See http://dbus.freedesktop.org/doc/dbus-python/api/dbus.connection-pysrc.html#Connection.call_async
|
|
#
|
|
real_send_message_with_reply = bus.send_message_with_reply
|
|
real_send_message_with_reply_and_block = bus.send_message_with_reply_and_block
|
|
def my_send_message_with_reply(msg, reply_handler, timeout_s=-1, require_main_loop=False):
|
|
if timeout_s == -1:
|
|
timeout_s = 100000
|
|
return real_send_message_with_reply(msg, reply_handler, timeout_s, require_main_loop=require_main_loop)
|
|
bus.send_message_with_reply = my_send_message_with_reply
|
|
def my_send_message_with_reply_and_block(msg, timeout_s):
|
|
if timeout_s == -1:
|
|
timeout_s = 100000
|
|
return real_send_message_with_reply_and_block(msg, timeout_s)
|
|
bus.send_message_with_reply_and_block = my_send_message_with_reply_and_block
|
|
|
|
# log to .dbus.log of a test and to stdout, if running a debugger
|
|
class Logging(dbus.service.Object):
|
|
def __init__(self):
|
|
dbus.service.Object.__init__(self, bus, '/test/dbus/py')
|
|
self.start = time.time()
|
|
|
|
@dbus.service.signal(dbus_interface='t.d.p',
|
|
signature='s')
|
|
def log2(self, str):
|
|
if debugger or os.environ.get("TEST_DBUS_VERBOSE", False):
|
|
print str
|
|
pass
|
|
|
|
def log(self, str):
|
|
now = time.time()
|
|
self.log2(('%.3fs: ' % (now - self.start)) + str)
|
|
|
|
def printf(self, format, *args):
|
|
self.log(format % args)
|
|
logging = Logging()
|
|
|
|
class NullLogging:
|
|
def log(self, str):
|
|
pass
|
|
def printf(self, format, *args):
|
|
pass
|
|
|
|
# Bluez default adapter
|
|
bt_adaptor = "/org/bluez/1036/hci0"
|
|
|
|
# handles D-Bus messages for '/' object in both net.connman and
|
|
# org.bluez; must be the same object because D-Bus cannot switch based
|
|
# on the bus name (not necessarily included in message)
|
|
class RootObject (dbus.service.Object):
|
|
# ConnMan state
|
|
state = "online"
|
|
getPropertiesCalled = False
|
|
waitingForGetProperties = False
|
|
|
|
def __init__(self):
|
|
self.bluez_name = dbus.service.BusName('org.bluez', bus)
|
|
self.conn_name = dbus.service.BusName("net.connman", bus)
|
|
dbus.service.Object.__init__(self, bus, '/')
|
|
|
|
@dbus.service.method(dbus_interface='org.bluez.Manager', in_signature='', out_signature='o')
|
|
def DefaultAdapter(self):
|
|
return bt_adaptor
|
|
|
|
@dbus.service.signal(dbus_interface='org.bluez.Manager', signature='o')
|
|
def DefaultAdapterChanged(self, obj):
|
|
return bt_adaptor
|
|
|
|
@dbus.service.method(dbus_interface='net.connman.Manager', in_signature='', out_signature='a{sv}')
|
|
def GetProperties(self):
|
|
# notify TestDBusServerPresence.setUp()?
|
|
if self.waitingForGetProperties:
|
|
loop.quit()
|
|
self.getPropertiesCalled = True
|
|
return { "State" : self.state }
|
|
|
|
@dbus.service.signal(dbus_interface='net.connman.Manager', signature='sv')
|
|
def PropertyChanged(self, key, value):
|
|
pass
|
|
|
|
def setState(self, state):
|
|
if self.state != state:
|
|
self.state = state
|
|
self.PropertyChanged("State", state)
|
|
# race condition: it happened that method calls
|
|
# reached syncevo-dbus-server before the state change,
|
|
# thus breaking the test
|
|
time.sleep(1)
|
|
|
|
def reset(self):
|
|
self.state = "online"
|
|
self.getPropertiesCalled = False
|
|
self.waitingForGetProperties = False
|
|
|
|
root = RootObject()
|
|
|
|
def property(key, value):
|
|
"""Function decorator which sets an arbitrary property of a test.
|
|
Use like this:
|
|
@property("foo", "bar")
|
|
def testMyTest:
|
|
...
|
|
|
|
print self.getTestProperty("foo", "default")
|
|
"""
|
|
def __setProperty(func):
|
|
if not "properties" in dir(func):
|
|
func.properties = {}
|
|
func.properties[key] = value
|
|
return func
|
|
return __setProperty
|
|
|
|
def timeout(seconds):
|
|
"""Function decorator which sets a non-default timeout for a test.
|
|
The default timeout, enforced by DBusTest.runTest(), are 40 seconds.
|
|
Use like this:
|
|
@timeout(60)
|
|
def testMyTest:
|
|
...
|
|
"""
|
|
return property("timeout", seconds)
|
|
|
|
class Timeout:
|
|
"""Implements global time-delayed callbacks."""
|
|
alarms = []
|
|
next_alarm = None
|
|
previous_handler = None
|
|
debugTimeout = False
|
|
|
|
@classmethod
|
|
def addTimeout(cls, delay_seconds, callback, use_glib=True):
|
|
"""Call function after a certain delay, specified in seconds.
|
|
If possible and use_glib=True, then it will only fire inside
|
|
glib event loop. Otherwise it uses signals. When signals are
|
|
used it is a bit uncertain what kind of Python code can
|
|
be executed. It was observed that trying to append to
|
|
DBusUtil.quit_events before calling loop.quit() caused
|
|
a KeyboardInterrupt"""
|
|
if glib and use_glib:
|
|
return glib.timeout_add(delay_seconds * 1000, callback)
|
|
else:
|
|
now = time.time()
|
|
if cls.debugTimeout:
|
|
print "addTimeout", now, delay_seconds, callback, use_glib
|
|
timeout = (now + delay_seconds, callback)
|
|
heapq.heappush(cls.alarms, timeout)
|
|
cls.__check_alarms()
|
|
return timeout
|
|
|
|
@classmethod
|
|
def removeTimeout(cls, timeout):
|
|
"""Remove a timeout returned by a previous addTimeout call.
|
|
None and timeouts which have already fired are acceptable."""
|
|
if timeout == None:
|
|
pass
|
|
elif isinstance(timeout, tuple):
|
|
# non-glib case
|
|
try:
|
|
cls.alarms.remove(timeout)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
heapq.heapify(cls.alarms)
|
|
cls.__check_alarms()
|
|
else:
|
|
# glib timeout
|
|
glib.source_remove(timeout)
|
|
|
|
@classmethod
|
|
def __handler(cls, signum, stack):
|
|
"""next_alarm has fired, check for expired timeouts and reinstall"""
|
|
if cls.debugTimeout:
|
|
print "fired", time.time()
|
|
cls.next_alarm = None
|
|
cls.__check_alarms()
|
|
|
|
@classmethod
|
|
def __check_alarms(cls):
|
|
now = time.time()
|
|
while cls.alarms and cls.alarms[0][0] <= now:
|
|
timeout = heapq.heappop(cls.alarms)
|
|
if cls.debugTimeout:
|
|
print "invoking", timeout
|
|
timeout[1]()
|
|
|
|
if cls.alarms:
|
|
if not cls.next_alarm or \
|
|
cls.next_alarm > cls.alarms[0][0]:
|
|
if cls.previous_handler == None:
|
|
cls.previous_handler = signal.signal(signal.SIGALRM, cls.__handler)
|
|
cls.next_alarm = cls.alarms[0][0]
|
|
delay = int(cls.next_alarm - now + 0.5)
|
|
if not delay:
|
|
delay = 1
|
|
if cls.debugTimeout:
|
|
print "next alarm", cls.next_alarm, delay
|
|
signal.alarm(delay)
|
|
elif cls.next_alarm:
|
|
if cls.debugTimeout:
|
|
print "disarming alarm"
|
|
signal.alarm(0)
|
|
cls.next_alarm = None
|
|
|
|
# commented out because running it takes time
|
|
#class TestTimeout(unittest.TestCase):
|
|
class TimeoutTest:
|
|
"""unit test for Timeout mechanism"""
|
|
|
|
def testOneTimeout(self):
|
|
"""testOneTimeout - OneTimeout"""
|
|
self.called = False
|
|
start = time.time()
|
|
def callback():
|
|
self.called = True
|
|
Timeout.addTimeout(2, callback, use_glib=False)
|
|
time.sleep(10)
|
|
end = time.time()
|
|
self.assertTrue(self.called)
|
|
self.assertFalse(end - start < 2)
|
|
self.assertFalse(end - start >= 3)
|
|
|
|
def testEmptyTimeout(self):
|
|
"""testEmptyTimeout - EmptyTimeout"""
|
|
self.called = False
|
|
start = time.time()
|
|
def callback():
|
|
self.called = True
|
|
Timeout.addTimeout(0, callback, use_glib=False)
|
|
if not self.called:
|
|
time.sleep(10)
|
|
end = time.time()
|
|
self.assertTrue(self.called)
|
|
self.assertFalse(end - start < 0)
|
|
self.assertFalse(end - start >= 1)
|
|
|
|
def testTwoTimeouts(self):
|
|
"""testTwoTimeouts - TwoTimeouts"""
|
|
self.called = False
|
|
start = time.time()
|
|
def callback():
|
|
self.called = True
|
|
Timeout.addTimeout(2, callback, use_glib=False)
|
|
Timeout.addTimeout(5, callback, use_glib=False)
|
|
time.sleep(10)
|
|
end = time.time()
|
|
self.assertTrue(self.called)
|
|
self.assertFalse(end - start < 2)
|
|
self.assertFalse(end - start >= 3)
|
|
time.sleep(10)
|
|
end = time.time()
|
|
self.assertTrue(self.called)
|
|
self.assertFalse(end - start < 5)
|
|
self.assertFalse(end - start >= 6)
|
|
|
|
def testTwoReversedTimeouts(self):
|
|
"""testTwoReversedTimeouts - TwoReversedTimeouts"""
|
|
self.called = False
|
|
start = time.time()
|
|
def callback():
|
|
self.called = True
|
|
Timeout.addTimeout(5, callback, use_glib=False)
|
|
Timeout.addTimeout(2, callback, use_glib=False)
|
|
time.sleep(10)
|
|
end = time.time()
|
|
self.assertTrue(self.called)
|
|
self.assertFalse(end - start < 2)
|
|
self.assertFalse(end - start >= 3)
|
|
time.sleep(10)
|
|
end = time.time()
|
|
self.assertTrue(self.called)
|
|
self.assertFalse(end - start < 5)
|
|
self.assertFalse(end - start >= 6)
|
|
|
|
def TryKill(pid, signal):
|
|
try:
|
|
os.kill(pid, signal)
|
|
return True
|
|
except OSError, ex:
|
|
# might have quit in the meantime, deal with the race
|
|
# condition
|
|
if ex.errno != 3:
|
|
raise ex
|
|
return False
|
|
|
|
def ShutdownSubprocess(popen, timeout):
|
|
start = time.time()
|
|
if popen.poll() == None:
|
|
# kill process and process group, in case that process has
|
|
# forked children (valgrindcheck.sh + syncevo-dbus-server
|
|
# case or syncevo-dbus-server + local sync)
|
|
TryKill(popen.pid, signal.SIGTERM)
|
|
TryKill(-popen.pid, signal.SIGTERM)
|
|
while popen.poll() == None and start + timeout >= time.time():
|
|
time.sleep(0.01)
|
|
if popen.poll() == None:
|
|
TryKill(popen.pid, signal.SIGKILL)
|
|
TryKill(-popen.pid, signal.SIGKILL)
|
|
while popen.poll() == None and start + timeout + 1 >= time.time():
|
|
time.sleep(0.01)
|
|
return False
|
|
else:
|
|
# there shouldn't be any processes in the process group left now
|
|
# because the parent process has quit normally, but make sure anyway
|
|
TryKill(-popen.pid, signal.SIGKILL)
|
|
return True
|
|
|
|
class DBusUtil(Timeout):
|
|
"""Contains the common run() method for all D-Bus test suites
|
|
and some utility functions."""
|
|
|
|
# Use class variables because that way it is ensured that there is
|
|
# only one set of them. Previously instance variables were used,
|
|
# which had the effect that D-Bus signal handlers from test A
|
|
# wrote into variables which weren't the ones used by test B.
|
|
# Unfortunately it is impossible to remove handlers when
|
|
# completing test A.
|
|
events = []
|
|
quit_events = []
|
|
reply = None
|
|
pserver = None
|
|
pmonitor = None
|
|
storedenv = {}
|
|
|
|
def getTestProperty(self, key, default):
|
|
"""retrieve values set with @property()"""
|
|
# Assume that IDs are composed of class name + dot + function name.
|
|
# Directly acccessing self._testMethodName would bypass the public
|
|
# Unittest API.
|
|
testMethodName = self.id().split('.')[-1]
|
|
testMethod = getattr(self, testMethodName)
|
|
properties = getattr(testMethod, "properties", {})
|
|
return properties.get(key, default)
|
|
|
|
def runTest(self, result, own_xdg=True, serverArgs=[], own_home=False, defTimeout=40):
|
|
"""Starts the D-Bus server and dbus-monitor before the test
|
|
itself. After the test run, the output of these two commands
|
|
are added to the test's failure, if any. Otherwise the output
|
|
is ignored. A non-zero return code of the D-Bus server is
|
|
logged as separate failure.
|
|
|
|
The D-Bus server must print at least one line of output
|
|
before the test is allowed to start.
|
|
|
|
The commands are run with XDG_DATA_HOME, XDG_CONFIG_HOME,
|
|
XDG_CACHE_HOME pointing towards local dirs
|
|
test-dbus/[data|config|cache] which are removed before each
|
|
test."""
|
|
|
|
DBusUtil.events = []
|
|
DBusUtil.quit_events = []
|
|
DBusUtil.reply = None
|
|
self.pserverpid = None
|
|
self.additional_logs = []
|
|
|
|
# allow arbitrarily long diffs in Python unittest
|
|
self.maxDiff = None
|
|
|
|
self.killChildren(0)
|
|
# collect zombies
|
|
try:
|
|
while os.waitpid(-1, os.WNOHANG)[0]:
|
|
pass
|
|
except OSError, ex:
|
|
if ex.errno != errno.ECHILD:
|
|
raise ex
|
|
|
|
"""own_xdg is saved in self for we use this flag to check whether
|
|
to copy the reference directory tree."""
|
|
self.own_xdg = own_xdg
|
|
env = copy.deepcopy(os.environ)
|
|
if own_xdg or own_home:
|
|
shutil.rmtree(xdg_root, True)
|
|
if own_xdg:
|
|
env["XDG_DATA_HOME"] = os.path.abspath(os.path.join(xdg_root, "data"))
|
|
env["XDG_CONFIG_HOME"] = os.path.abspath(os.path.join(xdg_root, "config"))
|
|
env["XDG_CACHE_HOME"] = os.path.abspath(os.path.join(xdg_root, "cache"))
|
|
if own_home:
|
|
env["HOME"] = os.path.abspath(xdg_root)
|
|
|
|
# populate xdg_root
|
|
snapshot = self.getTestProperty("snapshot", None)
|
|
if snapshot:
|
|
self.setUpFiles(snapshot)
|
|
|
|
# set additional environment variables for the test run,
|
|
# as defined by @property("ENV", "foo=bar x=y")
|
|
for assignment in self.getTestProperty("ENV", "").split():
|
|
var, value = assignment.split("=", 1)
|
|
env[var] = value
|
|
|
|
# always print all debug output directly (no output redirection),
|
|
# and increase log level
|
|
if self.getTestProperty("debug", True):
|
|
env["SYNCEVOLUTION_DEBUG"] = "1"
|
|
|
|
self.storedenv = env
|
|
|
|
# Can be set by a test to run additional tests on the content
|
|
# of the D-Bus log. May be set before calling runTest() or
|
|
# in the test method itself.
|
|
# self.runTestDBusCheck = lambda test, log: test.assertNotIn('ERROR', log)
|
|
#
|
|
# And for stdout.
|
|
# self.runTestOutputCheck = None
|
|
|
|
# Compress dbus log with gzip. Warning, can lead to truncated logs
|
|
# when gzip doesn't flush everything in time.
|
|
useGZip = os.environ.get("TEST_DBUS_GZIP", False)
|
|
|
|
# testAutoSyncFailure (__main__.TestSessionAPIsDummy) => testAutoSyncFailure_TestSessionAPIsDummy
|
|
self.testname = str(self).replace(" ", "_").replace("__main__.", "").replace("(", "").replace(")", "")
|
|
dbuslog = self.testname + ".dbus.log"
|
|
if useGZip:
|
|
dbuslog = dbuslog + ".gz"
|
|
syncevolog = self.testname + ".syncevo.log"
|
|
|
|
self.pmonitor = subprocess.Popen(useGZip and ['sh', '-c', 'dbus-monitor | gzip'] or ['dbus-monitor'],
|
|
stdout=open(dbuslog, "w"),
|
|
stderr=subprocess.STDOUT)
|
|
if usingDLT:
|
|
dltlog = self.testname + ".dlt.log"
|
|
# dlt-receive buffers output and doesn't flush when killed.
|
|
# Trick it into writing each line immediately by pretending that
|
|
# it runs interactively. 'script' had side-effects on the calling
|
|
# terminal, 'unbuffer' (from expect-dev on Debian) worked better.
|
|
self.pdlt = subprocess.Popen(# ['script', '-q', '-c', 'dlt-receive -a localhost', '/dev/null'],
|
|
'unbuffer dlt-receive -a localhost'.split(),
|
|
stdout=open(dltlog, "w"),
|
|
stderr=subprocess.STDOUT)
|
|
else:
|
|
self.pdlt = None
|
|
|
|
# Sanity check: must not have running syncevo-dbus-server.
|
|
# Perhaps the D-Bus daemon has not noticed that the previous owner
|
|
# is gone. Wait a bit.
|
|
start = time.time()
|
|
startServer = True
|
|
while bus.name_has_owner('org.syncevolution'):
|
|
if time.time() - start > 60:
|
|
startServer = False
|
|
break
|
|
logging.printf('There is a running syncevo-dbus-server?!')
|
|
time.sleep(1)
|
|
|
|
if not startServer:
|
|
logging.printf('Not starting syncevo-dbus-server, org.syncevolution is not available.')
|
|
elif debugger:
|
|
print "\n%s: %s\n" % (self.id(), self.shortDescription())
|
|
if env.get("HOME") != os.environ.get("HOME") and \
|
|
os.path.exists(os.path.join(os.environ.get("HOME"), ".gdbinit")):
|
|
gdbinit = ['-x', os.path.join(os.environ.get("HOME"), ".gdbinit")]
|
|
else:
|
|
gdbinit = []
|
|
DBusUtil.pserver = subprocess.Popen([debugger] + gdbinit + ['--args'] + server,
|
|
env=env)
|
|
else:
|
|
if os.environ.get("SYNCEVOLUTION_DEBUG", None):
|
|
logfile = None
|
|
else:
|
|
logfile = open(syncevolog, "w")
|
|
prefix = os.environ.get("TEST_DBUS_PREFIX", "")
|
|
args = []
|
|
if prefix:
|
|
args.append(prefix)
|
|
args.extend(server)
|
|
args.extend(serverArgs)
|
|
if logfile != None:
|
|
logfile.write("env:\n%s\n\nargs:\n%s\n\n" % (env, args))
|
|
logfile.flush()
|
|
size = os.path.getsize(syncevolog)
|
|
DBusUtil.pserver = subprocess.Popen(args,
|
|
preexec_fn=lambda: os.setpgid(0, 0),
|
|
env=env,
|
|
stdout=logfile,
|
|
stderr=subprocess.STDOUT)
|
|
|
|
# Don't use D-Bus auto-activation. Instead wait for our process to show up.
|
|
while self.isServerRunning() and not bus.name_has_owner('org.syncevolution'):
|
|
time.sleep(1)
|
|
# In addition, ensure that it is fully running by calling one method.
|
|
if self.isServerRunning():
|
|
dbus.Interface(bus.get_object('org.syncevolution',
|
|
'/org/syncevolution/Server'),
|
|
'org.syncevolution.Server').GetVersions()
|
|
|
|
# pserver.pid is not necessarily the pid of syncevo-dbus-server.
|
|
# It might be the child of the pserver process.
|
|
try:
|
|
self.pserverpid = self.serverPid()
|
|
self.running = True
|
|
except:
|
|
result.errors.append((self,
|
|
"server startup failed: %s\nchildren: %s" % (traceback.format_exc(), self.getChildren())))
|
|
self.running = False
|
|
|
|
numerrors = len(result.errors)
|
|
numfailures = len(result.failures)
|
|
if debugger:
|
|
print "\nrunning\n"
|
|
|
|
# Find out what test function we run and look into
|
|
# the function definition to see whether it comes
|
|
# with a non-default timeout, otherwise use a 40 second
|
|
# timeout.
|
|
timeout = self.getTestProperty("timeout", defTimeout)
|
|
timeout_handle = None
|
|
if timeout and not debugger and not os.environ.get("SYNCEVOLUTION_LOCAL_CHILD_DELAY", None):
|
|
logging.printf('killing test in %d seconds' % timeout)
|
|
def timedout():
|
|
error = "%s timed out after %d seconds, current quit events: %s" % (self.id(), timeout, self.quit_events)
|
|
logging.log('%s\n%s' % (error, ''.join(traceback.format_stack())))
|
|
raise Exception(error)
|
|
timeout_handle = self.addTimeout(timeout, timedout, use_glib=False)
|
|
else:
|
|
logging.printf('killing test disabled')
|
|
try:
|
|
if self.running:
|
|
unittest.TestCase.run(self, result)
|
|
except KeyboardInterrupt, ex:
|
|
# somehow this happens when timedout() above raises the exception
|
|
# while inside glib main loop
|
|
result.errors.append((self,
|
|
"interrupted by timeout (%d seconds, current quit events: %s) or CTRL-C or Python signal handler problem, exception is: %s" % (timeout, self.quit_events, traceback.format_exc())))
|
|
self.running = False
|
|
self.removeTimeout(timeout_handle)
|
|
if debugger:
|
|
# Print result of this test run.
|
|
for test, trace in result.errors[numerrors:] + result.failures[numfailures:]:
|
|
print trace
|
|
print "\nDone, quit gdb now to proceed.\nSee %s for D-Bus messages.\n" % dbuslog
|
|
hasfailed = numerrors + numfailures != len(result.errors) + len(result.failures)
|
|
|
|
if debugger:
|
|
# allow debugger to run as long as it is needed
|
|
DBusUtil.pserver.communicate()
|
|
|
|
# Mark the end of the test in the dbus log. Relevant for
|
|
# checking the dbus log, because killing processes may distort
|
|
# the result (for example, in testAutoSyncNoNetworkManager, some sync sessions
|
|
# may still be running now and will cause unexpected notifications to be emitted
|
|
# when we kill syncevo-dbus-helper).
|
|
eotest = '---------------- end of test --------------------'
|
|
logging.log(eotest)
|
|
|
|
# Find and kill all sub-processes except for dbus-monitor:
|
|
# first try SIGTERM, then if they don't respond, SIGKILL. The
|
|
# latter is an error. How much time we allow them between
|
|
# SIGTERM and SIGTERM depends on how much work still needs to
|
|
# be done after being asked to quit. valgrind leak checking
|
|
# can take a while.
|
|
unresponsive = self.killChildren(usingValgrind() and 300 or 20)
|
|
if unresponsive:
|
|
error = "/".join(unresponsive) + " had to be killed with SIGKILL"
|
|
print " ", error
|
|
result.errors.append((self, error))
|
|
|
|
if not startServer:
|
|
serverout = '<not started>'
|
|
elif debugger:
|
|
serverout = '<see console>'
|
|
else:
|
|
serverout = open(syncevolog).read()
|
|
if DBusUtil.pserver is not None and DBusUtil.pserver.returncode != -15:
|
|
hasfailed = True
|
|
if hasfailed and not debugger:
|
|
# give D-Bus time to settle down
|
|
time.sleep(1)
|
|
if not ShutdownSubprocess(self.pmonitor, 5):
|
|
print " dbus-monitor had to be killed with SIGKILL"
|
|
result.errors.append((self,
|
|
"dbus-monitor had to be killed with SIGKILL"))
|
|
if self.pdlt and not ShutdownSubprocess(self.pdlt, 5):
|
|
print " dlt-receive had to be killed with SIGKILL"
|
|
result.errors.append((self,
|
|
"dlt-receive had to be killed with SIGKILL"))
|
|
|
|
# If reading the dbus-monitor output, then read it completely.
|
|
# The runTestDBusCheck callbacks expect that and it is confusing
|
|
# when the report printed as part of a test failure is incomplete.
|
|
if debugger:
|
|
monitorout = '<see %s>' % dbuslog
|
|
elif useGZip:
|
|
monitorout = dbuslog + ':\n' + gzip.GzipFile(dbuslog).read()
|
|
else:
|
|
monitorout = dbuslog + ':\n' + open(dbuslog).read()
|
|
report = "\n\nD-Bus traffic:\n%s\n\nserver output:\n%s\n" % \
|
|
(monitorout, serverout)
|
|
|
|
# Also include any test-specific log files (like syncevo-http-server.log, see TestHTTP).
|
|
for logfile in self.additional_logs:
|
|
try:
|
|
report = report + ('\n\n%s:\n' % logfile) + open(logfile).read()
|
|
except:
|
|
pass
|
|
|
|
if usingDLT and not debugger:
|
|
dltout = dltlog + ':\n' + open(dltlog).read()
|
|
runTestDBusCheck = getattr(self, 'runTestDBusCheck', None)
|
|
if runTestDBusCheck:
|
|
try:
|
|
# Only pass the output from the test itself to the check.
|
|
# The rest is only relevant for debugging.
|
|
runTestDBusCheck(self, monitorout.split(eotest)[0])
|
|
except:
|
|
# only append report if not part of some other error below
|
|
result.errors.append((self,
|
|
"D-Bus log failed check: %s\n%s" % (sys.exc_info()[1], (not hasfailed and report) or "")))
|
|
runTestOutputCheck = getattr(self, 'runTestOutputCheck', None)
|
|
if runTestOutputCheck:
|
|
try:
|
|
runTestOutputCheck(self, serverout)
|
|
except:
|
|
# only append report if not part of some other error below
|
|
result.errors.append((self,
|
|
"server stdout failed check: %s\n%s" % (sys.exc_info()[1], (not hasfailed and report) or "")))
|
|
runTestDLTCheck = getattr(self, 'runTestDLTCheck', None)
|
|
if runTestDLTCheck:
|
|
try:
|
|
runTestDLTCheck(self, dltout)
|
|
except:
|
|
# only append report if not part of some other error below
|
|
result.errors.append((self,
|
|
"dlt-receive output failed check: %s\n%s" % (sys.exc_info()[1], (not hasfailed and report) or "")))
|
|
# detect the expected "killed by signal TERM" both when
|
|
# running syncevo-dbus-server directly (negative value) and
|
|
# when valgrindcheck.sh returns the error code 128 + 15 = 143
|
|
if DBusUtil.pserver and \
|
|
DBusUtil.pserver.returncode and \
|
|
DBusUtil.pserver.returncode != 128 + 15 and \
|
|
DBusUtil.pserver.returncode != -15:
|
|
# create a new failure specifically for the server
|
|
result.errors.append((self,
|
|
"server terminated with error code %d%s" % (DBusUtil.pserver.returncode, report)))
|
|
elif numerrors != len(result.errors):
|
|
# append report to last error
|
|
result.errors[-1] = (result.errors[-1][0], result.errors[-1][1] + report)
|
|
elif numfailures != len(result.failures):
|
|
# same for failure
|
|
result.failures[-1] = (result.failures[-1][0], result.failures[-1][1] + report)
|
|
|
|
def loopIteration(self, message, may_block=True):
|
|
'''Trigger glib, allow it to wait once (optional).'''
|
|
# Calling this is necessary, Python uses it to re-raise a
|
|
# timeout exception that was originally raised inside the C
|
|
# glib code. If None, the caller must do it.
|
|
if message:
|
|
logging.log(message)
|
|
loop.get_context().iteration(may_block)
|
|
|
|
def runUntil(self, state, check, until, may_block=False):
|
|
'''Loop until 'check' throws an exception or 'until' returns True.
|
|
Use check=lambda: (expr1, expr2, ...) when more than one check is needed.
|
|
'''
|
|
message = 'waiting for ' + state
|
|
if may_block:
|
|
# Allow process to get stuck for a while.
|
|
# Return from the loop every half second to
|
|
# check status.
|
|
logging.log(message)
|
|
t = glib.Timeout(500)
|
|
try:
|
|
t.set_callback(lambda: loop.quit() or True)
|
|
t.attach(loop.get_context())
|
|
while True:
|
|
check()
|
|
if until():
|
|
break
|
|
loop.run()
|
|
finally:
|
|
t.destroy()
|
|
else:
|
|
progress = True
|
|
while True:
|
|
# Only print it if something changed, not every 0.1 second.
|
|
if progress:
|
|
logging.log(message)
|
|
check()
|
|
if until():
|
|
break
|
|
if not progress and not may_block:
|
|
# Don't block forever if nothing is to be processed.
|
|
# To keep the load down, sleep for a short period.
|
|
time.sleep(0.1)
|
|
progress = self.loopIteration(None, may_block=False)
|
|
logging.log('succeeded waiting for ' + state)
|
|
|
|
def isServerRunning(self):
|
|
"""True while the syncevo-dbus-server executable is still running"""
|
|
return DBusUtil.pserver and DBusUtil.pserver.poll() == None
|
|
|
|
def getChildren(self):
|
|
'''Find all children of the current process (ppid = out pid,
|
|
in our process group, or in process group of known
|
|
children). Return mapping from pid to (name, cmdline),
|
|
where cmdline has spaces between parameters.'''
|
|
|
|
# Any of the known children might have its own process group.
|
|
# syncevo-dbus-server definitely does (we create it like that);
|
|
# that allows us to find its forked processes.
|
|
pgids = []
|
|
if self.pmonitor:
|
|
pgids.append(self.pmonitor.pid)
|
|
if self.pserverpid:
|
|
pgids.append(self.pserverpid)
|
|
|
|
# Maps from pid to name, process ID, process group ID.
|
|
procs = {}
|
|
statre = re.compile(r'^\d+ \((?P<name>.*?)\) \S (?P<ppid>\d+) (?P<pgid>\d+)')
|
|
for process in os.listdir('/proc'):
|
|
try:
|
|
pid = int(process)
|
|
stat = open('/proc/%d/stat' % pid).read()
|
|
m = statre.search(stat)
|
|
if m:
|
|
procs[pid] = m.groupdict()
|
|
procs[pid]['cmdline'] = open('/proc/%d/cmdline' % pid, 'r').read().replace('\0', ' ')
|
|
for i in ('ppid', 'pgid'):
|
|
procs[pid][i] = int(procs[pid][i])
|
|
except:
|
|
# ignore all errors
|
|
pass
|
|
logging.printf("found processes: %s", procs)
|
|
# Now determine direct or indirect children.
|
|
children = {}
|
|
mypid = os.getpid()
|
|
def isChild(pid):
|
|
if pid == mypid:
|
|
return False
|
|
if not procs.get(pid, None):
|
|
return False
|
|
return procs[pid]['ppid'] == mypid or \
|
|
procs[pid]['pgid'] in pgids or \
|
|
isChild(procs[pid]['ppid'])
|
|
for pid, info in procs.iteritems():
|
|
if isChild(pid):
|
|
children[pid] = (info['name'], info['cmdline'])
|
|
# Exclude dbus-monitor and forked test-dbus.py, they are handled separately.
|
|
if self.pmonitor:
|
|
del children[self.pmonitor.pid]
|
|
del children[child]
|
|
logging.printf("found children: %s", children)
|
|
return children
|
|
|
|
def killPending(self, pending):
|
|
'''Ensure that all processes listed with their pid are not running.'''
|
|
while True:
|
|
again = []
|
|
for pid in pending:
|
|
logging.printf("sending SIGKILL to pending proccess %d" % pid)
|
|
if not TryKill(pid, signal.SIGKILL):
|
|
logging.printf("pending process %d is gone" % pid)
|
|
else:
|
|
again.append(pid)
|
|
if again:
|
|
time.sleep(0.1)
|
|
pending = again
|
|
else:
|
|
break
|
|
|
|
def killChildren(self, delay):
|
|
'''Find all children of the current process and kill them. First send SIGTERM,
|
|
then after a grace period SIGKILL.'''
|
|
children = self.getChildren()
|
|
pending = []
|
|
# First pass with SIGTERM?
|
|
if delay:
|
|
for pid, (name, cmdline) in children.iteritems():
|
|
logging.printf("sending SIGTERM to %d %s = %s", pid, name, cmdline)
|
|
TryKill(pid, signal.SIGTERM)
|
|
start = time.time()
|
|
logging.printf("starting to wait for process termination at %s", time.asctime(time.localtime(start)))
|
|
while delay and start + delay >= time.time():
|
|
# Check if any process has quit, remove from list.
|
|
pids = copy.deepcopy(children)
|
|
def checkKnown(p):
|
|
if (p):
|
|
if p.pid in pids:
|
|
del pids[p.pid]
|
|
if p.poll() != None and p.pid in children:
|
|
logging.printf("known process %d %s has quit at %s",
|
|
p.pid, children[p.pid], time.asctime())
|
|
del children[p.pid]
|
|
checkKnown(self.pserver)
|
|
for pid, name in pids.iteritems():
|
|
try:
|
|
res = os.waitpid(pid, os.WNOHANG)
|
|
if res[0]:
|
|
# got result, remove process
|
|
logging.printf("got status for process %d %s at %s",
|
|
pid, name, time.asctime())
|
|
del children[pid]
|
|
except OSError, ex:
|
|
if ex.errno == errno.ECHILD:
|
|
# Process might have transferred to init as parent:
|
|
#
|
|
# PID PPID TID NLWP CMD COMMAND
|
|
# 20617 1 - 2 [memcheck-amd64-] <defunct> [memcheck-amd64-] <defunct>
|
|
# - - 20617 - - -
|
|
# - - 21012 - - -
|
|
#
|
|
# This was observed for syncevo-dbus-server when running under
|
|
# valgrind despite apparently having terminated normally
|
|
# (valgrindcheck.sh sees the exist status, log output confirms
|
|
# normal shutdown). Not sure why process shutdown did not
|
|
# terminate all threads.
|
|
#
|
|
# Remember to kill the remaining threads with SIGKILL,
|
|
# as a workaround.
|
|
logging.printf("process %d %s gone at %s",
|
|
pid, name, time.asctime())
|
|
pending.append(pid)
|
|
del children[pid]
|
|
else:
|
|
raise ex
|
|
if not children:
|
|
# All children quit normally.
|
|
logging.printf("all process gone at %s",
|
|
time.asctime())
|
|
self.killPending(pending)
|
|
return []
|
|
time.sleep(0.1)
|
|
# Force killing of remaining children. It's still possible
|
|
# that one of them quits before we get around to sending the
|
|
# signal.
|
|
if children:
|
|
logging.printf("starting to kill unresponsive processes at %s: %s", time.asctime(), str(children))
|
|
killed = []
|
|
for pid, name in children.iteritems():
|
|
stacktrace = subprocess.Popen(['gdb',
|
|
'-ex', 'thread apply all bt',
|
|
'-ex', 'detach',
|
|
'-p', '%d' % pid],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
(out, err) = stacktrace.communicate()
|
|
logging.printf("stack backtrace for %d %s: %s", pid, name, out)
|
|
if TryKill(pid, signal.SIGKILL):
|
|
logging.printf("killed %d %s", pid, name)
|
|
killed.append("%d %s" % (pid, name))
|
|
self.killPending(pending)
|
|
return killed
|
|
|
|
def serverExecutableHelper(self, pid):
|
|
self.assertTrue(self.isServerRunning())
|
|
maps = open("/proc/%d/maps" % pid, "r")
|
|
regex = re.compile(r'[0-9a-f]*-[0-9a-f]* r-xp [0-9a-f]* [^ ]* \d* *(.*)\n')
|
|
parentre = re.compile(r'^PPid:\s+(\d+)', re.MULTILINE)
|
|
for line in maps:
|
|
match = regex.match(line)
|
|
if match:
|
|
# must be syncevo-dbus-server
|
|
res = match.group(1)
|
|
if 'syncevo-dbus-server' in res:
|
|
return (res, pid)
|
|
# not found, try children
|
|
for process in os.listdir('/proc'):
|
|
try:
|
|
status = open('/proc/%s/status' % process).read()
|
|
parent = parentre.search(status)
|
|
if parent and int(parent.group(1)) == pid:
|
|
res = self.serverExecutableHelper(int(process))
|
|
if res:
|
|
return res
|
|
except:
|
|
# ignore all errors
|
|
pass
|
|
# no result
|
|
return None
|
|
|
|
def serverExecutable(self):
|
|
"""returns full path of currently running syncevo-dbus-server binary"""
|
|
res = self.serverExecutableHelper(DBusUtil.pserver.pid)
|
|
self.assertTrue(res)
|
|
return res[0]
|
|
|
|
def serverPid(self):
|
|
"""PID of syncevo-dbus-server, None if not running. Works regardless whether it is
|
|
started directly or with a wrapper script like valgrindcheck.sh."""
|
|
self.assertTrue(DBusUtil.pserver)
|
|
res = self.serverExecutableHelper(DBusUtil.pserver.pid)
|
|
self.assertTrue(res)
|
|
return res[1]
|
|
|
|
def setUpServer(self):
|
|
self.server = dbus.Interface(bus.get_object('org.syncevolution',
|
|
'/org/syncevolution/Server'),
|
|
'org.syncevolution.Server')
|
|
|
|
def createSession(self, config, wait, flags=[]):
|
|
"""Return sessionpath and session object for session using 'config'.
|
|
A signal handler calls loop.quit() when this session becomes ready.
|
|
If wait=True, then this call blocks until the session is ready.
|
|
"""
|
|
if flags:
|
|
sessionpath = self.server.StartSessionWithFlags(config, flags)
|
|
else:
|
|
sessionpath = self.server.StartSession(config)
|
|
|
|
def session_ready(object, ready):
|
|
if self.running and ready and object == sessionpath:
|
|
DBusUtil.quit_events.append("session " + object + " ready")
|
|
loop.quit()
|
|
|
|
signal = bus.add_signal_receiver(session_ready,
|
|
'SessionChanged',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
session = dbus.Interface(bus.get_object(self.server.bus_name,
|
|
sessionpath),
|
|
'org.syncevolution.Session')
|
|
status, error, sources = session.GetStatus(utf8_strings=True)
|
|
if wait and status == "queueing":
|
|
# wait for signal
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + sessionpath + " ready"])
|
|
elif DBusUtil.quit_events:
|
|
# signal was processed inside D-Bus call?
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + sessionpath + " ready"])
|
|
if wait:
|
|
# signal no longer needed, remove it because otherwise it
|
|
# might record unexpected "session ready" events
|
|
signal.remove()
|
|
DBusUtil.quit_events = []
|
|
return (sessionpath, session)
|
|
|
|
def setUpSession(self, config, flags=[]):
|
|
"""stores ready session in self.sessionpath and self.session"""
|
|
self.sessionpath, self.session = self.createSession(config, True, flags)
|
|
|
|
def progressChanged(self, *args, **keywords):
|
|
'''subclasses override this method to write specified callbacks for ProgressChanged signals
|
|
It is called by progress signal receivers in setUpListeners'''
|
|
pass
|
|
|
|
def statusChanged(self, *args, **keywords):
|
|
'''subclasses override this method to write specified callbacks for StatusChanged signals
|
|
It is called by status signal receivers in setUpListeners'''
|
|
pass
|
|
|
|
def setUpListeners(self, sessionpath):
|
|
"""records progress and status changes in DBusUtil.events and
|
|
quits the main loop when the session is done"""
|
|
|
|
def progress(*args, **keywords):
|
|
if self.running:
|
|
DBusUtil.events.append(("progress", args, keywords['path']))
|
|
self.progressChanged(*args, **keywords)
|
|
|
|
def status(*args, **keywords):
|
|
if self.running:
|
|
DBusUtil.events.append(("status", args, keywords['path']))
|
|
self.statusChanged(*args, **keywords)
|
|
if args[0] == "done":
|
|
if sessionpath:
|
|
DBusUtil.quit_events.append("session " + sessionpath + " done")
|
|
else:
|
|
DBusUtil.quit_events.append("session done")
|
|
loop.quit()
|
|
|
|
DBusUtil.events = []
|
|
bus.add_signal_receiver(progress,
|
|
'ProgressChanged',
|
|
'org.syncevolution.Session',
|
|
self.server.bus_name,
|
|
sessionpath,
|
|
path_keyword='path',
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
bus.add_signal_receiver(status,
|
|
'StatusChanged',
|
|
'org.syncevolution.Session',
|
|
self.server.bus_name,
|
|
sessionpath,
|
|
path_keyword='path',
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
def setUpConfigListeners(self):
|
|
"""records ConfigChanged signal and records it in DBusUtil.events, then quits the loop"""
|
|
|
|
def config():
|
|
if self.running:
|
|
DBusUtil.events.append("ConfigChanged")
|
|
DBusUtil.quit_events.append("ConfigChanged")
|
|
loop.quit()
|
|
|
|
bus.add_signal_receiver(config,
|
|
'ConfigChanged',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
def setUpConnectionListeners(self, conpath):
|
|
"""records connection signals (abort and reply), quits when
|
|
getting an abort"""
|
|
|
|
def abort():
|
|
if self.running:
|
|
DBusUtil.events.append(("abort",))
|
|
DBusUtil.quit_events.append("connection " + conpath + " aborted")
|
|
loop.quit()
|
|
|
|
def reply(*args):
|
|
if self.running:
|
|
DBusUtil.reply = args
|
|
if args[3]:
|
|
DBusUtil.quit_events.append("connection " + conpath + " got final reply")
|
|
else:
|
|
DBusUtil.quit_events.append("connection " + conpath + " got reply")
|
|
loop.quit()
|
|
|
|
bus.add_signal_receiver(abort,
|
|
'Abort',
|
|
'org.syncevolution.Connection',
|
|
self.server.bus_name,
|
|
conpath,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
bus.add_signal_receiver(reply,
|
|
'Reply',
|
|
'org.syncevolution.Connection',
|
|
self.server.bus_name,
|
|
conpath,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
def collectEvents(self, until='\nstatus: done'):
|
|
'''Normally collection of events stops when any of the 'quit events' are encounted.
|
|
Cmdline tests are different, they stop when the command line tool returns. At that
|
|
time the quit event may or may not have been seen.
|
|
This method continues collecting events until the 'until' string is in the
|
|
pretty-printed events.'''
|
|
while not until in self.prettyPrintEvents():
|
|
loop.get_context().iteration(True)
|
|
|
|
def setUpLocalSyncConfigs(self, childPassword=None, enableCalendar=False, preventSlowSync=None):
|
|
# create file<->file configs
|
|
self.setUpSession("remote@client")
|
|
addressbook = { "sync": "two-way",
|
|
"backend": "file",
|
|
"databaseFormat": "text/vcard",
|
|
"database": "file://" + xdg_root + "/client" }
|
|
calendar = { "sync": "two-way",
|
|
"backend": "file",
|
|
"databaseFormat": "text/calendar",
|
|
"database": "file://" + xdg_root + "/client-calendar" }
|
|
if childPassword:
|
|
addressbook["databaseUser"] = "foo-user"
|
|
addressbook["databasePassword"] = childPassword
|
|
config = {"" : { "loglevel": "4" } }
|
|
if preventSlowSync != None:
|
|
config = {"" : { "preventSlowSync": preventSlowSync and "1" or "0" }}
|
|
config["source/addressbook"] = addressbook
|
|
if enableCalendar:
|
|
config["source/calendar"] = calendar
|
|
self.session.SetConfig(False, False, config)
|
|
self.session.Detach()
|
|
self.setUpSession("server")
|
|
config = {"" : { "loglevel": "4",
|
|
"syncURL": "local://remote@client",
|
|
"RetryDuration": self.getTestProperty("resendDuration", "60"),
|
|
"peerIsClient": "1" },
|
|
"source/addressbook": { "sync": "two-way",
|
|
"uri": "addressbook",
|
|
"backend": "file",
|
|
"databaseFormat": "text/vcard",
|
|
"database": "file://" + xdg_root + "/server" } }
|
|
if enableCalendar:
|
|
config["source/calendar"] = { "sync": "two-way",
|
|
# "uri": "calendar", uri = local source name is the default
|
|
"backend": "file",
|
|
"databaseFormat": "text/calendar",
|
|
"database": "file://" + xdg_root + "/server-calendar" }
|
|
self.session.SetConfig(False, False, config)
|
|
|
|
def setUpFiles(self, snapshot):
|
|
""" Copy reference directory trees from
|
|
test/test-dbus/<snapshot> to own xdg_root (=./test-dbus). When
|
|
used in tests which called runTest() with
|
|
own_xdg=True, then the target directory is known to be empty. Otherwise
|
|
it may contain files from previous tests."""
|
|
# Get the absolute path of the current python file.
|
|
scriptpath = os.path.abspath(os.path.expanduser(os.path.expandvars(sys.argv[0])))
|
|
# reference directory 'test-dbus' is in the same directory as the current python file
|
|
sourcedir = os.path.join(os.path.dirname(scriptpath), 'test-dbus', snapshot)
|
|
""" Directories in test/test-dbus are copied to xdg_root, but
|
|
maybe with different names, mappings are:
|
|
'test/test-dbus/<snapshot>' './test-dbus'"""
|
|
pairs = { 'sync4j' : '.sync4j' ,
|
|
'config' : 'config' ,
|
|
'cache' : 'cache' ,
|
|
'data' : 'data' ,
|
|
'templates' : 'templates'}
|
|
for src, dest in pairs.items():
|
|
sourcepath = os.path.join(sourcedir, src)
|
|
destpath = os.path.join(xdg_root, dest)
|
|
# if source exists and could be accessed, then copy them
|
|
if os.access(sourcepath, os.F_OK):
|
|
# Add to the destination directory. Removing it, if and
|
|
# only if required, was done earlier. Because copytree
|
|
# expects target directories to not exist, we must use
|
|
# our own recursive copy routine.
|
|
def copy(dirname, dirs, files):
|
|
pass
|
|
for dirname, dirs, files in os.walk(sourcepath):
|
|
destdirname = os.path.join(destpath, os.path.relpath(dirname, sourcepath))
|
|
for dir in dirs:
|
|
dest = os.path.join(destdirname, dir)
|
|
if not os.access(dest, os.F_OK):
|
|
logging.printf('creating snapshot directory %s', dest)
|
|
os.makedirs(dest)
|
|
for file in files:
|
|
source = os.path.join(dirname, file)
|
|
dest = os.path.join(destdirname, file)
|
|
logging.printf('copying snapshot file %s -> %s', source, dest)
|
|
shutil.copyfile(source, dest)
|
|
|
|
def prettyPrintEvents(self, events=None):
|
|
'''Format events as lines without full class specifiers, like this:
|
|
status: idle, 0, {}
|
|
'''
|
|
if events == None:
|
|
events = DBusUtil.events
|
|
lines = []
|
|
def prettyPrintArg(arg):
|
|
if isinstance(arg, type(())):
|
|
res = []
|
|
for i in arg:
|
|
res.append(prettyPrintArg(i))
|
|
return '(' + ', '.join(res) + ')'
|
|
elif isinstance(arg, type([])):
|
|
res = []
|
|
for i in arg:
|
|
res.append(prettyPrintArg(i))
|
|
return '[' + ', '.join(res) + ']'
|
|
elif isinstance(arg, type({})):
|
|
res = []
|
|
items = arg.items()
|
|
items.sort()
|
|
for i,e in items:
|
|
res.append('%s: %s' % (prettyPrintArg(i), prettyPrintArg(e)))
|
|
return '{' + ', '.join(res) + '}'
|
|
else:
|
|
return str(arg)
|
|
def prettyPrintArgs(args):
|
|
res = []
|
|
for arg in args:
|
|
res.append(prettyPrintArg(arg))
|
|
return ', '.join(res)
|
|
for event in events:
|
|
self.assertTrue(len(event) >= 1,
|
|
'Unexpected number of entries in event: %s' % str(event))
|
|
if len(event) >= 2:
|
|
lines.append('%s: %s' % (event[0], prettyPrintArgs(event[1])))
|
|
else:
|
|
lines.append(event[0])
|
|
return '\n'.join(lines)
|
|
|
|
def getSyncStatus(self, config):
|
|
cache = self.own_xdg and os.path.join(xdg_root, 'cache', 'syncevolution') or \
|
|
os.path.join(os.environ['HOME'], '.cache', 'syncevolution')
|
|
entries = [x for x in os.listdir(cache) if x.startswith(config + '-')]
|
|
entries.sort()
|
|
self.assertEqual(1, len(entries))
|
|
config = ConfigParser.ConfigParser()
|
|
content = '[fake]\n' + open(os.path.join(cache, entries[0], 'status.ini')).read()
|
|
config.readfp(io.BytesIO(content))
|
|
status = config.getint('fake', 'status')
|
|
error = None
|
|
if config.has_option('fake', 'error'):
|
|
error = config.get('fake', 'error')
|
|
return (status, error)
|
|
|
|
def isSet(self, data):
|
|
return isinstance(data, type([])) or \
|
|
isinstance(data, type(()))
|
|
|
|
def assertSyncStatus(self, config, status, error):
|
|
realStatus, realError = self.getSyncStatus(config)
|
|
if self.isSet(status):
|
|
self.assertTrue(realStatus in status)
|
|
else:
|
|
self.assertEqual(status, realStatus)
|
|
if self.isSet(error):
|
|
self.assertTrue(realError in error)
|
|
else:
|
|
self.assertEqual(error, realError)
|
|
|
|
def doCheckSync(self, expectedError=0, expectedResult=0, reportOptional=False, numReports=1, checkPercent=True):
|
|
# check recorded events in DBusUtil.events, first filter them
|
|
statuses = []
|
|
progresses = []
|
|
# Dict is used to check status order.
|
|
statusPairs = {"": 0, "idle": 1, "running" : 2, "aborting" : 3, "done" : 4}
|
|
for item in DBusUtil.events:
|
|
if item[0] == "status":
|
|
statuses.append(item[1])
|
|
elif item[0] == "progress":
|
|
progresses.append(item[1])
|
|
|
|
# check statuses
|
|
lastStatus = ""
|
|
lastSources = {}
|
|
lastError = 0
|
|
for status, error, sources in statuses:
|
|
# consecutive entries should not be equal
|
|
self.assertNotEqual((lastStatus, lastError, lastSources), (status, error, sources))
|
|
# no error, unless expected
|
|
if expectedError:
|
|
if error:
|
|
self.assertEqual(expectedError, error)
|
|
else:
|
|
self.assertEqual(error, 0)
|
|
# keep order: session status must be unchanged or the next status
|
|
seps = status.split(';')
|
|
lastSeps = lastStatus.split(';')
|
|
self.assertTrue(statusPairs.has_key(seps[0]))
|
|
self.assertTrue(statusPairs[seps[0]] >= statusPairs[lastSeps[0]])
|
|
# check specifiers
|
|
if len(seps) > 1:
|
|
self.assertEqual(seps[1], "waiting")
|
|
for sourcename, value in sources.items():
|
|
# no error
|
|
self.assertEqual(value[2], 0)
|
|
# keep order: source status must also be unchanged or the next status
|
|
if lastSources.has_key(sourcename):
|
|
lastValue = lastSources[sourcename]
|
|
self.assertTrue(statusPairs[value[1]] >= statusPairs[lastValue[1]])
|
|
|
|
lastStatus = status
|
|
lastSources = sources
|
|
lastError = error
|
|
|
|
# check increasing progress percentage
|
|
lastPercent = 0
|
|
for percent, sources in progresses:
|
|
self.assertFalse(percent < lastPercent)
|
|
lastPercent = percent
|
|
|
|
status, error, sources = self.session.GetStatus(utf8_strings=True)
|
|
self.assertEqual(status, "done")
|
|
self.assertEqual(error, expectedError)
|
|
|
|
# now check that report is sane
|
|
reports = self.session.GetReports(0, 100, utf8_strings=True)
|
|
if reportOptional and len(reports) == 0:
|
|
# no report was written
|
|
return None
|
|
self.assertEqual(len(reports), numReports)
|
|
if expectedResult:
|
|
self.assertEqual(int(reports[0]["status"]), expectedResult)
|
|
else:
|
|
self.assertEqual(int(reports[0]["status"]), 200)
|
|
self.assertNotIn("error", reports[0])
|
|
return reports[0]
|
|
|
|
def checkSync(self, *args, **keywords):
|
|
'''augment any assertion in doCheckSync() with text dump of events'''
|
|
events = self.prettyPrintEvents()
|
|
try:
|
|
return self.doCheckSync(*args, **keywords)
|
|
except AssertionError, ex:
|
|
raise self.failureException('Assertion about the following events failed:\n%s\n%s' %
|
|
(events, traceback.format_exc()))
|
|
|
|
def assertEqualDiff(self, expected, res):
|
|
'''Like assertEqual(), but raises an error which contains a
|
|
diff of the two parameters. Useful when they are long strings
|
|
(will be split at newlines automatically) or lists (compared
|
|
as-is). Very similar to Python's 2.7 unittest, but also works
|
|
for older Python releases and allows comparing strings against lists.'''
|
|
def splitlines(str):
|
|
'''split any object which looks like a string == has splitlines'''
|
|
if 'splitlines' in dir(str):
|
|
return str.splitlines(True)
|
|
else:
|
|
return str
|
|
expected = splitlines(expected)
|
|
res = splitlines(res)
|
|
if expected != res:
|
|
diff = ''.join(difflib.Differ().compare(expected, res))
|
|
self.fail('differences between expected and actual text\n\n' + diff)
|
|
|
|
def assertRegexpMatchesCustom(self, text, regex, msg=None):
|
|
if isinstance(regex, str):
|
|
regex = re.compile(regex)
|
|
if not regex.search(text):
|
|
if msg != None:
|
|
self.fail(msg)
|
|
else:
|
|
self.fail('text does not match regex\n\nText:\n%s\n\nRegex:\n%s' % \
|
|
(text, regex.pattern))
|
|
|
|
|
|
def assertInCustom(self, needle, haystack, msg=None):
|
|
if not needle in haystack:
|
|
if msg != None:
|
|
self.fail(msg)
|
|
else:
|
|
self.fail("'" + str(needle) + "' not found in '" + str(haystack) + "'")
|
|
|
|
def assertNotInCustom(self, needle, haystack, msg=None):
|
|
if needle in haystack:
|
|
if msg != None:
|
|
self.fail(msg)
|
|
else:
|
|
self.fail("'" + str(needle) + "' found in '" + str(haystack) + "'")
|
|
|
|
dbusTypeMapping = {
|
|
dbus.Array: list,
|
|
dbus.Boolean: bool,
|
|
dbus.Byte: int,
|
|
dbus.Dictionary: dict,
|
|
dbus.Double: float,
|
|
dbus.Int16: int,
|
|
dbus.Int32: int,
|
|
dbus.Int64: long,
|
|
dbus.ObjectPath: str,
|
|
dbus.Signature: str,
|
|
dbus.String: unicode,
|
|
dbus.Struct: tuple,
|
|
dbus.UInt16: int,
|
|
dbus.UInt32: int,
|
|
dbus.UInt64: long,
|
|
dbus.UTF8String: unicode
|
|
}
|
|
|
|
def stripDBus(self, instance, sortLists):
|
|
base = DBusUtil.dbusTypeMapping.get(type(instance), None)
|
|
if base == dict or isinstance(instance, dict):
|
|
return dict([(self.stripDBus(k, sortLists), self.stripDBus(v, sortLists)) for k, v in instance.iteritems()])
|
|
if base == list or isinstance(instance, list):
|
|
l = [self.stripDBus(v, sortLists) for v in instance]
|
|
if sortLists:
|
|
l.sort()
|
|
return l
|
|
if base == tuple or isinstance(instance, tuple):
|
|
return tuple([self.stripDBus(v, sortLists) for v in instance])
|
|
if base == None:
|
|
return instance
|
|
if base == unicode:
|
|
# try conversion to normal string
|
|
try:
|
|
return str(instance)
|
|
except UnicodeEncodeError:
|
|
pass
|
|
return base(instance)
|
|
|
|
def assertEqualCustom(self, a, b, msg=None, sortLists=False):
|
|
'''Reduce custom dbus types to the corresponding Python type, to simplify the output in the error message.'''
|
|
unittest.TestCase.assertEqual(self, self.stripDBus(a, sortLists), self.stripDBus(b, sortLists), msg)
|
|
|
|
def assertLessCustom(self, a, b, msg=None):
|
|
self.assertTrue(a < b, msg=msg)
|
|
|
|
def assertLessEqualCustom(self, a, b, msg=None):
|
|
self.assertTrue(a <= b, msg=msg)
|
|
|
|
def assertAlmostEqualCustom(self, a, b, places=7, msg=None, delta=None):
|
|
if delta != None:
|
|
self.assertTrue(abs(a - b) < delta, msg=msg)
|
|
else:
|
|
self.assertTrue(round(a-b, places) == 0, msg=msg)
|
|
|
|
# always use our own compare
|
|
assertEqual = assertEqualCustom
|
|
|
|
# reimplement Python 2.7 assertions only in older Python
|
|
if True or not 'assertRegexpMatches' in dir(unittest.TestCase):
|
|
assertRegexpMatches = assertRegexpMatchesCustom
|
|
|
|
if not 'assertIn' in dir(unittest.TestCase):
|
|
assertIn = assertInCustom
|
|
|
|
if not 'assertNotIn' in dir(unittest.TestCase):
|
|
assertNotIn = assertNotInCustom
|
|
|
|
if not 'assertLess' in dir(unittest.TestCase):
|
|
assertLess = assertLessCustom
|
|
# Can't check for delta parameter in assertAlmostEqual. Assume it's missing when assertLess is missing.
|
|
assertAlmostEqual = assertAlmostEqualCustom
|
|
|
|
if not 'assertLessEqual' in dir(unittest.TestCase):
|
|
assertLessEqual = assertLessEqualCustom
|
|
|
|
class TestDBusServer(DBusUtil, unittest.TestCase):
|
|
"""Tests for the read-only Server API."""
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
|
|
def run(self, result):
|
|
self.runTest(result)
|
|
|
|
def testCapabilities(self):
|
|
"""TestDBusServer.testCapabilities - Server.Capabilities()"""
|
|
capabilities = self.server.GetCapabilities()
|
|
capabilities.sort()
|
|
self.assertEqual(capabilities, ['ConfigChanged', 'DatabaseProperties', 'GetConfigName', 'NamedConfig', 'Notifications', 'SessionAttach', 'SessionFlags', 'Version'])
|
|
|
|
def testVersions(self):
|
|
"""TestDBusServer.testVersions - Server.GetVersions()"""
|
|
versions = self.server.GetVersions()
|
|
self.assertNotEqual(versions["version"], "")
|
|
self.assertNotEqual(versions["system"], None)
|
|
self.assertNotEqual(versions["backends"], None)
|
|
|
|
def testGetConfigsEmpty(self):
|
|
"""TestDBusServer.testGetConfigsEmpty - Server.GetConfigsEmpty()"""
|
|
configs = self.server.GetConfigs(False, utf8_strings=True)
|
|
self.assertEqual(configs, [])
|
|
|
|
@property("ENV", "DBUS_TEST_BLUETOOTH=none")
|
|
def testGetConfigsTemplates(self):
|
|
"""TestDBusServer.testGetConfigsTemplates - Server.GetConfigsTemplates()"""
|
|
configs = self.server.GetConfigs(True, utf8_strings=True)
|
|
configs.sort()
|
|
self.assertEqual(configs, ["Funambol",
|
|
"Google",
|
|
"Goosync",
|
|
"Memotoo",
|
|
"Mobical",
|
|
"Oracle",
|
|
"Ovi",
|
|
"ScheduleWorld",
|
|
"SyncEvolution",
|
|
"Synthesis",
|
|
"WebDAV",
|
|
"Yahoo",
|
|
"eGroupware"])
|
|
|
|
def testGetConfigScheduleWorld(self):
|
|
"""TestDBusServer.testGetConfigScheduleWorld - Server.GetConfigScheduleWorld()"""
|
|
config1 = self.server.GetConfig("scheduleworld", True, utf8_strings=True)
|
|
config2 = self.server.GetConfig("ScheduleWorld", True, utf8_strings=True)
|
|
self.assertNotEqual(config1[""]["deviceId"], config2[""]["deviceId"])
|
|
config1[""]["deviceId"] = "foo"
|
|
config2[""]["deviceId"] = "foo"
|
|
self.assertEqual(config1, config2)
|
|
|
|
def testInvalidConfig(self):
|
|
"""TestDBusServer.testInvalidConfig - Server.NoSuchConfig exception"""
|
|
try:
|
|
config1 = self.server.GetConfig("no-such-config", False, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchConfig: No configuration 'no-such-config' found")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
class TestDBusServerStart(DBusUtil, unittest.TestCase):
|
|
def testAutoActivation(self):
|
|
'''TestDBusServerStart.testAutoActivation - check that activating syncevo-dbus-server via D-Bus daemon works'''
|
|
# Create a D-Bus service file for the syncevo-dbus-server that we
|
|
# are testing (= the one in PATH).
|
|
shutil.rmtree(xdg_root, True)
|
|
dirname = os.path.join(xdg_root, "share", "dbus-1", "services")
|
|
os.makedirs(dirname)
|
|
service = open(os.path.join(dirname, "org.syncevolution.service"), "w")
|
|
service.write('''[D-BUS Service]
|
|
Name=org.syncevolution
|
|
Exec=%s
|
|
''' % which('syncevo-dbus-server'))
|
|
service.close()
|
|
|
|
# Now run a private D-Bus session in which dbus-send activates
|
|
# that syncevo-dbus-server. Uses a dbus-session.sh from the
|
|
# same dir as test-dbus.py itself.
|
|
env = copy.deepcopy(os.environ)
|
|
env['XDG_DATA_DIRS'] = env.get('XDG_DATA_DIRS', '') + ':' + os.path.abspath(os.path.join(xdg_root, "share"))
|
|
|
|
# Avoid running EDS and Akonadi. They are not needed for this test
|
|
# and can cause false failures, for example when the daemons
|
|
# return an unexpected error cause, which then would be returned
|
|
# by dbus-session.sh.
|
|
for key in ('DBUS_SESSION_SH_EDS_BASE', 'DBUS_SESSION_SH_AKONADI'):
|
|
if env.has_key(key):
|
|
del env[key]
|
|
|
|
# First run something which just starts the daemon.
|
|
cmd = (os.path.join(os.path.dirname(sys.argv[0]), 'dbus-session.sh'),
|
|
'dbus-send',
|
|
'--print-reply',
|
|
'--dest=org.syncevolution',
|
|
'/',
|
|
'org.freedesktop.DBus.Introspectable.Introspect')
|
|
dbus = subprocess.Popen(cmd,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
(out, err) = dbus.communicate()
|
|
self.assertEqual(0, dbus.returncode,
|
|
msg='introspection of syncevo-dbus-server failed:\nenv = %s\ncmd = %s:\output:\n%s' %
|
|
(env, cmd, out))
|
|
|
|
# Now try some real command.
|
|
dbus = subprocess.Popen((os.path.join(os.path.dirname(sys.argv[0]), 'dbus-session.sh'),
|
|
'dbus-send',
|
|
'--print-reply',
|
|
'--dest=org.syncevolution',
|
|
'/org/syncevolution/Server',
|
|
'org.syncevolution.Server.GetVersions'),
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
(out, err) = dbus.communicate()
|
|
self.assertEqual(0, dbus.returncode,
|
|
msg='GetVersions of syncevo-dbus-server failed:\n' + out)
|
|
|
|
|
|
class TestDBusServerTerm(DBusUtil, unittest.TestCase):
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
|
|
def run(self, result):
|
|
self.runTest(result, True, ["-d", "10"])
|
|
|
|
def testSingleton(self):
|
|
"""TestDBusServerTerm.testSingleton - a second instance of syncevo-dbus-server must terminate right away"""
|
|
dbus = subprocess.Popen('syncevo-dbus-server --no-syslog --stdout'.split(),
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
(out, err) = dbus.communicate()
|
|
if not re.search(r'''\[ERROR.*already running\?\n''', out):
|
|
self.fail('second dbus server did not recognize already running server:\n%s' % out)
|
|
|
|
@timeout(100)
|
|
def testNoTerm(self):
|
|
"""TestDBusServerTerm.testNoTerm - D-Bus server must stay around during calls"""
|
|
|
|
"""The server should stay alive because we have dbus call within
|
|
the duration. The loop is to make sure the total time is longer
|
|
than duration and the dbus server still stays alive for dbus calls."""
|
|
for i in range(0, 16):
|
|
time.sleep(1)
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
logging.printf('GetConfigs failed: %s', ex)
|
|
self.fail("dbus server should work correctly")
|
|
|
|
@timeout(100)
|
|
def testTerm(self):
|
|
"""TestDBusServerTerm.testTerm - D-Bus server must auto-terminate"""
|
|
#sleep a duration and wait for syncevo-dbus-server termination
|
|
time.sleep(16)
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException:
|
|
pass
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
@timeout(100)
|
|
def testTermConnection(self):
|
|
"""TestDBusServerTerm.testTermConnection - D-Bus server must terminate after closing connection and not sooner"""
|
|
conpath = self.server.Connect({'description': 'test-dbus.py',
|
|
'transport': 'dummy'},
|
|
False,
|
|
"")
|
|
time.sleep(16)
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException:
|
|
self.fail("dbus server should not terminate")
|
|
|
|
connection = dbus.Interface(bus.get_object(self.server.bus_name,
|
|
conpath),
|
|
'org.syncevolution.Connection')
|
|
connection.Close(False, "good bye", utf8_strings=True)
|
|
time.sleep(16)
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException:
|
|
pass
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
@timeout(100)
|
|
def testTermAttachedClients(self):
|
|
"""TestDBusServerTerm.testTermAttachedClients - D-Bus server must not terminate while clients are attached"""
|
|
|
|
"""Also it tries to test the dbus server's behavior when a client
|
|
attaches the server many times"""
|
|
self.server.Attach()
|
|
self.server.Attach()
|
|
time.sleep(16)
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException:
|
|
self.fail("dbus server should not terminate")
|
|
self.server.Detach()
|
|
time.sleep(16)
|
|
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException:
|
|
self.fail("dbus server should not terminate")
|
|
|
|
self.server.Detach()
|
|
time.sleep(16)
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException:
|
|
pass
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
@timeout(100)
|
|
def testAutoSyncOn(self):
|
|
"""TestDBusServerTerm.testAutoSyncOn - D-Bus server must not terminate while auto syncing is enabled"""
|
|
self.setUpSession("scheduleworld")
|
|
# enable auto syncing with a very long delay to prevent accidentally running it
|
|
config = self.session.GetConfig(True, utf8_strings=True)
|
|
config[""]["autoSync"] = "1"
|
|
config[""]["autoSyncInterval"] = "60m"
|
|
self.session.SetConfig(False, False, config)
|
|
self.session.Detach()
|
|
|
|
time.sleep(16)
|
|
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException:
|
|
self.fail("dbus server should not terminate")
|
|
|
|
@timeout(100)
|
|
def testAutoSyncOff(self):
|
|
"""TestDBusServerTerm.testAutoSyncOff - D-Bus server must terminate after auto syncing was disabled"""
|
|
self.setUpSession("scheduleworld")
|
|
# enable auto syncing with a very long delay to prevent accidentally running it
|
|
config = self.session.GetConfig(True, utf8_strings=True)
|
|
config[""]["autoSync"] = "1"
|
|
config[""]["autoSyncInterval"] = "60m"
|
|
self.session.SetConfig(False, False, config)
|
|
self.session.Detach()
|
|
|
|
self.setUpSession("scheduleworld")
|
|
config[""]["autoSync"] = "0"
|
|
self.session.SetConfig(True, False, config)
|
|
self.session.Detach()
|
|
|
|
time.sleep(16)
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException:
|
|
pass
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
@timeout(100)
|
|
def testAutoSyncOff2(self):
|
|
"""TestDBusServerTerm.testAutoSyncOff2 - D-Bus server must terminate after auto syncing was disabled after a while"""
|
|
self.setUpSession("scheduleworld")
|
|
# enable auto syncing with a very long delay to prevent accidentally running it
|
|
config = self.session.GetConfig(True, utf8_strings=True)
|
|
config[""]["autoSync"] = "1"
|
|
config[""]["autoSyncInterval"] = "60m"
|
|
self.session.SetConfig(False, False, config)
|
|
self.session.Detach()
|
|
|
|
# wait until -d 10 second timeout has triggered in syncevo-dbus-server
|
|
time.sleep(11)
|
|
|
|
self.setUpSession("scheduleworld")
|
|
config[""]["autoSync"] = "0"
|
|
self.session.SetConfig(True, False, config)
|
|
self.session.Detach()
|
|
|
|
# should shut down after the 10 second idle period
|
|
time.sleep(16)
|
|
|
|
try:
|
|
self.server.GetConfigs(True, utf8_strings=True)
|
|
except dbus.DBusException:
|
|
pass
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
# configure server with files in test/test-dbus/auto-sync before starting it
|
|
@property("snapshot", "auto-sync")
|
|
@timeout(100)
|
|
def testAutoSyncOff3(self):
|
|
"""TestDBusServerTerm.testAutoSyncOff3 - start with auto-syncing on, D-Bus server must terminate after disabling auto syncing"""
|
|
# wait until -d 10 second timeout has triggered in syncevo-dbus-server,
|
|
# should not shut down because of auto sync
|
|
time.sleep(11)
|
|
self.assertTrue(self.isServerRunning())
|
|
|
|
self.setUpSession("scheduleworld")
|
|
config = self.session.GetConfig(False)
|
|
config[""]["autoSync"] = "0"
|
|
self.session.SetConfig(False, False, config)
|
|
self.session.Detach()
|
|
|
|
# should shut down after the 10 second idle period,
|
|
# may take longer when using valgrind
|
|
if usingValgrind():
|
|
time.sleep(30)
|
|
else:
|
|
time.sleep(16)
|
|
self.assertFalse(self.isServerRunning())
|
|
|
|
|
|
class TestNamedConfig(DBusUtil, unittest.TestCase):
|
|
"""Tests for Set/GetNamedConfig"""
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
|
|
def run(self, result):
|
|
self.runTest(result)
|
|
|
|
def testSetNamedConfigError(self):
|
|
"""TestNamedConfig.testSetNamedConfigError - SetNamedConfig() only allowed in 'all-configs' sessions"""
|
|
self.setUpSession("")
|
|
try:
|
|
self.session.SetNamedConfig("foobar", False, False, {})
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.InvalidCall: SetNameConfig() only allowed in 'all-configs' sessions")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testSetNamedConfigErrorTemporary(self):
|
|
"""TestNamedConfig.testSetNamedConfigErrorTemporary - SetNamedConfig() only implemented for session config"""
|
|
self.setUpSession("foo", [ "all-configs" ])
|
|
try:
|
|
self.session.SetNamedConfig("foobar", False, True, {})
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.InvalidCall: SetNameConfig() with temporary config change only supported for config named when starting the session")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
self.session.Detach()
|
|
|
|
self.setUpSession("")
|
|
self.session.SetNamedConfig("", False, True, {})
|
|
|
|
def testSetNamedConfig(self):
|
|
"""TestNamedConfig.testSetNamedConfig - create two different configs in one session"""
|
|
self.setUpSession("", [ "all-configs" ])
|
|
|
|
fooConfig = {"": {"username": "foo", "configName": "foo"}}
|
|
barConfig = {"": {"username": "bar", "configName": "bar"}}
|
|
|
|
self.session.SetNamedConfig("foo", False, False, fooConfig)
|
|
self.session.SetNamedConfig("bar", False, False, barConfig)
|
|
|
|
self.assertEqual(fooConfig, self.server.GetConfig("foo", False))
|
|
self.assertEqual(barConfig, self.server.GetConfig("bar", False))
|
|
|
|
self.assertEqual(fooConfig, self.session.GetNamedConfig("foo", False))
|
|
self.assertEqual(barConfig, self.session.GetNamedConfig("bar", False))
|
|
|
|
class TestDBusServerPresence(DBusUtil, unittest.TestCase):
|
|
"""Tests Presence signal and checkPresence API"""
|
|
|
|
# TODO: check auto sync + presence combination
|
|
|
|
# Our fake ConnMan implementation must be present on the
|
|
# bus also outside of tests, because syncevo-dbus-server
|
|
# will try to call it before setUp(). The implementation's
|
|
# initialization and tearDown() below ensures that the state
|
|
# is "online" outside of tests.
|
|
conn = root
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
self.cbFailure = None
|
|
# we don't know if the GetProperties() call was already
|
|
# processed; if not, wait for it here
|
|
if not self.conn.getPropertiesCalled:
|
|
self.conn.waitingForGetProperties = True
|
|
loop.run()
|
|
|
|
def tearDown(self):
|
|
self.conn.reset()
|
|
self.conf = None
|
|
|
|
def presenceCB(self,
|
|
server, status, transport,
|
|
expected):
|
|
try:
|
|
state = expected.pop(server, None)
|
|
if not state:
|
|
self.fail("unexpected presence signal for config " + server)
|
|
self.failUnlessEqual(status, state[0])
|
|
if not status:
|
|
self.failUnlessEqual(transport, state[1])
|
|
except Exception, ex:
|
|
# tell test method about the problem
|
|
loop.quit()
|
|
self.cbFailure = ex
|
|
# log exception
|
|
raise ex
|
|
|
|
if not expected:
|
|
# got all expected signals
|
|
loop.quit()
|
|
|
|
def expect(self, expected):
|
|
'''expected: hash from server config name to state+transport'''
|
|
match = bus.add_signal_receiver(lambda x,y,z:
|
|
self.presenceCB(x,y,z, expected), \
|
|
'Presence',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
return match
|
|
|
|
@property("ENV", "DBUS_TEST_CONNMAN=session DBUS_TEST_NETWORK_MANAGER=none")
|
|
@timeout(100)
|
|
def testPresenceSignal(self):
|
|
"""TestDBusServerPresence.testPresenceSignal - check Server.Presence signal"""
|
|
|
|
# creating a config does not trigger a Presence signal
|
|
self.setUpSession("foo")
|
|
self.session.SetConfig(False, False, {"" : {"syncURL": "http://http-only-1"}})
|
|
self.session.Detach()
|
|
|
|
# go offline
|
|
match = self.expect({"foo" : ("no transport", "")})
|
|
self.conn.setState("idle")
|
|
loop.run()
|
|
self.failIf(self.cbFailure)
|
|
match.remove()
|
|
|
|
# Changing the properties temporarily does change
|
|
# the presence of the config although strictly speaking,
|
|
# the presence of the config on disk hasn't changed.
|
|
# Not sure whether we really want that behavior.
|
|
match = self.expect({"foo" : ("", "obex-bt://temp-bluetooth-peer-changed-from-http")})
|
|
self.setUpSession("foo")
|
|
self.session.SetConfig(True, False, {"" : {"syncURL":
|
|
"obex-bt://temp-bluetooth-peer-changed-from-http"}})
|
|
# A ConnMan state change is needed to trigger the presence signal.
|
|
# Definitely not the behavior that we want :-/
|
|
self.conn.setState("failure")
|
|
loop.run()
|
|
self.failIf(self.cbFailure)
|
|
match.remove()
|
|
# remove temporary config change, back to using HTTP
|
|
# BUG BMC #24648 in syncevo-dbus-server: after discarding the temporary
|
|
# config change it keeps using the obex-bt syncURL.
|
|
# Work around that bug in thus test here temporarily
|
|
# by explicitly restoring the previous URL.
|
|
self.session.SetConfig(True, False, {"" : {"syncURL": "http://http-only-1"}})
|
|
self.session.Detach()
|
|
|
|
# create second session
|
|
self.setUpSession("bar")
|
|
self.session.SetConfig(False, False, {"" : {"syncURL":
|
|
"http://http-client-2"}})
|
|
self.session.Detach()
|
|
|
|
# go back to online mode
|
|
match = self.expect({"foo" : ("", "http://http-only-1"),
|
|
"bar" : ("", "http://http-client-2")})
|
|
self.conn.setState("online")
|
|
loop.run()
|
|
self.failIf(self.cbFailure)
|
|
match.remove()
|
|
|
|
# and offline
|
|
match = self.expect({"foo" : ("no transport", ""),
|
|
"bar" : ("no transport", "")})
|
|
self.conn.setState("idle")
|
|
loop.run()
|
|
self.failIf(self.cbFailure)
|
|
match.remove()
|
|
|
|
@property("ENV", "DBUS_TEST_CONNMAN=session DBUS_TEST_NETWORK_MANAGER=none")
|
|
@timeout(100)
|
|
def testServerCheckPresence(self):
|
|
"""TestDBusServerPresence.testServerCheckPresence - check Server.CheckPresence()"""
|
|
self.setUpSession("foo")
|
|
self.session.SetConfig(False, False, {"" : {"syncURL":
|
|
"http://http-client"}})
|
|
self.session.Detach()
|
|
self.setUpSession("bar")
|
|
self.session.SetConfig(False, False, {"" : {"syncURL":
|
|
"obex-bt://bt-client"}})
|
|
self.session.Detach()
|
|
self.setUpSession("foobar")
|
|
self.session.SetConfig(False, False, {"" : {"syncURL":
|
|
"obex-bt://bt-client-mixed http://http-client-mixed"}})
|
|
self.session.Detach()
|
|
|
|
# online initially
|
|
(status, transports) = self.server.CheckPresence ("foo")
|
|
self.assertEqual (status, "")
|
|
self.assertEqual (transports, ["http://http-client"])
|
|
(status, transports) = self.server.CheckPresence ("bar")
|
|
self.assertEqual (status, "")
|
|
self.assertEqual (transports, ["obex-bt://bt-client"])
|
|
(status, transports) = self.server.CheckPresence ("foobar")
|
|
self.assertEqual (status, "")
|
|
self.assertEqual (transports, ["obex-bt://bt-client-mixed",
|
|
"http://http-client-mixed"])
|
|
|
|
# go offline; Bluetooth remains on
|
|
self.conn.setState("idle")
|
|
# wait until server has seen the state change
|
|
match = bus.add_signal_receiver(lambda x,y,z: loop.quit(),
|
|
'Presence',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
match.remove()
|
|
(status, transports) = self.server.CheckPresence ("foo")
|
|
self.assertEqual (status, "no transport")
|
|
(status, transports) = self.server.CheckPresence ("bar")
|
|
self.assertEqual (status, "")
|
|
self.assertEqual (transports, ["obex-bt://bt-client"])
|
|
(status, transports) = self.server.CheckPresence ("foobar")
|
|
self.assertEqual (status, "")
|
|
self.assertEqual (transports, ["obex-bt://bt-client-mixed"])
|
|
|
|
@property("ENV", "DBUS_TEST_CONNMAN=session DBUS_TEST_NETWORK_MANAGER=none")
|
|
@timeout(100)
|
|
def testSessionCheckPresence(self):
|
|
"""TestDBusServerPresence.testSessionCheckPresence - check Session.CheckPresence()"""
|
|
self.setUpSession("foobar")
|
|
self.session.SetConfig(False, False, {"" : {"syncURL":
|
|
"obex-bt://bt-client-mixed http://http-client-mixed"}})
|
|
status = self.session.CheckPresence()
|
|
self.failUnlessEqual (status, "")
|
|
|
|
# go offline; Bluetooth remains on
|
|
self.conn.setState("idle")
|
|
# wait until server has seen the state change
|
|
match = bus.add_signal_receiver(lambda x,y,z: loop.quit(),
|
|
'Presence',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
match.remove()
|
|
|
|
# config uses Bluetooth, so syncing still possible
|
|
status = self.session.CheckPresence()
|
|
self.failUnlessEqual (status, "")
|
|
|
|
# now the same without Bluetooth, while offline
|
|
self.session.Detach()
|
|
self.setUpSession("foo")
|
|
self.session.SetConfig(False, False, {"" : {"syncURL": "http://http-only"}})
|
|
status = self.session.CheckPresence()
|
|
self.assertEqual (status, "no transport")
|
|
|
|
# go online
|
|
self.conn.setState("online")
|
|
# wait until server has seen the state change
|
|
match = bus.add_signal_receiver(lambda x,y,z: loop.quit(),
|
|
'Presence',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
match.remove()
|
|
status = self.session.CheckPresence()
|
|
self.failUnlessEqual (status, "")
|
|
|
|
# temporary config change shall always affect the
|
|
# Session.CheckPresence() result: go offline,
|
|
# then switch to Bluetooth (still present)
|
|
self.conn.setState("idle")
|
|
match = bus.add_signal_receiver(lambda x,y,z: loop.quit(),
|
|
'Presence',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
match.remove()
|
|
status = self.session.CheckPresence()
|
|
self.failUnlessEqual (status, "no transport")
|
|
self.session.SetConfig(True, False, {"" : {"syncURL": "obex-bt://bt-client-mixed"}})
|
|
status = self.session.CheckPresence()
|
|
self.failUnlessEqual (status, "")
|
|
|
|
def run(self, result):
|
|
self.runTest(result, True)
|
|
|
|
class TestDBusSession(DBusUtil, unittest.TestCase):
|
|
"""Tests that work with an active session."""
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
self.setUpSession("")
|
|
|
|
def run(self, result):
|
|
self.runTest(result)
|
|
|
|
def testCreateSession(self):
|
|
"""TestDBusSession.testCreateSession - ask for session"""
|
|
self.assertEqual(self.session.GetFlags(), [])
|
|
self.assertEqual(self.session.GetConfigName(), "@default")
|
|
|
|
def testAttachSession(self):
|
|
"""TestDBusSession.testAttachSession - attach to running session"""
|
|
self.session.Attach()
|
|
self.session.Detach()
|
|
self.assertEqual(self.session.GetFlags(), [])
|
|
self.assertEqual(self.session.GetConfigName(), "@default")
|
|
|
|
@timeout(70)
|
|
def testAttachOldSession(self):
|
|
"""TestDBusSession.testAttachOldSession - attach to session which no longer has clients"""
|
|
self.session.Detach()
|
|
time.sleep(5)
|
|
# This used to be impossible with SyncEvolution 1.0 because it
|
|
# removed the session right after the previous client
|
|
# left. SyncEvolution 1.1 makes it possible by keeping
|
|
# sessions around for a minute. However, the session is
|
|
# no longer listed because it really should only be used
|
|
# by clients which heard about it before.
|
|
self.assertEqual(self.server.GetSessions(), [])
|
|
self.session.Attach()
|
|
self.assertEqual(self.session.GetFlags(), [])
|
|
self.assertEqual(self.session.GetConfigName(), "@default")
|
|
time.sleep(60)
|
|
self.assertEqual(self.session.GetFlags(), [])
|
|
|
|
@timeout(70)
|
|
def testExpireSession(self):
|
|
"""TestDBusSession.testExpireSession - ensure that session stays around for a minute"""
|
|
self.session.Detach()
|
|
time.sleep(5)
|
|
self.assertEqual(self.session.GetFlags(), [])
|
|
self.assertEqual(self.session.GetConfigName(), "@default")
|
|
time.sleep(60)
|
|
try:
|
|
self.session.GetFlags()
|
|
except:
|
|
pass
|
|
else:
|
|
self.fail("Session.GetFlags() should have failed")
|
|
|
|
def testCreateSessionWithFlags(self):
|
|
"""TestDBusSession.testCreateSessionWithFlags - ask for session with some specific flags and config"""
|
|
self.session.Detach()
|
|
self.sessionpath, self.session = self.createSession("FooBar@no-such-context", True, ["foo", "bar"])
|
|
self.assertEqual(self.session.GetFlags(), ["foo", "bar"])
|
|
self.assertEqual(self.session.GetConfigName(), "foobar@no-such-context")
|
|
|
|
def testSecondSession(self):
|
|
"""TestDBusSession.testSecondSession - a second session should not run unless the first one stops"""
|
|
sessions = self.server.GetSessions()
|
|
self.assertEqual(sessions, [self.sessionpath])
|
|
sessionpath = self.server.StartSession("")
|
|
sessions = self.server.GetSessions()
|
|
self.assertEqual(sessions, [self.sessionpath, sessionpath])
|
|
|
|
def session_ready(object, ready):
|
|
if self.running:
|
|
DBusUtil.quit_events.append("session " + object + (ready and " ready" or " done"))
|
|
loop.quit()
|
|
|
|
bus.add_signal_receiver(session_ready,
|
|
'SessionChanged',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
def status(*args):
|
|
if self.running:
|
|
DBusUtil.events.append(("status", args))
|
|
if args[0] == "idle":
|
|
DBusUtil.quit_events.append("session " + sessionpath + " idle")
|
|
loop.quit()
|
|
|
|
bus.add_signal_receiver(status,
|
|
'StatusChanged',
|
|
'org.syncevolution.Session',
|
|
self.server.bus_name,
|
|
sessionpath,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
session = dbus.Interface(bus.get_object(self.server.bus_name,
|
|
sessionpath),
|
|
'org.syncevolution.Session')
|
|
status, error, sources = session.GetStatus(utf8_strings=True)
|
|
self.assertEqual(status, "queueing")
|
|
# use hash so that we can write into it in callback()
|
|
callback_called = {}
|
|
def callback():
|
|
callback_called[1] = "callback()"
|
|
self.session.Detach()
|
|
try:
|
|
t1 = self.addTimeout(2, callback)
|
|
# session 1 done
|
|
loop.run()
|
|
self.assertTrue(callback_called)
|
|
# session 2 ready and idle
|
|
loop.run()
|
|
loop.run()
|
|
expected = ["session " + self.sessionpath + " done",
|
|
"session " + sessionpath + " idle",
|
|
"session " + sessionpath + " ready"]
|
|
expected.sort()
|
|
DBusUtil.quit_events.sort()
|
|
self.assertEqual(DBusUtil.quit_events, expected)
|
|
status, error, sources = session.GetStatus(utf8_strings=True)
|
|
self.assertEqual(status, "idle")
|
|
finally:
|
|
self.removeTimeout(t1)
|
|
|
|
class TestSessionAPIsEmptyName(DBusUtil, unittest.TestCase):
|
|
"""Test session APIs that work with an empty server name. Thus, all of session APIs which
|
|
need this kind of checking are put in this class. """
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
self.setUpSession("")
|
|
|
|
def run(self, result):
|
|
self.runTest(result)
|
|
|
|
def testGetConfigEmptyName(self):
|
|
"""TestSessionAPIsEmptyName.testGetConfigEmptyName - reading empty default config"""
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
|
|
def testGetTemplateEmptyName(self):
|
|
"""TestSessionAPIsEmptyName.testGetTemplateEmptyName - trigger error by getting template for empty server name"""
|
|
try:
|
|
config = self.session.GetConfig(True, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchConfig: No template '' found")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testCheckSourceEmptyName(self):
|
|
"""TestSessionAPIsEmptyName.testCheckSourceEmptyName - Test the error is reported when the server name is empty for CheckSource"""
|
|
try:
|
|
self.session.CheckSource("", utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchSource: '' has no '' datastore")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testGetDatabasesEmptyName(self):
|
|
"""TestSessionAPIsEmptyName.testGetDatabasesEmptyName - Test the error is reported when the server name is empty for GetDatabases"""
|
|
try:
|
|
self.session.GetDatabases("", utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchSource: '' has no '' datastore")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testGetReportsEmptyName(self):
|
|
"""TestSessionAPIsEmptyName.testGetReportsEmptyName - Test reports from all peers are returned in order when the peer name is empty for GetReports"""
|
|
self.setUpFiles('reports')
|
|
reports = self.session.GetReports(0, 0xFFFFFFFF, utf8_strings=True)
|
|
self.assertEqual(len(reports), 7)
|
|
refPeers = ["dummy-test", "dummy", "dummy-test", "dummy-test",
|
|
"dummy-test", "dummy_test", "dummy-test"]
|
|
for i in range(0, len(refPeers)):
|
|
self.assertEqual(reports[i]["peer"], refPeers[i])
|
|
|
|
def testGetReportsContext(self):
|
|
"""TestSessionAPIsEmptyName.testGetReportsContext - Test reports from a context are returned when the peer name is empty for GetReports"""
|
|
self.setUpFiles('reports')
|
|
self.session.Detach()
|
|
self.setUpSession("@context")
|
|
reports = self.session.GetReports(0, 0xFFFFFFFF, utf8_strings=True)
|
|
self.assertEqual(len(reports), 1)
|
|
self.assertTrue(reports[0]["dir"].endswith("dummy_+test@context-2010-01-20-10-10"))
|
|
|
|
|
|
class TestSessionAPIsDummy(DBusUtil, unittest.TestCase):
|
|
"""Tests that work for GetConfig/SetConfig/CheckSource/GetDatabases/GetReports in Session.
|
|
This class is only working in a dummy config. Thus it can't do sync correctly. The purpose
|
|
is to test some cleanup cases and expected errors. Also, some unit tests for some APIs
|
|
depend on a clean configuration so they are included here. For those unit tests depending
|
|
on sync, another class is used """
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
# use 'dummy-test' as the server name
|
|
self.setUpSession("dummy-test")
|
|
# default config
|
|
self.config = {
|
|
"" : { "syncURL" : "http://impossible-syncurl-just-for-testing-to-avoid-conflict",
|
|
"username" : "unknown",
|
|
# the password request tests depend on not having a real password here
|
|
"password" : "-",
|
|
"deviceId" : "foo",
|
|
"RetryInterval" : "10",
|
|
"RetryDuration" : "20",
|
|
"ConsumerReady" : "1",
|
|
"configName" : "dummy-test"
|
|
},
|
|
"source/addressbook" : { "sync" : "slow",
|
|
"backend" : "file",
|
|
"database" : "file://" + xdg_root + "/addressbook",
|
|
"databaseFormat" : "text/vcard",
|
|
"uri" : "card"
|
|
},
|
|
"source/calendar" : { "sync" : "disabled",
|
|
"backend" : "file",
|
|
"database" : "file://" + xdg_root + "/calendar",
|
|
"databaseFormat" : "text/calendar",
|
|
"uri" : "cal"
|
|
},
|
|
"source/todo" : { "sync" : "disabled",
|
|
"backend" : "file",
|
|
"database" : "file://" + xdg_root + "/todo",
|
|
"databaseFormat" : "text/calendar",
|
|
"uri" : "task"
|
|
},
|
|
"source/memo" : { "sync" : "disabled",
|
|
"backend" : "file",
|
|
"database" : "file://" + xdg_root + "/memo",
|
|
"databaseFormat" : "text/calendar",
|
|
"uri" : "text"
|
|
}
|
|
}
|
|
# update config
|
|
self.updateConfig = {
|
|
"" : { "username" : "doe"},
|
|
"source/addressbook" : { "sync" : "slow"}
|
|
}
|
|
self.sources = ['addressbook', 'calendar', 'todo', 'memo']
|
|
|
|
# set by SessionReady signal handlers in some tests
|
|
self.auto_sync_session_path = None
|
|
|
|
def run(self, result):
|
|
self.runTest(result)
|
|
|
|
def clearAllConfig(self):
|
|
""" clear a server config. All should be removed. Used internally. """
|
|
emptyConfig = {}
|
|
self.session.SetConfig(False, False, emptyConfig, utf8_strings=True)
|
|
|
|
def setupConfig(self):
|
|
""" create a server with full config. Used internally. """
|
|
self.session.SetConfig(False, False, self.config, utf8_strings=True)
|
|
|
|
def testTemporaryConfig(self):
|
|
"""TestSessionAPIsDummy.testTemporaryConfig - various temporary config changes"""
|
|
ref = { "": { "loglevel": "2", "configName": "dummy-test" } }
|
|
config = copy.deepcopy(ref)
|
|
self.session.SetConfig(False, False, config, utf8_strings=True)
|
|
# reset
|
|
self.session.SetConfig(False, True, {}, utf8_strings=True)
|
|
self.assertEqual(config, self.session.GetConfig(False, utf8_strings=True))
|
|
# add sync prop
|
|
self.session.SetConfig(True, True, { "": { "loglevel": "100" } }, utf8_strings=True)
|
|
config[""]["loglevel"] = "100"
|
|
self.assertEqual(config, self.session.GetConfig(False, utf8_strings=True))
|
|
# add source
|
|
self.session.SetConfig(True, True, { "source/foobar": { "sync": "two-way" } }, utf8_strings=True)
|
|
config["source/foobar"] = { "sync": "two-way" }
|
|
self.session.SetConfig(True, True, { "": { "loglevel": "100" } }, utf8_strings=True)
|
|
# add source prop
|
|
self.session.SetConfig(True, True, { "source/foobar": { "database": "xyz" } }, utf8_strings=True)
|
|
config["source/foobar"]["database"] = "xyz"
|
|
self.assertEqual(config, self.session.GetConfig(False, utf8_strings=True))
|
|
# reset temporary settings
|
|
self.session.SetConfig(False, True, { }, utf8_strings=True)
|
|
config = copy.deepcopy(ref)
|
|
self.assertEqual(config, self.session.GetConfig(False, utf8_strings=True))
|
|
|
|
def testCreateGetConfig(self):
|
|
"""TestSessionAPIsDummy.testCreateGetConfig - test the config is created successfully. """
|
|
self.setUpConfigListeners()
|
|
self.config[""]["username"] = "creategetconfig"
|
|
self.config[""]["password"] = "112233445566778"
|
|
self.setupConfig()
|
|
""" get config and compare """
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config, self.config)
|
|
# terminate session and check whether a "config changed" signal
|
|
# was sent as required
|
|
self.session.Detach()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.events, ["ConfigChanged"])
|
|
|
|
def testUpdateConfig(self):
|
|
"""TestSessionAPIsDummy.testUpdateConfig - test the config is permenantly updated correctly. """
|
|
self.setupConfig()
|
|
""" update the given config """
|
|
self.session.SetConfig(True, False, self.updateConfig, utf8_strings=True)
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["username"], "doe")
|
|
self.assertEqual(config["source/addressbook"]["sync"], "slow")
|
|
|
|
def testUpdateConfigTemp(self):
|
|
"""TestSessionAPIsDummy.testUpdateConfigTemp - test the config is just temporary updated but no effect in storage. """
|
|
self.setupConfig()
|
|
""" set config temporary """
|
|
self.session.SetConfig(True, True, self.updateConfig, utf8_strings=True)
|
|
self.session.Detach()
|
|
""" creat a new session to lose the temporary configs """
|
|
self.setUpSession("dummy-test")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
""" no change of any properties """
|
|
self.assertEqual(config, self.config)
|
|
|
|
def testGetConfigUpdateConfigTemp(self):
|
|
"""TestSessionAPIsDummy.testGetConfigUpdateConfigTemp - test the config is temporary updated and in effect for GetConfig in the current session. """
|
|
self.setupConfig()
|
|
""" set config temporary """
|
|
self.session.SetConfig(True, True, self.updateConfig, utf8_strings=True)
|
|
""" GetConfig is affected """
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
""" no change of any properties """
|
|
self.assertEqual(config[""]["username"], "doe")
|
|
self.assertEqual(config["source/addressbook"]["sync"], "slow")
|
|
|
|
def testGetConfigWithTempConfig(self):
|
|
"""TestSessionAPIsDummy.testGetConfigWithTempConfig - test the config is gotten for a new temporary config. """
|
|
""" The given config doesn't exist on disk and it's set temporarily. Then GetConfig should
|
|
return the configs temporarily set. """
|
|
self.session.SetConfig(True, True, self.config, utf8_strings=True)
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config, self.config)
|
|
|
|
def testUpdateConfigError(self):
|
|
"""TestSessionAPIsDummy.testUpdateConfigError - test the right error is reported when an invalid property value is set """
|
|
self.setupConfig()
|
|
config = {
|
|
"source/addressbook" : { "sync" : "invalid-value"}
|
|
}
|
|
try:
|
|
self.session.SetConfig(True, False, config, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.InvalidCall: invalid value 'invalid-value' for "
|
|
"property 'sync': 'not one of the valid values (two-way, slow, "
|
|
"refresh-from-local, refresh-from-remote = refresh, one-way-from-local, "
|
|
"one-way-from-remote = one-way, refresh-from-client = refresh-client, "
|
|
"refresh-from-server = refresh-server, one-way-from-client = one-way-client, "
|
|
"one-way-from-server = one-way-server, "
|
|
"local-cache-slow, local-cache-incremental = local-cache, "
|
|
"disabled = none)'")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testUpdateNoConfig(self):
|
|
"""TestSessionAPIsDummy.testUpdateNoConfig - test the right error is reported when updating properties for a non-existing server """
|
|
try:
|
|
self.session.SetConfig(True, False, self.updateConfig, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchConfig: The configuration 'dummy-test' doesn't exist")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testUnknownConfigContent(self):
|
|
"""TestSessionAPIsDummy.testUnknownConfigContent - config with unkown must be rejected"""
|
|
self.setupConfig()
|
|
|
|
try:
|
|
config1 = copy.deepcopy(self.config)
|
|
config1[""]["no-such-sync-property"] = "foo"
|
|
self.session.SetConfig(False, False, config1, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.InvalidCall: unknown property 'no-such-sync-property'")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
try:
|
|
config1 = copy.deepcopy(self.config)
|
|
config1["source/addressbook"]["no-such-source-property"] = "foo"
|
|
self.session.SetConfig(False, False, config1, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.InvalidCall: unknown property 'no-such-source-property'")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
try:
|
|
config1 = copy.deepcopy(self.config)
|
|
config1["no-such-key"] = { "foo": "bar" }
|
|
self.session.SetConfig(False, False, config1, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.InvalidCall: invalid config entry 'no-such-key'")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testClearAllConfig(self):
|
|
"""TestSessionAPIsDummy.testClearAllConfig - test all configs of a server are cleared correctly. """
|
|
""" first set up config and then clear all configs and also check a non-existing config """
|
|
self.setupConfig()
|
|
self.clearAllConfig()
|
|
try:
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchConfig: No configuration 'dummy-test' found")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testCheckSourceNoConfig(self):
|
|
"""TestSessionAPIsDummy.testCheckSourceNoConfig - test the right error is reported when the server doesn't exist """
|
|
try:
|
|
self.session.CheckSource("", utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchSource: 'dummy-test' has no '' datastore")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testCheckSourceNoSourceName(self):
|
|
"""TestSessionAPIsDummy.testCheckSourceNoSourceName - test the right error is reported when the datastore doesn't exist """
|
|
self.setupConfig()
|
|
try:
|
|
self.session.CheckSource("dummy", utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchSource: 'dummy-test' "
|
|
"has no 'dummy' datastore")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testCheckSourceInvalidDatabase(self):
|
|
"""TestSessionAPIsDummy.testCheckSourceInvalidEvolutionSource - test the right error is reported when the evolutionsource is invalid """
|
|
self.setupConfig()
|
|
config = { "source/memo" : { "database" : "impossible-source"} }
|
|
self.session.SetConfig(True, False, config, utf8_strings=True)
|
|
try:
|
|
self.session.CheckSource("memo", utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.SourceUnusable: The datastore 'memo' is not usable")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testCheckSourceInvalidBackend(self):
|
|
"""TestSessionAPIsDummy.testCheckSourceInvalidBackend - test the right error is reported when the type is invalid """
|
|
self.setupConfig()
|
|
config = { "source/memo" : { "backend" : "no-such-backend"} }
|
|
try:
|
|
self.session.SetConfig(True, False, config, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
expected = "org.syncevolution.InvalidCall: invalid value 'no-such-backend' for property 'backend': "
|
|
self.assertEqual(str(ex)[0:len(expected)], expected)
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testCheckSourceNoBackend(self):
|
|
"""TestSessionAPIsDummy.testCheckSourceNoBackend - test the right error is reported when the datastore is unusable"""
|
|
self.setupConfig()
|
|
config = { "source/memo" : { "backend" : "file",
|
|
"databaseFormat" : "text/calendar",
|
|
"database" : "file:///no/such/path" } }
|
|
self.session.SetConfig(True, False, config, utf8_strings=True)
|
|
try:
|
|
self.session.CheckSource("memo", utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.SourceUnusable: The datastore 'memo' is not usable")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testCheckSource(self):
|
|
"""TestSessionAPIsDummy.testCheckSource - testCheckSource - test all datastores are okay"""
|
|
self.setupConfig()
|
|
try:
|
|
for source in self.sources:
|
|
self.session.CheckSource(source, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.fail(ex)
|
|
|
|
def testCheckSourceUpdateConfigTemp(self):
|
|
"""TestSessionAPIsDummy.testCheckSourceUpdateConfigTemp - test the config is temporary updated and in effect for GetDatabases in the current session. """
|
|
self.setupConfig()
|
|
tempConfig = {"source/temp" : { "backend" : "calendar"}}
|
|
self.session.SetConfig(True, True, tempConfig, utf8_strings=True)
|
|
databases2 = self.session.CheckSource("temp", utf8_strings=True)
|
|
|
|
def testGetDatabasesNoConfig(self):
|
|
"""TestSessionAPIsDummy.testGetDatabasesNoConfig - test the right error is reported when the server doesn't exist """
|
|
# make sure the config doesn't exist """
|
|
try:
|
|
self.session.GetDatabases("", utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchSource: 'dummy-test' has no '' datastore")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testGetDatabasesEmpty(self):
|
|
"""TestSessionAPIsDummy.testGetDatabasesEmpty - test the right error is reported for non-existing datastore"""
|
|
self.setupConfig()
|
|
try:
|
|
databases = self.session.GetDatabases("never_use_this_source_name", utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.NoSuchSource: 'dummy-test' has no 'never_use_this_source_name' datastore")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
def testGetDatabases(self):
|
|
"""TestSessionAPIsDummy.testGetDatabases - test the right way to get databases """
|
|
self.setupConfig()
|
|
|
|
# don't know actual databases, so compare results of two different times
|
|
sources = ['addressbook', 'calendar', 'todo', 'memo']
|
|
databases1 = []
|
|
for source in sources:
|
|
databases1.append(self.session.GetDatabases(source, utf8_strings=True))
|
|
# reverse the list of sources and get databases again
|
|
sources.reverse()
|
|
databases2 = []
|
|
for source in sources:
|
|
databases2.append(self.session.GetDatabases(source, utf8_strings=True))
|
|
# sort two arrays
|
|
databases1.sort()
|
|
databases2.sort()
|
|
self.assertEqual(databases1, databases2)
|
|
|
|
def testGetDatabasesUpdateConfigTemp(self):
|
|
"""TestSessionAPIsDummy.testGetDatabasesUpdateConfigTemp - test the config is temporary updated and in effect for GetDatabases in the current session. """
|
|
self.setupConfig()
|
|
# file backend: reports a short help text instead of a real database list
|
|
databases1 = self.session.GetDatabases("calendar", utf8_strings=True)
|
|
# databaseFormat is required for file backend, otherwise it
|
|
# cannot be instantiated and even simple operations as reading
|
|
# the (in this case fixed) list of databases fail
|
|
tempConfig = {"source/temp" : { "backend" : "file", "databaseFormat" : "text/calendar" }}
|
|
self.session.SetConfig(True, True, tempConfig, utf8_strings=True)
|
|
databases2 = self.session.GetDatabases("temp", utf8_strings=True)
|
|
self.assertEqual(databases2, databases1)
|
|
|
|
def testGetReportsNoConfig(self):
|
|
"""TestSessionAPIsDummy.testGetReportsNoConfig - Test nothing is gotten when the given server doesn't exist. Also covers boundaries """
|
|
reports = self.session.GetReports(0, 0, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
reports = self.session.GetReports(0, 1, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
reports = self.session.GetReports(0, 0xFFFFFFFF, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
reports = self.session.GetReports(0xFFFFFFFF, 0xFFFFFFFF, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
|
|
def testGetReportsNoReports(self):
|
|
"""TestSessionAPIsDummy.testGetReportsNoReports - Test when the given server has no reports. Also covers boundaries """
|
|
self.setupConfig()
|
|
reports = self.session.GetReports(0, 0, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
reports = self.session.GetReports(0, 1, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
reports = self.session.GetReports(0, 0xFFFFFFFF, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
reports = self.session.GetReports(0xFFFFFFFF, 0xFFFFFFFF, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
|
|
def testGetReportsByRef(self):
|
|
"""TestSessionAPIsDummy.testGetReportsByRef - Test the reports are gotten correctly from reference files. Also covers boundaries """
|
|
""" This could be extractly compared since the reference files are known """
|
|
self.setUpFiles('reports')
|
|
report0 = { "peer" : "dummy-test",
|
|
"start" : "1258520955",
|
|
"end" : "1258520964",
|
|
"status" : "200",
|
|
"source-addressbook-mode" : "slow",
|
|
"source-addressbook-first" : "true",
|
|
"source-addressbook-resume" : "false",
|
|
"source-addressbook-status" : "0",
|
|
"source-addressbook-backup-before" : "0",
|
|
"source-addressbook-backup-after" : "0",
|
|
"source-addressbook-stat-local-any-sent" : "9168",
|
|
"source-addressbook-stat-remote-added-total" : "71",
|
|
"source-addressbook-stat-remote-updated-total" : "100",
|
|
"source-addressbook-stat-local-updated-total" : "632",
|
|
"source-addressbook-stat-remote-any-reject" : "100",
|
|
"source-addressbook-stat-remote-any-conflict_duplicated" : "5293487",
|
|
"source-addressbook-stat-remote-any-conflict_client_won" : "33",
|
|
"source-addressbook-stat-local-any-received" : "2",
|
|
"source-addressbook-stat-local-removed-total" : "4",
|
|
"source-addressbook-stat-remote-any-conflict_server_won" : "38",
|
|
"source-addressbook-stat-local-any-reject" : "77",
|
|
"source-addressbook-stat-local-added-total" : "84",
|
|
"source-addressbook-stat-remote-removed-total" : "66",
|
|
"source-calendar-mode" : "slow",
|
|
"source-calendar-first" : "true",
|
|
"source-calendar-resume" : "false",
|
|
"source-calendar-status" : "0",
|
|
"source-calendar-backup-before" : "17",
|
|
"source-calendar-backup-after" : "17",
|
|
"source-calendar-stat-local-any-sent" : "8619",
|
|
"source-calendar-stat-remote-added-total": "17",
|
|
"source-calendar-stat-remote-updated-total" : "10",
|
|
"source-calendar-stat-local-updated-total" : "6",
|
|
"source-calendar-stat-remote-any-reject" : "1",
|
|
"source-calendar-stat-remote-any-conflict_duplicated" : "5",
|
|
"source-calendar-stat-remote-any-conflict_client_won" : "3",
|
|
"source-calendar-stat-local-any-received" : "24",
|
|
"source-calendar-stat-local-removed-total" : "54",
|
|
"source-calendar-stat-remote-any-conflict_server_won" : "38",
|
|
"source-calendar-stat-local-any-reject" : "7",
|
|
"source-calendar-stat-local-added-total" : "42",
|
|
"source-calendar-stat-remote-removed-total" : "6",
|
|
"source-memo-mode" : "slow",
|
|
"source-memo-first" : "true",
|
|
"source-memo-resume" : "false",
|
|
"source-memo-status" : "0",
|
|
"source-memo-backup-before" : "3",
|
|
"source-memo-backup-after" : "4",
|
|
"source-memo-stat-local-any-sent" : "8123",
|
|
"source-memo-stat-remote-added-total" : "15",
|
|
"source-memo-stat-remote-updated-total" : "6",
|
|
"source-memo-stat-local-updated-total" : "8",
|
|
"source-memo-stat-remote-any-reject" : "16",
|
|
"source-memo-stat-remote-any-conflict_duplicated" : "27",
|
|
"source-memo-stat-remote-any-conflict_client_won" : "2",
|
|
"source-memo-stat-local-any-received" : "3",
|
|
"source-memo-stat-local-removed-total" : "4",
|
|
"source-memo-stat-remote-any-conflict_server_won" : "8",
|
|
"source-memo-stat-local-any-reject" : "40",
|
|
"source-memo-stat-local-added-total" : "34",
|
|
"source-memo-stat-remote-removed-total" : "5",
|
|
"source-todo-mode" : "slow",
|
|
"source-todo-first" : "true",
|
|
"source-todo-resume" : "false",
|
|
"source-todo-status" : "0",
|
|
"source-todo-backup-before" : "2",
|
|
"source-todo-backup-after" : "2",
|
|
"source-todo-stat-local-any-sent" : "619",
|
|
"source-todo-stat-remote-added-total" : "71",
|
|
"source-todo-stat-remote-updated-total" : "1",
|
|
"source-todo-stat-local-updated-total" : "9",
|
|
"source-todo-stat-remote-any-reject" : "10",
|
|
"source-todo-stat-remote-any-conflict_duplicated" : "15",
|
|
"source-todo-stat-remote-any-conflict_client_won" : "7",
|
|
"source-todo-stat-local-any-received" : "2",
|
|
"source-todo-stat-local-removed-total" : "4",
|
|
"source-todo-stat-remote-any-conflict_server_won" : "8",
|
|
"source-todo-stat-local-any-reject" : "3",
|
|
"source-todo-stat-local-added-total" : "24",
|
|
"source-todo-stat-remote-removed-total" : "80" }
|
|
reports = self.session.GetReports(0, 0, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
# get only one report
|
|
reports = self.session.GetReports(0, 1, utf8_strings=True)
|
|
self.assertTrue(len(reports) == 1)
|
|
del reports[0]["dir"]
|
|
|
|
self.assertEqual(reports[0], report0)
|
|
""" the number of reference sessions is totally 5. Check the returned count
|
|
when parameter is bigger than 5 """
|
|
reports = self.session.GetReports(0, 0xFFFFFFFF, utf8_strings=True)
|
|
self.assertTrue(len(reports) == 5)
|
|
# start from 2, this could check integer overflow
|
|
reports2 = self.session.GetReports(2, 0xFFFFFFFF, utf8_strings=True)
|
|
self.assertTrue(len(reports2) == 3)
|
|
# the first element of reports2 should be the same as the third element of reports
|
|
self.assertEqual(reports[2], reports2[0])
|
|
# indexed from 5, nothing could be gotten
|
|
reports = self.session.GetReports(5, 0xFFFFFFFF, utf8_strings=True)
|
|
self.assertEqual(reports, [])
|
|
|
|
def testRestoreByRef(self):
|
|
"""TestSessionAPIsDummy.testRestoreByRef - restore data before or after a given session"""
|
|
self.setUpFiles('restore')
|
|
self.setupConfig()
|
|
self.setUpListeners(self.sessionpath)
|
|
reports = self.session.GetReports(0, 1, utf8_strings=True)
|
|
dir = reports[0]["dir"]
|
|
sessionpath, session = self.createSession("dummy-test", False)
|
|
#TODO: check restore result, how?
|
|
#restore data before this session
|
|
self.session.Restore(dir, True, [], utf8_strings=True)
|
|
loop.run()
|
|
self.session.Detach()
|
|
|
|
# check recorded events in DBusUtil.events, first filter them
|
|
statuses = []
|
|
progresses = []
|
|
for item in DBusUtil.events:
|
|
if item[0] == "status":
|
|
statuses.append(item[1])
|
|
elif item[0] == "progress":
|
|
progresses.append(item[1])
|
|
|
|
lastStatus = ""
|
|
lastSources = {}
|
|
statusPairs = {"": 0, "idle": 1, "running" : 2, "done" : 3}
|
|
for status, error, sources in statuses:
|
|
self.assertFalse(status == lastStatus and lastSources == sources)
|
|
# no error
|
|
self.assertEqual(error, 0)
|
|
for sourcename, value in sources.items():
|
|
# no error
|
|
self.assertEqual(value[2], 0)
|
|
# keep order: source status must also be unchanged or the next status
|
|
if lastSources.has_key(sourcename):
|
|
lastValue = lastSources[sourcename]
|
|
self.assertTrue(statusPairs[value[1]] >= statusPairs[lastValue[1]])
|
|
|
|
lastStatus = status
|
|
lastSources = sources
|
|
|
|
# check increasing progress percentage
|
|
lastPercent = 0
|
|
for percent, sources in progresses:
|
|
self.assertFalse(percent < lastPercent)
|
|
lastPercent = percent
|
|
|
|
session.SetConfig(False, False, self.config, utf8_strings=True)
|
|
#restore data after this session
|
|
session.Restore(dir, False, ["addressbook", "calendar"], utf8_strings=True)
|
|
loop.run()
|
|
|
|
def testSecondRestore(self):
|
|
"""TestSessionAPIsDummy.testSecondRestore - right error thrown when session is not active?"""
|
|
self.setUpFiles('restore')
|
|
self.setupConfig()
|
|
self.setUpListeners(self.sessionpath)
|
|
reports = self.session.GetReports(0, 1, utf8_strings=True)
|
|
dir = reports[0]["dir"]
|
|
sessionpath, session = self.createSession("dummy-test", False)
|
|
try:
|
|
session.Restore(dir, False, [], utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.InvalidCall: session is not active, call not allowed at this time")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
self.session.Detach()
|
|
session.SetConfig(False, False, self.config, utf8_strings=True)
|
|
session.Restore(dir, False, [], utf8_strings=True)
|
|
loop.run()
|
|
|
|
@timeout(300)
|
|
def testInteractivePassword(self):
|
|
"""TestSessionAPIsDummy.testInteractivePassword - test the info request is correctly working for password """
|
|
self.setupConfig()
|
|
self.setUpListeners(self.sessionpath)
|
|
self.lastState = "unknown"
|
|
# define callback for InfoRequest signals and send corresponds response
|
|
# to dbus server
|
|
def infoRequest(id, session, state, handler, type, params):
|
|
if state == "request":
|
|
self.assertEqual(self.lastState, "unknown")
|
|
self.lastState = "request"
|
|
self.server.InfoResponse(id, "working", {}, utf8_strings=True)
|
|
elif state == "waiting":
|
|
self.assertEqual(self.lastState, "request")
|
|
self.lastState = "waiting"
|
|
self.server.InfoResponse(id, "response", {"password" : "123456"}, utf8_strings=True)
|
|
elif state == "done":
|
|
self.assertEqual(self.lastState, "waiting")
|
|
self.lastState = "done"
|
|
else:
|
|
self.fail("state should not be '" + state + "'")
|
|
|
|
signal = bus.add_signal_receiver(infoRequest,
|
|
'InfoRequest',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
# dbus server will be blocked by gnome-keyring-ask dialog, so we kill it, and then
|
|
# it can't get the password from gnome keyring and send info request for password
|
|
def callback():
|
|
kill = subprocess.Popen('killall -9 gnome-keyring-ask'.split(),
|
|
stdout=open('/dev/null', 'w'),
|
|
stderr=subprocess.STDOUT)
|
|
kill.wait()
|
|
# Kill again soon.
|
|
return True
|
|
|
|
try:
|
|
timeout_handler = Timeout.addTimeout(1, callback)
|
|
|
|
# try to sync and invoke password request
|
|
self.session.Sync("", {})
|
|
loop.run()
|
|
self.assertEqual(self.lastState, "done")
|
|
finally:
|
|
Timeout.removeTimeout(timeout_handler)
|
|
|
|
def doAutoSyncNetworkFailure(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncNetworkFailure - test that auto-sync is triggered, fails due to (temporary?!) network error here"""
|
|
self.setupConfig()
|
|
# enable auto-sync
|
|
config = copy.deepcopy(self.config)
|
|
# Note that writing this config will modify the host's keyring!
|
|
# Use a syncURL that is unlikely to conflict with the host
|
|
# or any other D-Bus test.
|
|
config[""]["syncURL"] = "http://no-such-domain.foobar"
|
|
config[""]["autoSync"] = "1"
|
|
config[""]["autoSyncDelay"] = "1"
|
|
config[""]["autoSyncInterval"] = "10s"
|
|
config[""]["password"] = "foobar"
|
|
self.session.SetConfig(True, False, config, utf8_strings=True)
|
|
|
|
def session_ready(object, ready):
|
|
if self.running and object != self.sessionpath and \
|
|
(self.auto_sync_session_path == None and ready or \
|
|
self.auto_sync_session_path == object):
|
|
self.auto_sync_session_path = object
|
|
DBusUtil.quit_events.append("session " + object + (ready and " ready" or " done"))
|
|
loop.quit()
|
|
|
|
signal = bus.add_signal_receiver(session_ready,
|
|
'SessionChanged',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
# shut down current session, will allow auto-sync
|
|
self.session.Detach()
|
|
|
|
# wait for start and end of auto-sync session
|
|
loop.run()
|
|
start1 = time.time()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.auto_sync_session_path + " ready",
|
|
"session " + self.auto_sync_session_path + " done"])
|
|
DBusUtil.quit_events = []
|
|
# session must be around for a while after terminating, to allow
|
|
# reading information about it by clients who didn't start it
|
|
# and thus wouldn't know what the session was about otherwise
|
|
session = dbus.Interface(bus.get_object(self.server.bus_name,
|
|
self.auto_sync_session_path),
|
|
'org.syncevolution.Session')
|
|
reports = session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(len(reports), 1)
|
|
self.assertEqual(reports[0]["status"], "20043")
|
|
name = session.GetConfigName()
|
|
self.assertEqual(name, "dummy-test")
|
|
flags = session.GetFlags()
|
|
self.assertEqual(flags, [])
|
|
first_auto = self.auto_sync_session_path
|
|
self.auto_sync_session_path = None
|
|
|
|
# check that interval between auto-sync sessions is right
|
|
loop.run()
|
|
start2 = time.time()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.auto_sync_session_path + " ready",
|
|
"session " + self.auto_sync_session_path + " done"])
|
|
self.assertNotEqual(first_auto, self.auto_sync_session_path)
|
|
delta = start2 - start1
|
|
# avoid timing checks when running under valgrind
|
|
if not usingValgrind():
|
|
self.assertTrue(delta < 13)
|
|
self.assertTrue(delta > 7)
|
|
|
|
# check that org.freedesktop.Notifications.Notify was not called
|
|
# (network errors are considered temporary, can't tell in this case
|
|
# that the name lookup error is permanent)
|
|
def checkDBusLog(self, content):
|
|
notifications = GrepNotifications(content)
|
|
self.assertEqual(notifications, [])
|
|
|
|
# done as part of post-processing in runTest()
|
|
self.runTestDBusCheck = checkDBusLog
|
|
|
|
@timeout(usingValgrind() and 200 or 60)
|
|
@property("ENV", "LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testAutoSyncNetworkFailure(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncNetworkFailure - test that auto-sync is triggered, fails due to (temporary?!) network error here"""
|
|
self.doAutoSyncNetworkFailure()
|
|
|
|
@timeout(usingValgrind() and 200 or 60)
|
|
@property("ENV", "DBUS_TEST_CONNMAN=session DBUS_TEST_NETWORK_MANAGER=session "
|
|
"LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testAutoSyncNoNetworkManager(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncNoNetworkManager - test that auto-sync is triggered despite having neither NetworkManager nor Connman, fails due to (temporary?!) network error here"""
|
|
self.doAutoSyncNetworkFailure()
|
|
|
|
@timeout(usingValgrind() and 200 or 60)
|
|
def doAutoSyncLocalConfigError(self, notifyLevel,
|
|
notification=
|
|
' string "SyncEvolution"\n'
|
|
' uint32 0\n'
|
|
' string ""\n'
|
|
' string "Sync problem."\n'
|
|
' string "Sorry, there\'s a problem with your sync that you need to attend to."\n'
|
|
' array [\n'
|
|
' string "view"\n'
|
|
' string "View"\n'
|
|
' string "default"\n'
|
|
' string "Dismiss"\n'
|
|
' ]\n'
|
|
' array [\n'
|
|
' ]\n'
|
|
' int32 -1\n'):
|
|
self.setupConfig()
|
|
# enable auto-sync
|
|
config = copy.deepcopy(self.config)
|
|
config[""]["syncURL"] = "local://@foobar" # will fail
|
|
config[""]["autoSync"] = "1"
|
|
config[""]["autoSyncDelay"] = "0"
|
|
config[""]["autoSyncInterval"] = "10s"
|
|
config[""]["password"] = "foobar"
|
|
if notifyLevel != 3:
|
|
config[""]["notifyLevel"] = str(notifyLevel)
|
|
self.session.SetConfig(True, False, config, utf8_strings=True)
|
|
|
|
def session_ready(object, ready):
|
|
if self.running and object != self.sessionpath and \
|
|
(self.auto_sync_session_path == None and ready or \
|
|
self.auto_sync_session_path == object):
|
|
self.auto_sync_session_path = object
|
|
DBusUtil.quit_events.append("session " + object + (ready and " ready" or " done"))
|
|
loop.quit()
|
|
|
|
signal = bus.add_signal_receiver(session_ready,
|
|
'SessionChanged',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
# shut down current session, will allow auto-sync
|
|
self.session.Detach()
|
|
|
|
# wait for start and end of auto-sync session
|
|
loop.run()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.auto_sync_session_path + " ready",
|
|
"session " + self.auto_sync_session_path + " done"])
|
|
session = dbus.Interface(bus.get_object(self.server.bus_name,
|
|
self.auto_sync_session_path),
|
|
'org.syncevolution.Session')
|
|
reports = session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(len(reports), 1)
|
|
self.assertEqual(reports[0]["status"], "10500")
|
|
name = session.GetConfigName()
|
|
self.assertEqual(name, "dummy-test")
|
|
flags = session.GetFlags()
|
|
self.assertEqual(flags, [])
|
|
|
|
# check that org.freedesktop.Notifications.Notify was called
|
|
# once to report the failed attempt to start the sync
|
|
def checkDBusLog(self, content):
|
|
notifications = GrepNotifications(content)
|
|
self.assertEqual(notifications,
|
|
notifyLevel >= 1 and [notification]
|
|
or [])
|
|
|
|
# check that no other session is started at the time of
|
|
# the next regular auto sync session
|
|
def testDone():
|
|
DBusUtil.quit_events.append("test done")
|
|
loop.quit()
|
|
return False
|
|
def any_session_ready(object, ready):
|
|
if self.running:
|
|
DBusUtil.quit_events.append("session " + object + (ready and " ready" or " done"))
|
|
loop.quit()
|
|
signal = bus.add_signal_receiver(any_session_ready,
|
|
'SessionChanged',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
try:
|
|
timeout = glib.timeout_add(15 * 1000, testDone)
|
|
loop.run()
|
|
finally:
|
|
glib.source_remove(timeout)
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.auto_sync_session_path + " ready",
|
|
"session " + self.auto_sync_session_path + " done",
|
|
"test done"])
|
|
|
|
# done as part of post-processing in runTest()
|
|
self.runTestDBusCheck = checkDBusLog
|
|
|
|
@timeout(usingValgrind() and 200 or 60)
|
|
@property("ENV", "LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testAutoSyncLocalConfigError(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncLocalConfigError - test that auto-sync is triggered for local sync, fails due to permanent config error here"""
|
|
self.doAutoSyncLocalConfigError(3)
|
|
|
|
@timeout(usingValgrind() and 200 or 60)
|
|
@property("ENV", "LC_ALL=de_DE.UTF-8 LANGUAGE=de_DE:de")
|
|
def testAutoSyncLocalConfigErrorGerman(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncLocalConfigErrorGerman - test that auto-sync is triggered for local sync, fails due to permanent config error here"""
|
|
self.doAutoSyncLocalConfigError(3,
|
|
' string "SyncEvolution"\n'
|
|
' uint32 0\n'
|
|
' string ""\n'
|
|
' string "Sync-Problem"\n'
|
|
' string "Ein Problem mit der Synchronisation bedarf deiner Aufmerksamkeit."\n'
|
|
' array [\n'
|
|
' string "view"\n'
|
|
' string "Anzeigen"\n'
|
|
' string "default"\n'
|
|
' string "Ignorieren"\n'
|
|
' ]\n'
|
|
' array [\n'
|
|
' ]\n'
|
|
' int32 -1\n')
|
|
|
|
@timeout(usingValgrind() and 200 or 60)
|
|
@property("ENV", "LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testAutoSyncLocalConfigErrorEssential(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncLocalConfigErrorEssential - test that auto-sync is triggered for local sync, fails due to permanent config error here, with only the essential error notification"""
|
|
self.doAutoSyncLocalConfigError(1)
|
|
|
|
@timeout(usingValgrind() and 200 or 60)
|
|
@property("ENV", "LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testAutoSyncLocalConfigErrorQuiet(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncLocalConfigErrorQuiet - test that auto-sync is triggered for local sync, fails due to permanent config error here, with no notification"""
|
|
self.doAutoSyncLocalConfigError(0)
|
|
|
|
def doAutoSyncLocalSuccess(self, notifyLevel, repeat=False,
|
|
afterSession=lambda step: 0,
|
|
atSessionChanged=lambda path, ready: 0):
|
|
# create @foobar config
|
|
self.session.Detach()
|
|
self.setUpSession("target-config@foobar")
|
|
config = copy.deepcopy(self.config)
|
|
config[""]["remoteDeviceId"] = "foo"
|
|
config[""]["deviceId"] = "bar"
|
|
del config[""]["password"]
|
|
for i in ("addressbook", "calendar", "todo", "memo"):
|
|
source = config["source/" + i]
|
|
source["database"] = source["database"] + ".server"
|
|
self.session.SetConfig(False, False, config, utf8_strings=True)
|
|
self.session.Detach()
|
|
|
|
# create dummy-test@default auto-sync config
|
|
self.setUpSession("dummy-test")
|
|
config = copy.deepcopy(self.config)
|
|
config[""]["syncURL"] = "local://@foobar"
|
|
config[""]["PeerIsClient"] = "1"
|
|
config[""]["autoSync"] = "1"
|
|
config[""]["autoSyncDelay"] = "0"
|
|
if notifyLevel != 3:
|
|
config[""]["notifyLevel"] = str(notifyLevel)
|
|
del config[""]["password"]
|
|
# must be small enough (otherwise test runs a long time)
|
|
# but not too small (otherwise the next sync already starts
|
|
# before we can check the result and kill the daemon)
|
|
autoSyncIntervalSeconds = usingValgrind() and 60 or 10
|
|
autoSyncIntervalAccuracy = 3
|
|
config[""]["autoSyncInterval"] = '%ds' % autoSyncIntervalSeconds
|
|
config["source/addressbook"]["uri"] = "addressbook"
|
|
self.session.SetConfig(False, False, config, utf8_strings=True)
|
|
|
|
def session_ready(object, ready):
|
|
if self.running and ready:
|
|
logging.printf('session ready')
|
|
session_ready.sessionStart.append(time.time())
|
|
if self.running and object != self.sessionpath and \
|
|
not atSessionChanged(object, ready) and \
|
|
(self.auto_sync_session_path == None and ready or \
|
|
self.auto_sync_session_path == object):
|
|
self.auto_sync_session_path = object
|
|
DBusUtil.quit_events.append("session " + object + (ready and " ready" or " done"))
|
|
loop.quit()
|
|
|
|
# Track time of "session started" signal. We need that when
|
|
# sessions get started concurrently, which affects our
|
|
# expected start time of the auto sync interval (see
|
|
# testAutoSyncLocalMultiple).
|
|
session_ready.sessionStart = []
|
|
|
|
signal = bus.add_signal_receiver(session_ready,
|
|
'SessionChanged',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
# shut down current session, will allow auto-sync
|
|
self.session.Detach()
|
|
afterSession(0)
|
|
|
|
# Remember when auto-sync countdown started.
|
|
session_ready.sessionStart.append(time.time())
|
|
|
|
# wait for start and end of auto-sync session
|
|
def run(operation, numSyncs):
|
|
logging.log(operation)
|
|
loop.run()
|
|
logging.printf('%s: session ready' % operation)
|
|
loop.run()
|
|
logging.printf('%s: session done' % operation)
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.auto_sync_session_path + " ready",
|
|
"session " + self.auto_sync_session_path + " done"])
|
|
session = dbus.Interface(bus.get_object(self.server.bus_name,
|
|
self.auto_sync_session_path),
|
|
'org.syncevolution.Session')
|
|
reports = session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(len(reports), numSyncs)
|
|
self.assertEqual(reports[0]["status"], "200")
|
|
name = session.GetConfigName()
|
|
self.assertEqual(name, "dummy-test")
|
|
flags = session.GetFlags()
|
|
self.assertEqual(flags, [])
|
|
DBusUtil.quit_events = []
|
|
self.auto_sync_session_path = None
|
|
|
|
run('waiting for first auto sync', 1)
|
|
afterSession(1)
|
|
|
|
# Actual delay should have been roughly the configured delay,
|
|
# give or take a few seconds.
|
|
self.assertLessEqual(2, len(session_ready.sessionStart))
|
|
self.assertLess(session_ready.sessionStart[-2], session_ready.sessionStart[-1])
|
|
self.assertAlmostEqual(session_ready.sessionStart[-1] - session_ready.sessionStart[-2], autoSyncIntervalSeconds,
|
|
delta=autoSyncIntervalAccuracy) # seconds
|
|
|
|
# check that org.freedesktop.Notifications.Notify was called
|
|
# when starting and completing the sync
|
|
def checkDBusLog(self, content):
|
|
notifications = GrepNotifications(content)
|
|
self.assertEqual(notifications,
|
|
notifyLevel >= 3 and
|
|
[' string "SyncEvolution"\n'
|
|
' uint32 0\n'
|
|
' string ""\n'
|
|
' string "dummy-test is syncing"\n'
|
|
' string "We have just started to sync your computer with the dummy-test sync service."\n'
|
|
' array [\n'
|
|
' string "view"\n'
|
|
' string "View"\n'
|
|
' string "default"\n'
|
|
' string "Dismiss"\n'
|
|
' ]\n'
|
|
' array [\n'
|
|
' ]\n'
|
|
' int32 -1\n',
|
|
|
|
' string "SyncEvolution"\n'
|
|
' uint32 0\n'
|
|
' string ""\n'
|
|
' string "dummy-test sync complete"\n'
|
|
' string "We have just finished syncing your computer with the dummy-test sync service."\n'
|
|
' array [\n'
|
|
' string "view"\n'
|
|
' string "View"\n'
|
|
' string "default"\n'
|
|
' string "Dismiss"\n'
|
|
' ]\n'
|
|
' array [\n'
|
|
' ]\n'
|
|
' int32 -1\n']
|
|
or [])
|
|
|
|
if repeat:
|
|
for i in range(1,3):
|
|
run('waiting for auto sync #%d' % i, i + 1)
|
|
self.assertLess(session_ready.sessionStart[-2], session_ready.sessionStart[-1])
|
|
self.assertAlmostEqual(session_ready.sessionStart[-1] - session_ready.sessionStart[-2], autoSyncIntervalSeconds,
|
|
delta=autoSyncIntervalAccuracy) # seconds
|
|
else:
|
|
# done as part of post-processing in runTest()
|
|
self.runTestDBusCheck = checkDBusLog
|
|
|
|
@timeout(usingValgrind() and 400 or 120)
|
|
@property("ENV", "LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testAutoSyncLocalSuccess(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncLocalSuccess - test that auto-sync is done successfully for local sync between file backends, with notifications"""
|
|
self.doAutoSyncLocalSuccess(3)
|
|
|
|
@timeout(usingValgrind() and 400 or 120)
|
|
@property("ENV", "LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testAutoSyncLocalSuccessQuiet(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncLocalSuccessQuiet - test that auto-sync is done successfully for local sync between file backends, without notifications"""
|
|
self.doAutoSyncLocalSuccess(1)
|
|
|
|
@timeout(usingValgrind() and 400 or 240)
|
|
@property("ENV", "LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testAutoSyncLocalMultiple(self):
|
|
"""TestSessionAPIsDummy.testAutoSyncLocalMultiple - test that auto-sync is done successfully for local sync between file backends, several times, despite additional work going on"""
|
|
|
|
# Simulate concurrent command line session as in FDO #73562.
|
|
# syncevo-dbus-server used to die when that happened.
|
|
class FakeConfigSession:
|
|
def __init__(self, server):
|
|
self.server = server
|
|
self.ran = None
|
|
|
|
def start(self):
|
|
self.ran = 0
|
|
logging.printf('intermediate session starting')
|
|
self.server.StartSessionWithFlags('dummy-test', ['no-sync'])
|
|
|
|
def atSessionStatusChange(self, status, error, sources):
|
|
if not status in ['queueing', 'idle', 'running']:
|
|
logging.printf('intermediate session stopping: %s, %d, %s', status, error, sources)
|
|
self.session.Detach()
|
|
self.ran = True
|
|
|
|
def atSessionChanged(self, path, ready):
|
|
if ready and self.ran == 0:
|
|
# This must be our session, even if StartSessionWithFlags()
|
|
# hasn't returned yet.
|
|
logging.printf('intermediate session ready')
|
|
self.session = dbus.Interface(bus.get_object(self.server.bus_name,
|
|
path),
|
|
'org.syncevolution.Session')
|
|
bus.add_signal_receiver(self.atSessionStatusChange,
|
|
'StatusChanged',
|
|
'org.syncevolution.Session',
|
|
self.server.bus_name,
|
|
path,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
self.session.Execute(['syncevolution', '-q', '--print-config', 'dummy-test'], [])
|
|
self.ran = 1
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def afterSession(self, step):
|
|
logging.printf('step %d' % step)
|
|
if step == 0:
|
|
# Read-only access to the auto-sync config in five seconds.
|
|
# Simulates a usage pattern which caused syncevo-dbus-server
|
|
# to abort when misusing the timeout callbacks (FDO #73562).
|
|
Timeout.addTimeout(5, self.start)
|
|
|
|
fakeConfigSession = FakeConfigSession(self.server)
|
|
|
|
self.doAutoSyncLocalSuccess(1, repeat=True,
|
|
afterSession=fakeConfigSession.afterSession,
|
|
atSessionChanged=fakeConfigSession.atSessionChanged)
|
|
self.assertEqual(fakeConfigSession.ran, True)
|
|
|
|
class TestSessionAPIsReal(DBusUtil, unittest.TestCase):
|
|
""" This class is used to test those unit tests of session APIs, depending on doing sync.
|
|
Thus we need a real server configuration to confirm sync could be run successfully.
|
|
Typically we need make sure that at least one sync has been done before testing our
|
|
desired unit tests. Note that it also covers session.Sync API itself """
|
|
""" All unit tests in this class have a dependency on a real sync
|
|
config named 'dbus_unittest'. That config must have preventSlowSync=0,
|
|
maxLogDirs=1, username, password set such that syncing succeeds
|
|
for at least one source. It does not matter which data is synchronized.
|
|
For example, the following config will work:
|
|
syncevolution --configure --template <server of your choice> \
|
|
username=<your username> \
|
|
password=<your password> \
|
|
preventSlowSync=0 \
|
|
maxLogDirs=1 \
|
|
backend=file \
|
|
database=file:///tmp/test_dbus_data \
|
|
databaseFormat=text/vcard \
|
|
dbus_unittest@test-dbus addressbook
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
self.setUpSession(configName)
|
|
self.operation = ""
|
|
|
|
def run(self, result):
|
|
self.runTest(result, own_xdg=False)
|
|
|
|
def setupConfig(self):
|
|
""" Apply for user settings. Used internally. """
|
|
configProps = { }
|
|
# check whether 'dbus_unittest' is configured.
|
|
try:
|
|
configProps = self.session.GetConfig(False, utf8_strings=True)
|
|
except dbus.DBusException, ex:
|
|
self.fail(str(ex) +
|
|
". To test this case, please first set up a correct config named 'dbus_unittest'.")
|
|
|
|
def doSync(self):
|
|
self.setupConfig()
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("slow", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
|
|
def progressChanged(self, *args, **keywords):
|
|
'''abort or suspend once session has progressed far enough'''
|
|
percentage = args[0]
|
|
# make sure sync is really running
|
|
if percentage > 20:
|
|
if self.operation == "abort":
|
|
self.session.Abort()
|
|
self.operation = "aborted"
|
|
if self.operation == "suspend":
|
|
self.session.Suspend()
|
|
self.operation = "suspended"
|
|
|
|
@timeout(300)
|
|
def testSync(self):
|
|
"""TestSessionAPIsReal.testSync - run a real sync with default server and test status list and progress number"""
|
|
""" check events list is correct for StatusChanged and ProgressChanged """
|
|
# do sync
|
|
self.doSync()
|
|
self.checkSync()
|
|
|
|
@timeout(300)
|
|
def testSyncStatusAbort(self):
|
|
"""TestSessionAPIsReal.testSyncStatusAbort - test status is set correctly when the session is aborted """
|
|
self.operation = "abort"
|
|
self.doSync()
|
|
hasAbortingStatus = False
|
|
for item in DBusUtil.events:
|
|
if item[0] == "status" and item[1][0] == "aborting":
|
|
hasAbortingStatus = True
|
|
break
|
|
self.assertEqual(hasAbortingStatus, True)
|
|
|
|
@timeout(300)
|
|
def testSyncStatusSuspend(self):
|
|
"""TestSessionAPIsReal.testSyncStatusSuspend - test status is set correctly when the session is suspended """
|
|
self.operation = "suspend"
|
|
self.doSync()
|
|
hasSuspendingStatus = False
|
|
for item in DBusUtil.events:
|
|
if item[0] == "status" and "suspending" in item[1][0] :
|
|
hasSuspendingStatus = True
|
|
break
|
|
self.assertEqual(hasSuspendingStatus, True)
|
|
|
|
@timeout(300)
|
|
def testSyncSecondSession(self):
|
|
"""TestSessionAPIsReal.testSyncSecondSession - ask for a second session that becomes ready after a real sync"""
|
|
sessionpath2, session2 = self.createSession("", False)
|
|
status, error, sources = session2.GetStatus(utf8_strings=True)
|
|
self.assertEqual(status, "queueing")
|
|
self.testSync()
|
|
# now wait for second session becoming ready
|
|
loop.run()
|
|
status, error, sources = session2.GetStatus(utf8_strings=True)
|
|
self.assertEqual(status, "idle")
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done",
|
|
"session " + sessionpath2 + " ready"])
|
|
session2.Detach()
|
|
|
|
class TestDBusSyncError(DBusUtil, unittest.TestCase):
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
self.setUpSession(configName)
|
|
|
|
def run(self, result):
|
|
self.runTest(result, own_xdg=True)
|
|
|
|
def testSyncNoConfig(self):
|
|
"""TestDBusSyncError.testSyncNoConfig - Executes a real sync with no corresponding config."""
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("", {})
|
|
loop.run()
|
|
# TODO: check recorded events in DBusUtil.events
|
|
status, error, sources = self.session.GetStatus(utf8_strings=True)
|
|
self.assertEqual(status, "done")
|
|
self.assertEqual(error, 10500)
|
|
|
|
class TestConnection(DBusUtil, unittest.TestCase):
|
|
"""Tests Server.Connect(). Tests depend on getting one Abort signal to terminate."""
|
|
|
|
"""a real message sent to our own server, DevInf stripped, username/password foo/bar"""
|
|
message1 = '''<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.2'><SyncHdr><VerDTD>1.2</VerDTD><VerProto>SyncML/1.2</VerProto><SessionID>255</SessionID><MsgID>1</MsgID><Target><LocURI>http://127.0.0.1:9000/syncevolution</LocURI></Target><Source><LocURI>sc-api-nat</LocURI><LocName>test</LocName></Source><Cred><Meta><Format xmlns='syncml:metinf'>b64</Format><Type xmlns='syncml:metinf'>syncml:auth-md5</Type></Meta><Data>kHzMn3RWFGWSKeBpXicppQ==</Data></Cred><Meta><MaxMsgSize xmlns='syncml:metinf'>20000</MaxMsgSize><MaxObjSize xmlns='syncml:metinf'>4000000</MaxObjSize></Meta></SyncHdr><SyncBody><Alert><CmdID>1</CmdID><Data>200</Data><Item><Target><LocURI>addressbook</LocURI></Target><Source><LocURI>./addressbook</LocURI></Source><Meta><Anchor xmlns='syncml:metinf'><Last>20091105T092757Z</Last><Next>20091105T092831Z</Next></Anchor><MaxObjSize xmlns='syncml:metinf'>4000000</MaxObjSize></Meta></Item></Alert><Final/></SyncBody></SyncML>'''
|
|
|
|
"""a real WBXML message, expected to trigger an authentication failure"""
|
|
message1WBXML = base64.b64decode('AqQBagBtbHEDMS4yAAFyA1N5bmNNTC8xLjIAAWUDMjExAAFbAzEAAW5XA2h0dHA6Ly9teS5mdW5hbWJvbC5jb20vc3luYwABAWdXA3NjLWFwaS1uYXQAAVYDcGF0cmljay5vaGx5AAEBTloAAUcDYjY0AAFTA3N5bmNtbDphdXRoLW1kNQABAQAATwNyT0dFbGR1Y2FjNE5mc3dZSm5lR2lnPT0AAQFaAAFMAzE1MDAwMAABVQM0MDAwMDAwAAEBAQAAa19LAzEAAVoAAVMDYXBwbGljYXRpb24vdm5kLnN5bmNtbC1kZXZpbmYrd2J4bWwAAQEAAFRnVwMuL2RldmluZjEyAAEBT8OMewKkA2oASmUDMS4yAAFRA1BhdHJpY2sgT2hseQABVQNTeW5jRXZvbHV0aW9uAAFWA1N5bnRoZXNpcyBBRwABTwMxLjIuOTkrMjAxMjAzMjcrU0UrMjI1MGVhMCt1bmNsZWFuAAFeAzMuNC4wLjM1AAFQA3Vua25vd24AAUkDc2MtYXBpLW5hdAABSwN3b3Jrc3RhdGlvbgABKCkqR10DLi9lZHNfZXZlbnQAAUwDZWRzX2V2ZW50AAFSAzY0AAFaRgN0ZXh0L2NhbGVuZGFyAAFkAzIuMAABAWJGA3RleHQvY2FsZW5kYXIAAWQDMi4wAAEBRUYDdGV4dC9jYWxlbmRhcgABZAMyLjAAAWtYA0JFR0lOAAFjA1ZDQUxFTkRBUgABYwNWVElNRVpPTkUAAWMDU1RBTkRBUkQAAWMDREFZTElHSFQAAWMDVlRPRE8AAWMDVkFMQVJNAAFjA1ZFVkVOVAABYwNWQUxBUk0AAQFrWANFTkQAAWMDVkNBTEVOREFSAAFjA1ZUSU1FWk9ORQABYwNTVEFOREFSRAABYwNEQVlMSUdIVAABYwNWVE9ETwABYwNWQUxBUk0AAWMDVkVWRU5UAAFjA1ZBTEFSTQABAWtYA1ZFUlNJT04AAWMDMi4wAAFtAzEAAQFrWANQUk9ESUQAAW0DMQABAWtYA1RaSUQAAQFrWANEVFNUQVJUAAEBa1gDUlJVTEUAAQFrWANUWk9GRlNFVEZST00AAQFrWANUWk9GRlNFVFRPAAEBa1gDVFpOQU1FAAEBa1gDTEFTVC1NT0RJRklFRAABbQMxAAEBa1gDRFRTVEFNUAABbQMxAAEBa1gDQ1JFQVRFRAABbQMxAAEBa1gDVUlEAAFtAzEAAQFrWANTRVFVRU5DRQABbQMxAAEBa1gDR0VPAAFtAzEAAQFrWANDQVRFR09SSUVTAAEBa1gDQ0xBU1MAAW0DMQABAWtYA1NVTU1BUlkAAW0DMQABAWtYA0RFU0NSSVBUSU9OAAFtAzEAAQFrWANMT0NBVElPTgABbQMxAAEBa1gDVVJMAAFtAzEAAQFrWANDT01QTEVURUQAAW0DMQABbFcDVFpJRAABAWxXA1ZBTFVFAAEBAWtYA0RVRQABbQMxAAFsVwNUWklEAAEBbFcDVkFMVUUAAQEBa1gDUFJJT1JJVFkAAW0DMQABAWtYA1NUQVRVUwABbQMxAAEBa1gDUEVSQ0VOVC1DT01QTEVURQABbQMxAAEBa1gDUkVMQVRFRC1UTwABbQMxAAFsVwNSRUxUWVBFAAFjA1BBUkVOVAABAQFrWANUUklHR0VSAAFtAzEAAWxXA1ZBTFVFAAEBbFcDUkVMQVRFRAABYwNTVEFSVAABYwNFTkQAAQEBa1gDQUNUSU9OAAFtAzEAAQFrWANSRVBFQVQAAW0DMQABAWtYA1gtRVZPTFVUSU9OLUFMQVJNLVVJRAABbQMxAAEBa1gDVFoAAW0DMQABAWtYA1RSQU5TUAABbQMxAAEBa1gDUkVDVVJSRU5DRS1JRAABbQMxAAFsVwNUWklEAAEBbFcDVkFMVUUAAQEBa1gDRVhEQVRFAAEBa1gDWC1TWU5DRVZPTFVUSU9OLUVYREFURS1ERVRBQ0hFRAABbFcDVFpJRAABAQFrWANEVEVORAABbQMxAAFsVwNUWklEAAEBbFcDVkFMVUUAAQEBa1gDRFVSQVRJT04AAW0DMQABAWtYA0FUVEVOREVFAAFsVwNDTgABAWxXA1BBUlRTVEFUAAFjA05FRURTLUFDVElPTgABYwNBQ0NFUFRFRAABYwNERUNMSU5FRAABYwNURU5UQVRJVkUAAWMDREVMRUdBVEVEAAEBbFcDUk9MRQABYwNDSEFJUgABYwNSRVEtUEFSVElDSVBBTlQAAWMDT1BULVBBUlRJQ0lQQU5UAAFjA05PTi1QQVJUSUNJUEFOVAABAWxXA1JTVlAAAWMDVFJVRQABYwNGQUxTRQABAWxXA0xBTkdVQUdFAAEBbFcDQ1VUWVBFAAFjA0lORElWSURVQUwAAWMDR1JPVVAAAWMDUkVTT1VSQ0UAAWMDUk9PTQABYwNVTktOT1dOAAEBAWtYA09SR0FOSVpFUgABbQMxAAFsVwNDTgABAQEBX2ADMQABYAMyAAFgAzMAAWADNAABYAM1AAFgAzYAAWADNwABYAM1NDQwMDEAAQEBAQEBAVNLAzIAAVoAAVMDYXBwbGljYXRpb24vdm5kLnN5bmNtbC1kZXZpbmYrd2J4bWwAAQEAAFRuVwMuL2RldmluZjEyAAEBAQFGSwMzAAFPAzIwMAABVG5XA2V2ZW50AAEBZ1cDLi9lZHNfZXZlbnQAAQFaAAFFSgMyMDEyMDMyOVQxMTAxMjZaAAFPAzIwMTIwMzI5VDExMDE1MloAAQFVAzQwMDAwMDAAAQEBAQAAEgEB')
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
self.setUpListeners(None)
|
|
# default config
|
|
self.config = {
|
|
"" : { "remoteDeviceId" : "sc-api-nat",
|
|
"password" : "test",
|
|
"username" : "test",
|
|
"PeerIsClient" : "1",
|
|
"RetryInterval" : "1",
|
|
"RetryDuration" : "10"
|
|
},
|
|
"source/addressbook" : { "sync" : "slow",
|
|
"backend" : "file",
|
|
"database" : "file://" + xdg_root + "/addressbook",
|
|
"databaseFormat" : "text/vcard",
|
|
"uri" : "card"
|
|
},
|
|
"source/calendar" : { "sync" : "disabled",
|
|
"backend" : "file",
|
|
"database" : "file://" + xdg_root + "/calendar",
|
|
"databaseFormat" : "text/calendar",
|
|
"uri" : "cal"
|
|
},
|
|
"source/todo" : { "sync" : "disabled",
|
|
"backend" : "file",
|
|
"database" : "file://" + xdg_root + "/todo",
|
|
"databaseFormat" : "text/calendar",
|
|
"uri" : "task"
|
|
},
|
|
"source/memo" : { "sync" : "disabled",
|
|
"backend" : "file",
|
|
"database" : "file://" + xdg_root + "/memo",
|
|
"databaseFormat" : "text/calendar",
|
|
"uri" : "text"
|
|
}
|
|
}
|
|
|
|
def setupConfig(self, name="dummy-test", deviceId="sc-api-nat", retryDuration=None):
|
|
self.setUpSession(name)
|
|
self.config[""]["remoteDeviceId"] = deviceId
|
|
if retryDuration is not None:
|
|
self.config[""]["RetryDuration"] = retryDuration
|
|
self.session.SetConfig(False, False, self.config, utf8_strings=True)
|
|
self.session.Detach()
|
|
# SyncEvolution <= 1.2.2 delayed the "Session.StatusChanged"
|
|
# "done" signal. The correct behavior is to send that
|
|
# important change right away.
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session done"])
|
|
DBusUtil.quit_events = []
|
|
|
|
def run(self, result):
|
|
self.runTest(result, own_xdg=True)
|
|
|
|
def getConnection(self, must_authenticate=False):
|
|
conpath = self.server.Connect({'description': 'test-dbus.py',
|
|
'transport': 'dummy'},
|
|
must_authenticate,
|
|
"")
|
|
self.setUpConnectionListeners(conpath)
|
|
connection = dbus.Interface(bus.get_object(self.server.bus_name,
|
|
conpath),
|
|
'org.syncevolution.Connection')
|
|
return (conpath, connection)
|
|
|
|
def testConnect(self):
|
|
"""TestConnection.testConnect - get connection and close it"""
|
|
conpath, connection = self.getConnection()
|
|
connection.Close(False, 'good bye')
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.events, [('abort',)])
|
|
|
|
def testInvalidConnect(self):
|
|
"""TestConnection.testInvalidConnect - get connection, send invalid initial message"""
|
|
self.setupConfig()
|
|
conpath, connection = self.getConnection()
|
|
try:
|
|
connection.Process('1234', 'invalid message type')
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.Exception: message type 'invalid message type' not supported for starting a sync")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
loop.run()
|
|
# 'idle' status doesn't be checked
|
|
self.assertIn(('abort',), DBusUtil.events)
|
|
|
|
@timeout(60)
|
|
def testStartSync(self):
|
|
"""TestConnection.testStartSync - send a valid initial SyncML message"""
|
|
self.setupConfig()
|
|
conpath, connection = self.getConnection()
|
|
connection.Process(TestConnection.message1, 'application/vnd.syncml+xml')
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " got reply"])
|
|
DBusUtil.quit_events = []
|
|
# TODO: check events
|
|
self.assertNotEqual(DBusUtil.reply, None)
|
|
self.assertEqual(DBusUtil.reply[1], 'application/vnd.syncml+xml')
|
|
# credentials should have been accepted because must_authenticate=False
|
|
# in Connect(); 508 = "refresh required" is normal
|
|
self.assertIn('<Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>addressbook</TargetRef><SourceRef>./addressbook</SourceRef><Data>508</Data>', DBusUtil.reply[0])
|
|
self.assertNotIn('<Chal>', DBusUtil.reply[0])
|
|
self.assertEqual(DBusUtil.reply[3], False)
|
|
self.assertNotEqual(DBusUtil.reply[4], '')
|
|
connection.Close(False, 'good bye')
|
|
loop.run()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " aborted",
|
|
"session done"])
|
|
# start another session for the server (ensures that the previous one is done),
|
|
# then check the server side report
|
|
DBusUtil.quit_events = []
|
|
self.setUpSession("dummy-test")
|
|
sessions = self.session.GetReports(0, 100)
|
|
self.assertEqual(len(sessions), 1)
|
|
# transport failure, only addressbook active and later aborted
|
|
self.assertEqual(sessions[0]["status"], "20043")
|
|
# Exact error string doesn't matter much, it is mostly internal.
|
|
if sessions[0]["error"] != "D-Bus peer has disconnected": # old error
|
|
self.assertEqual(sessions[0]["error"], "transport problem: send() on connection which is not ready")
|
|
self.assertEqual(sessions[0]["source-addressbook-status"], "20017")
|
|
# The other three sources are disabled and should not be listed in the
|
|
# report. Used to be listed with status 0 in the past, which would also
|
|
# be acceptable, but here we use the strict check for "not present" to
|
|
# ensure that the current behavior is preserved.
|
|
self.assertNotIn("source-calendar-status", sessions[0])
|
|
self.assertNotIn("source-todo-status", sessions[0])
|
|
self.assertNotIn("source-memo-status", sessions[0])
|
|
|
|
@timeout(60)
|
|
def testCredentialsWrong(self):
|
|
"""TestConnection.testCredentialsWrong - send invalid credentials"""
|
|
self.setupConfig()
|
|
conpath, connection = self.getConnection(must_authenticate=True)
|
|
connection.Process(TestConnection.message1, 'application/vnd.syncml+xml')
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " got reply"])
|
|
DBusUtil.quit_events = []
|
|
# TODO: check events
|
|
self.assertNotEqual(DBusUtil.reply, None)
|
|
self.assertEqual(DBusUtil.reply[1], 'application/vnd.syncml+xml')
|
|
# credentials should have been rejected because of wrong Nonce
|
|
self.assertIn('<Chal>', DBusUtil.reply[0])
|
|
self.assertEqual(DBusUtil.reply[3], False)
|
|
self.assertNotEqual(DBusUtil.reply[4], '')
|
|
# When the login fails, the server also ends the session and the connection;
|
|
# no more Connection.close() possible (could fail, depending on the timing).
|
|
loop.run()
|
|
loop.run()
|
|
DBusUtil.quit_events.sort()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " aborted",
|
|
"session done"])
|
|
|
|
@timeout(60)
|
|
def testCredentialsWrongWBXML(self):
|
|
"""TestConnection.testCredentialsWrongWBXML - send invalid credentials using WBXML"""
|
|
self.setupConfig()
|
|
conpath, connection = self.getConnection(must_authenticate=True)
|
|
connection.Process(TestConnection.message1WBXML, 'application/vnd.syncml+wbxml')
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " got reply"])
|
|
DBusUtil.quit_events = []
|
|
# TODO: check events
|
|
self.assertNotEqual(DBusUtil.reply, None)
|
|
self.assertEqual(DBusUtil.reply[1], 'application/vnd.syncml+wbxml')
|
|
# Credentials should have been rejected because of wrong Nonce.
|
|
# Impossible to check with WBXML...
|
|
# self.assertTrue('<Chal>' in DBusUtil.reply[0])
|
|
self.assertEqual(DBusUtil.reply[3], False)
|
|
self.assertNotEqual(DBusUtil.reply[4], '')
|
|
loop.run()
|
|
loop.run()
|
|
DBusUtil.quit_events.sort()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " aborted",
|
|
"session done"])
|
|
|
|
@timeout(60)
|
|
def testCredentialsRight(self):
|
|
"""TestConnection.testCredentialsRight - send correct credentials"""
|
|
self.setupConfig()
|
|
conpath, connection = self.getConnection(must_authenticate=True)
|
|
plain_auth = TestConnection.message1.replace("<Type xmlns='syncml:metinf'>syncml:auth-md5</Type></Meta><Data>kHzMn3RWFGWSKeBpXicppQ==</Data>",
|
|
"<Type xmlns='syncml:metinf'>syncml:auth-basic</Type></Meta><Data>dGVzdDp0ZXN0</Data>")
|
|
connection.Process(plain_auth, 'application/vnd.syncml+xml')
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " got reply"])
|
|
DBusUtil.quit_events = []
|
|
self.assertNotEqual(DBusUtil.reply, None)
|
|
self.assertEqual(DBusUtil.reply[1], 'application/vnd.syncml+xml')
|
|
# credentials should have been accepted because with basic auth,
|
|
# credentials can be replayed; 508 = "refresh required" is normal
|
|
self.assertIn('<Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>addressbook</TargetRef><SourceRef>./addressbook</SourceRef><Data>508</Data>', DBusUtil.reply[0])
|
|
self.assertEqual(DBusUtil.reply[3], False)
|
|
self.assertNotEqual(DBusUtil.reply[4], '')
|
|
connection.Close(False, 'good bye')
|
|
loop.run()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " aborted",
|
|
"session done"])
|
|
|
|
@timeout(usingValgrind() and 200 or 60)
|
|
def testStartSyncTwice(self):
|
|
"""TestConnection.testStartSyncTwice - send the same SyncML message twice, starting two sessions"""
|
|
self.setupConfig()
|
|
conpath, connection = self.getConnection()
|
|
connection.Process(TestConnection.message1, 'application/vnd.syncml+xml')
|
|
loop.run()
|
|
# TODO: check events
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " got reply"])
|
|
self.assertNotEqual(DBusUtil.reply, None)
|
|
self.assertEqual(DBusUtil.reply[1], 'application/vnd.syncml+xml')
|
|
self.assertEqual(DBusUtil.reply[3], False)
|
|
self.assertNotEqual(DBusUtil.reply[4], '')
|
|
DBusUtil.reply = None
|
|
DBusUtil.quit_events = []
|
|
|
|
# Now start another session with the same client *without*
|
|
# closing the first one. The server should detect this
|
|
# and forcefully close the first one.
|
|
conpath2, connection2 = self.getConnection()
|
|
connection2.Process(TestConnection.message1, 'application/vnd.syncml+xml')
|
|
|
|
# reasons for leaving the loop, in random order:
|
|
# - abort of first connection
|
|
# - first session done
|
|
# - reply for second one
|
|
loop.run()
|
|
loop.run()
|
|
loop.run()
|
|
DBusUtil.quit_events.sort()
|
|
expected = [ "connection " + conpath + " aborted",
|
|
"session done",
|
|
"connection " + conpath2 + " got reply" ]
|
|
expected.sort()
|
|
self.assertEqual(DBusUtil.quit_events, expected)
|
|
self.assertNotEqual(DBusUtil.reply, None)
|
|
self.assertEqual(DBusUtil.reply[1], 'application/vnd.syncml+xml')
|
|
self.assertEqual(DBusUtil.reply[3], False)
|
|
self.assertNotEqual(DBusUtil.reply[4], '')
|
|
DBusUtil.quit_events = []
|
|
|
|
# now quit for good
|
|
connection2.Close(False, 'good bye')
|
|
loop.run()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath2 + " aborted",
|
|
"session done"])
|
|
|
|
@timeout(usingValgrind() and 120 or 60)
|
|
def testKillInactive(self):
|
|
"""TestConnection.testKillInactive - block server with client A, then let client B connect twice"""
|
|
#set up 2 configs
|
|
self.setupConfig(retryDuration="120")
|
|
self.setupConfig("dummy", "sc-pim-ppc", retryDuration="120")
|
|
conpath, connection = self.getConnection()
|
|
connection.Process(TestConnection.message1, 'application/vnd.syncml+xml')
|
|
loop.run()
|
|
# TODO: check events
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " got reply"])
|
|
self.assertNotEqual(DBusUtil.reply, None)
|
|
self.assertEqual(DBusUtil.reply[1], 'application/vnd.syncml+xml')
|
|
self.assertEqual(DBusUtil.reply[3], False)
|
|
self.assertNotEqual(DBusUtil.reply[4], '')
|
|
DBusUtil.reply = None
|
|
DBusUtil.quit_events = []
|
|
|
|
# Now start two more sessions with the second client *without*
|
|
# closing the first one. The server should remove only the
|
|
# first connection of client B.
|
|
message1_clientB = TestConnection.message1.replace("sc-api-nat", "sc-pim-ppc")
|
|
conpath2, connection2 = self.getConnection()
|
|
connection2.Process(message1_clientB, 'application/vnd.syncml+xml')
|
|
conpath3, connection3 = self.getConnection()
|
|
connection3.Process(message1_clientB, 'application/vnd.syncml+xml')
|
|
# Queueing session for connection2 done now.
|
|
loop.run()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath2 + " aborted",
|
|
"session done"])
|
|
DBusUtil.quit_events = []
|
|
|
|
# now quit for good
|
|
connection3.Close(False, 'good bye client B')
|
|
loop.run()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath3 + " aborted",
|
|
"session done"])
|
|
DBusUtil.quit_events = []
|
|
connection.Close(False, 'good bye client A')
|
|
loop.run()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " aborted",
|
|
"session done"])
|
|
|
|
@timeout(60)
|
|
def testTimeoutSync(self):
|
|
"""TestConnection.testTimeoutSync - start a sync, then wait for server to detect that we stopped replying"""
|
|
|
|
# The server-side configuration for sc-api-nat must contain a retryDuration=10
|
|
# because this test itself will time out with a failure after 60 seconds.
|
|
self.setupConfig()
|
|
conpath, connection = self.getConnection()
|
|
connection.Process(TestConnection.message1, 'application/vnd.syncml+xml')
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " got reply"])
|
|
DBusUtil.quit_events = []
|
|
# TODO: check events
|
|
self.assertNotEqual(DBusUtil.reply, None)
|
|
self.assertEqual(DBusUtil.reply[1], 'application/vnd.syncml+xml')
|
|
# wait for connection reset and "session done" due to timeout
|
|
loop.run()
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["connection " + conpath + " aborted",
|
|
"session done"])
|
|
|
|
class TestMultipleConfigs(unittest.TestCase, DBusUtil):
|
|
""" sharing of properties between configs
|
|
|
|
Creates and tests the configs 'foo', 'bar', 'foo@other_context',
|
|
'@default' and checks that 'defaultPeer' (global), 'syncURL' (per
|
|
peer), 'database' (per source), 'uri' (per source and peer)
|
|
are shared correctly.
|
|
|
|
Runs with a the server ready, without session."""
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
|
|
def run(self, result):
|
|
self.runTest(result)
|
|
|
|
def setupEmpty(self):
|
|
"""Creates empty configs 'foo', 'bar', 'foo@other_context'.
|
|
Updating non-existant configs is an error. Use this
|
|
function before trying to update one of these configs."""
|
|
self.setUpSession("foo")
|
|
self.session.SetConfig(False, False, {"" : {}})
|
|
self.session.Detach()
|
|
self.setUpSession("bar")
|
|
self.session.SetConfig(False, False, {"": {}})
|
|
self.session.Detach()
|
|
self.setUpSession("foo@other_CONTEXT")
|
|
self.session.SetConfig(False, False, {"": {}})
|
|
self.session.Detach()
|
|
|
|
def setupConfigs(self):
|
|
"""Creates polulated configs 'foo', 'bar', 'foo@other_context'."""
|
|
self.setupEmpty()
|
|
|
|
# update normal view on "foo"
|
|
self.setUpSession("foo")
|
|
self.session.SetConfig(True, False,
|
|
{ "" : { "defaultPeer" : "foobar_peer",
|
|
"deviceId" : "shared-device-identifier",
|
|
"syncURL": "http://scheduleworld" },
|
|
"source/calendar" : { "uri" : "cal3" },
|
|
"source/addressbook" : { "database": "Personal",
|
|
"sync" : "two-way",
|
|
"uri": "card3" } },
|
|
utf8_strings=True)
|
|
self.session.Detach()
|
|
|
|
# "bar" shares properties with "foo"
|
|
self.setUpSession("bar")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertEqual(config[""]["deviceId"], "shared-device-identifier")
|
|
self.assertEqual(config["source/addressbook"]["database"], "Personal")
|
|
self.session.SetConfig(True, False,
|
|
{ "" : { "syncURL": "http://funambol" },
|
|
"source/calendar" : { "uri" : "cal" },
|
|
"source/addressbook" : { "database": "Work",
|
|
"sync" : "refresh-from-client",
|
|
"uri": "card" } },
|
|
utf8_strings=True)
|
|
self.session.Detach()
|
|
|
|
def testSharing(self):
|
|
"""TestMultipleConfigs.testSharing - set up configs and tests reading them"""
|
|
self.setupConfigs()
|
|
|
|
# check how view "foo" has been modified
|
|
self.setUpSession("Foo@deFAULT")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertEqual(config[""]["syncURL"], "http://scheduleworld")
|
|
self.assertEqual(config["source/addressbook"]["database"], "Work")
|
|
self.assertEqual(config["source/addressbook"]["uri"], "card3")
|
|
self.session.Detach()
|
|
|
|
# different ways of addressing this context
|
|
self.setUpSession("")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertIn("source/addressbook", config)
|
|
self.assertNotIn("uri", config["source/addressbook"])
|
|
self.session.Detach()
|
|
|
|
self.setUpSession("@DEFAULT")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertEqual(config[""]["deviceId"], "shared-device-identifier")
|
|
self.assertIn("source/addressbook", config)
|
|
self.assertNotIn("uri", config["source/addressbook"])
|
|
self.session.Detach()
|
|
|
|
# different context
|
|
self.setUpSession("@other_context")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertNotIn("source/addressbook", config)
|
|
self.session.Detach()
|
|
|
|
def testSharedTemplate(self):
|
|
"""TestMultipleConfigs.testSharedTemplate - templates must contain shared properties"""
|
|
self.setupConfigs()
|
|
|
|
config = self.server.GetConfig("scheduleworld", True, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertEqual(config[""]["deviceId"], "shared-device-identifier")
|
|
self.assertEqual(config["source/addressbook"]["database"], "Work")
|
|
|
|
def testSharedProperties(self):
|
|
"""TestMultipleConfigs.testSharedProperties - 'type' consists of per-peer and shared properties"""
|
|
self.setupConfigs()
|
|
|
|
# writing for peer modifies properties in "foo" and context
|
|
self.setUpSession("Foo@deFAULT")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
config["source/addressbook"]["syncFormat"] = "text/vcard"
|
|
config["source/addressbook"]["backend"] = "file"
|
|
config["source/addressbook"]["databaseFormat"] = "text/x-vcard"
|
|
self.session.SetConfig(True, False,
|
|
config,
|
|
utf8_strings=True)
|
|
config = self.server.GetConfig("Foo", False, utf8_strings=True)
|
|
self.assertEqual(config["source/addressbook"]["syncFormat"], "text/vcard")
|
|
config = self.server.GetConfig("@default", False, utf8_strings=True)
|
|
self.assertEqual(config["source/addressbook"]["backend"], "file")
|
|
self.assertEqual(config["source/addressbook"]["databaseFormat"], "text/x-vcard")
|
|
self.session.Detach()
|
|
|
|
def testSharedPropertyOther(self):
|
|
"""TestMultipleConfigs.testSharedPropertyOther - shared backend properties must be preserved when adding peers"""
|
|
# writing peer modifies properties in "foo" and creates context "@other"
|
|
self.setUpSession("Foo@other")
|
|
config = self.server.GetConfig("ScheduleWorld@other", True, utf8_strings=True)
|
|
config["source/addressbook"]["backend"] = "file"
|
|
config["source/addressbook"]["databaseFormat"] = "text/x-vcard"
|
|
self.session.SetConfig(False, False,
|
|
config,
|
|
utf8_strings=True)
|
|
config = self.server.GetConfig("Foo", False, utf8_strings=True)
|
|
self.assertEqual(config["source/addressbook"]["backend"], "file")
|
|
config = self.server.GetConfig("@other", False, utf8_strings=True)
|
|
self.assertEqual(config["source/addressbook"]["databaseFormat"], "text/x-vcard")
|
|
self.session.Detach()
|
|
|
|
# adding second client must preserve backend value
|
|
self.setUpSession("bar@other")
|
|
config = self.server.GetConfig("Funambol@other", True, utf8_strings=True)
|
|
self.assertEqual(config["source/addressbook"]["backend"], "file")
|
|
self.session.SetConfig(False, False,
|
|
config,
|
|
utf8_strings=True)
|
|
config = self.server.GetConfig("bar", False, utf8_strings=True)
|
|
self.assertEqual(config["source/addressbook"]["backend"], "file")
|
|
self.assertEqual(config["source/addressbook"].get("syncFormat"), None)
|
|
config = self.server.GetConfig("@other", False, utf8_strings=True)
|
|
self.assertEqual(config["source/addressbook"]["databaseFormat"], "text/x-vcard")
|
|
|
|
def testOtherContext(self):
|
|
"""TestMultipleConfigs.testOtherContext - write into independent context"""
|
|
self.setupConfigs()
|
|
|
|
# write independent "foo@other_context" config
|
|
self.setUpSession("foo@other_context")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
config[""]["syncURL"] = "http://scheduleworld2"
|
|
config["source/addressbook"] = { "database": "Play",
|
|
"uri": "card30" }
|
|
self.session.SetConfig(True, False,
|
|
config,
|
|
utf8_strings=True)
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertEqual(config[""]["syncURL"], "http://scheduleworld2")
|
|
self.assertEqual(config["source/addressbook"]["database"], "Play")
|
|
self.assertEqual(config["source/addressbook"]["uri"], "card30")
|
|
self.session.Detach()
|
|
|
|
# "foo" modified?
|
|
self.setUpSession("foo")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertEqual(config[""]["syncURL"], "http://scheduleworld")
|
|
self.assertEqual(config["source/addressbook"]["database"], "Work")
|
|
self.assertEqual(config["source/addressbook"]["uri"], "card3")
|
|
self.session.Detach()
|
|
|
|
def testSourceRemovalLocal(self):
|
|
"""TestMultipleConfigs.testSourceRemovalLocal - remove 'addressbook' source in 'foo'"""
|
|
self.setupConfigs()
|
|
self.setUpSession("foo")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
del config["source/addressbook"]
|
|
self.session.SetConfig(False, False, config, utf8_strings=True)
|
|
self.session.Detach()
|
|
|
|
# "addressbook" still exists in "foo" but only with default values
|
|
config = self.server.GetConfig("foo", False, utf8_strings=True)
|
|
self.assertNotIn("uri", config["source/addressbook"])
|
|
self.assertNotIn("sync", config["source/addressbook"])
|
|
|
|
# "addressbook" unchanged in "bar"
|
|
config = self.server.GetConfig("bar", False, utf8_strings=True)
|
|
self.assertEqual(config["source/addressbook"]["uri"], "card")
|
|
self.assertEqual(config["source/addressbook"]["sync"], "refresh-from-client")
|
|
|
|
def testSourceRemovalGlobal(self):
|
|
"""TestMultipleConfigs.testSourceRemovalGlobal - remove "addressbook" everywhere"""
|
|
self.setupConfigs()
|
|
self.setUpSession("")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
del config["source/addressbook"]
|
|
self.session.SetConfig(False, False, config, utf8_strings=True)
|
|
self.session.Detach()
|
|
|
|
# "addressbook" gone in "foo" and "bar"
|
|
config = self.server.GetConfig("foo", False, utf8_strings=True)
|
|
self.assertNotIn("source/addressbook", config)
|
|
config = self.server.GetConfig("bar", False, utf8_strings=True)
|
|
self.assertNotIn("source/addressbook", config)
|
|
|
|
def testRemovePeer(self):
|
|
"""TestMultipleConfigs.testRemovePeer - check listing of peers while removing 'bar'"""
|
|
self.setupConfigs()
|
|
self.testOtherContext()
|
|
self.setUpSession("bar")
|
|
peers = self.session.GetConfigs(False, utf8_strings=True)
|
|
self.assertEqual(peers,
|
|
[ "bar", "foo", "foo@other_context" ])
|
|
peers2 = self.server.GetConfigs(False, utf8_strings=True)
|
|
self.assertEqual(peers, peers2)
|
|
# remove "bar"
|
|
self.session.SetConfig(False, False, {}, utf8_strings=True)
|
|
peers = self.server.GetConfigs(False, utf8_strings=True)
|
|
self.assertEqual(peers,
|
|
[ "foo", "foo@other_context" ])
|
|
self.session.Detach()
|
|
|
|
# other configs should not have been affected
|
|
config = self.server.GetConfig("foo", False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertEqual(config[""]["syncURL"], "http://scheduleworld")
|
|
self.assertEqual(config["source/calendar"]["uri"], "cal3")
|
|
config = self.server.GetConfig("foo@other_context", False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertEqual(config[""]["syncURL"], "http://scheduleworld2")
|
|
self.assertEqual(config["source/addressbook"]["database"], "Play")
|
|
self.assertEqual(config["source/addressbook"]["uri"], "card30")
|
|
|
|
def testRemoveContext(self):
|
|
"""TestMultipleConfigs.testRemoveContext - remove complete config"""
|
|
self.setupConfigs()
|
|
self.setUpSession("")
|
|
self.session.SetConfig(False, False, {}, utf8_strings=True)
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
peers = self.server.GetConfigs(False, utf8_strings=True)
|
|
self.assertEqual(peers, ['foo@other_context'])
|
|
self.session.Detach()
|
|
|
|
def testTemplates(self):
|
|
"""TestMultipleConfigs.testTemplates - templates reuse common properties"""
|
|
self.setupConfigs()
|
|
|
|
# deviceID must be shared and thus be reused in templates
|
|
self.setUpSession("")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
config[""]["DEVICEID"] = "shared-device-identifier"
|
|
self.session.SetConfig(True, False, config, utf8_strings=True)
|
|
config = self.server.GetConfig("", False, utf8_strings=True)
|
|
self.assertEqual(config[""]["deviceId"], "shared-device-identifier")
|
|
|
|
# get template for default context
|
|
config = self.server.GetConfig("scheduleworld", True, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertEqual(config[""]["deviceId"], "shared-device-identifier")
|
|
|
|
# now for @other_context - different device ID!
|
|
config = self.server.GetConfig("scheduleworld@other_context", True, utf8_strings=True)
|
|
self.assertEqual(config[""]["defaultPeer"], "foobar_peer")
|
|
self.assertNotEqual(config[""]["deviceId"], "shared-device-identifier")
|
|
|
|
def testCredentials(self):
|
|
"""TestMultipleConfigs.testCredentials - test storing username/password"""
|
|
|
|
self.setupConfigs()
|
|
|
|
# Store username/password in config 'foo' without using keyring.
|
|
# We want this test to work in all cases.
|
|
self.setUpSession("foo")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
config[""]["keyring"] = "no"
|
|
config[""]["username"] = "john"
|
|
config[""]["password"] = "doe-pwd"
|
|
self.session.SetConfig(True, False, config)
|
|
self.session.Detach()
|
|
|
|
# Retrieve username/password.
|
|
self.setUpSession("foo")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["username"], "john")
|
|
self.assertEqual(config[""]["password"], "doe-pwd")
|
|
self.session.Detach()
|
|
|
|
# Re-use credentials in second config.
|
|
self.setUpSession("bar")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
config[""]["username"] = "id:foo"
|
|
self.assertNotIn("password", config[""])
|
|
self.session.SetConfig(True, False, config)
|
|
self.session.Detach()
|
|
|
|
# We expect to see the actual "username" value from "bar"
|
|
# here, not the one from "foo". When running a sync, the
|
|
# credentials from "foo" must be used, which needs to be
|
|
# tested in configurations used by the nightly testing.
|
|
#
|
|
# The "password" however should come from "foo", to allow the
|
|
# user to edit it. This is consistent with showing the
|
|
# password after retrieving it from a keyring.
|
|
self.setUpSession("bar")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["username"], "id:foo")
|
|
self.assertEqual(config[""]["password"], "doe-pwd")
|
|
|
|
# When changing passwords via "bar", the actual writes need
|
|
# to go to "foo".
|
|
config[""]["password"] = "doe-pwd-2"
|
|
self.session.SetConfig(True, False, config)
|
|
self.session.Detach()
|
|
|
|
# Check in "foo".
|
|
self.setUpSession("foo")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["username"], "john")
|
|
self.assertEqual(config[""]["password"], "doe-pwd-2")
|
|
self.session.Detach()
|
|
|
|
# Check in "bar".
|
|
self.setUpSession("bar")
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["username"], "id:foo")
|
|
self.assertEqual(config[""]["password"], "doe-pwd-2")
|
|
|
|
# Now try it also with proxy and database crdentials.
|
|
config[""]["proxyUsername"] = "id:foo"
|
|
config[""]["useProxy"] = "1"
|
|
config["source/calendar"]["databaseUser"] = "id:foo"
|
|
self.session.SetConfig(True, False, config)
|
|
|
|
config = self.session.GetConfig(False, utf8_strings=True)
|
|
self.assertEqual(config[""]["proxyUsername"], "id:foo")
|
|
self.assertEqual(config[""]["proxyPassword"], "doe-pwd-2")
|
|
self.assertEqual(config["source/calendar"]["databaseUser"], "id:foo")
|
|
self.assertEqual(config["source/calendar"]["databasePassword"], "doe-pwd-2")
|
|
|
|
class TestLocalSync(unittest.TestCase, DBusUtil):
|
|
"""Tests involving local sync."""
|
|
|
|
def run(self, result):
|
|
self.runTest(result)
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
|
|
def setUpConfigs(self, childPassword=None):
|
|
self.setUpLocalSyncConfigs(childPassword)
|
|
|
|
@timeout(100)
|
|
def testSync(self):
|
|
"""TestLocalSync.testSync - run a simple slow sync between local dirs"""
|
|
self.setUpConfigs()
|
|
os.makedirs(xdg_root + "/server")
|
|
output = open(xdg_root + "/server/0", "w")
|
|
output.write('''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:John Doe
|
|
N:Doe;John
|
|
END:VCARD''')
|
|
output.close()
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("slow", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
self.checkSync()
|
|
input = open(xdg_root + "/client/0", "r")
|
|
self.assertIn("FN:John Doe", input.read())
|
|
input = open(xdg_root + "/server/0", "r")
|
|
self.assertIn("FN:John Doe", input.read())
|
|
|
|
def setUpInfoRequest(self, response={"password" : "123456"}):
|
|
self.lastState = "unknown"
|
|
def infoRequest(id, session, state, handler, type, params):
|
|
if state == "request":
|
|
self.assertEqual(self.lastState, "unknown")
|
|
self.lastState = "request"
|
|
if response != None:
|
|
self.server.InfoResponse(id, "working", {}, utf8_strings=True)
|
|
elif state == "waiting":
|
|
self.assertEqual(self.lastState, "request")
|
|
self.lastState = "waiting"
|
|
if response != None:
|
|
self.server.InfoResponse(id, "response", response, utf8_strings=True)
|
|
elif state == "done":
|
|
self.assertEqual(self.lastState, "waiting")
|
|
self.lastState = "done"
|
|
else:
|
|
self.fail("state should not be '" + state + "'")
|
|
|
|
signal = bus.add_signal_receiver(infoRequest,
|
|
'InfoRequest',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
return signal
|
|
|
|
@timeout(100)
|
|
def testPasswordRequest(self):
|
|
"""TestLocalSync.testPasswordRequest - check that password request child->parent->us works"""
|
|
self.setUpConfigs(childPassword="-")
|
|
self.setUpListeners(self.sessionpath)
|
|
signal = self.setUpInfoRequest()
|
|
try:
|
|
self.session.Sync("slow", {})
|
|
loop.run()
|
|
finally:
|
|
signal.remove()
|
|
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
self.assertEqual(self.lastState, "done")
|
|
self.checkSync()
|
|
self.assertSyncStatus('server', 200, None)
|
|
|
|
@timeout(100)
|
|
def testPasswordRequestAbort(self):
|
|
"""TestLocalSync.testPasswordRequestAbort - let user cancel password request"""
|
|
self.setUpConfigs(childPassword="-")
|
|
self.setUpListeners(self.sessionpath)
|
|
signal = self.setUpInfoRequest(response={})
|
|
try:
|
|
self.session.Sync("slow", {})
|
|
loop.run()
|
|
finally:
|
|
signal.remove()
|
|
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
self.assertEqual(self.lastState, "done")
|
|
self.checkSync(expectedError=20017, expectedResult=20017)
|
|
self.assertSyncStatus('server', 20017, "error code from SyncEvolution aborted on behalf of user (local, status 20017): User did not provide the 'addressbook backend' password.")
|
|
|
|
@timeout(200)
|
|
def testPasswordRequestTimeout(self):
|
|
"""TestLocalSync.testPasswordRequestTimeout - let password request time out"""
|
|
self.setUpConfigs(childPassword="-")
|
|
self.setUpListeners(self.sessionpath)
|
|
signal = self.setUpInfoRequest(response=None)
|
|
try:
|
|
# Will time out roughly after 120 seconds.
|
|
start = time.time()
|
|
self.session.Sync("slow", {})
|
|
loop.run()
|
|
finally:
|
|
signal.remove()
|
|
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
self.assertEqual(self.lastState, "request")
|
|
self.checkSync(expectedError=22003, expectedResult=22003)
|
|
self.assertSyncStatus('server', 22003, "error code from SyncEvolution password request timed out (local, status 22003): Could not get the 'addressbook backend' password from user.")
|
|
end = time.time()
|
|
self.assertTrue(abs(120 + (usingValgrind() and 20 or 0) -
|
|
(end - start)) <
|
|
(usingValgrind() and 60 or 20))
|
|
|
|
|
|
# Killing the syncevo-dbus-helper before it even starts (SYNCEVOLUTION_LOCAL_CHILD_DELAY=5)
|
|
# is possible, but leads to ugly valgrind warnings about "possibly lost" memory because
|
|
# SIGTERM really kills the process right away. Better wait until syncing really has started
|
|
# in the helper (SYNCEVOLUTION_SYNC_DELAY). The test is more realistic that way, too.
|
|
# The delay itself can be aborted, which is why this test will complete fairly quickly
|
|
# despite that delay.
|
|
@property("ENV", "SYNCEVOLUTION_SYNC_DELAY=1000")
|
|
@timeout(100)
|
|
def testConcurrency(self):
|
|
"""TestLocalSync.testConcurrency - D-Bus server must remain responsive while sync runs"""
|
|
self.setUpConfigs()
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("slow", {})
|
|
|
|
self.aborted = False
|
|
def output(path, level, text, procname):
|
|
if self.running and not self.aborted and text == 'ready to sync':
|
|
logging.printf('aborting sync')
|
|
self.session.Abort()
|
|
self.aborted = True
|
|
|
|
receiver = bus.add_signal_receiver(output,
|
|
'LogOutput',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
try:
|
|
loop.run()
|
|
finally:
|
|
receiver.remove()
|
|
self.assertTrue(self.aborted)
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
report = self.checkSync(20017, 20017, reportOptional=True) # aborted, with or without report
|
|
if report:
|
|
self.assertNotIn("error", report) # ... but without error message
|
|
self.assertEqual(report["source-addressbook-status"], "0") # unknown status for source (aborted early)
|
|
|
|
@timeout(200)
|
|
@property("ENV", "LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testParentFailure(self):
|
|
"""TestLocalSync.testParentFailure - check that server and local sync helper detect when D-Bus helper dies"""
|
|
self.setUpConfigs(childPassword="-")
|
|
self.setUpListeners(self.sessionpath)
|
|
self.lastState = "unknown"
|
|
def infoRequest(id, session, state, handler, type, params):
|
|
if state == "request":
|
|
self.assertEqual(self.lastState, "unknown")
|
|
self.lastState = "request"
|
|
# kill syncevo-dbus-helper
|
|
for pid, (name, cmdline) in self.getChildren().iteritems():
|
|
if 'syncevo-dbus-helper' in cmdline:
|
|
logging.printf('killing syncevo-dbus-helper with pid %d', pid)
|
|
os.kill(pid, signal.SIGKILL)
|
|
break
|
|
dbusSignal = bus.add_signal_receiver(infoRequest,
|
|
'InfoRequest',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
try:
|
|
self.session.Sync("slow", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
self.checkSync(expectedError=10500, expectedResult=22002)
|
|
finally:
|
|
dbusSignal.remove()
|
|
|
|
# kill syncevo-dbus-server (and not the wrapper, because that would
|
|
# also kill the process group)
|
|
pid = self.serverPid()
|
|
logging.printf('killing syncevo-dbus-server with pid %d', pid)
|
|
os.kill(pid, signal.SIGKILL)
|
|
|
|
# Give syncevo-local-sync some time to shut down.
|
|
time.sleep(usingValgrind() and 60 or 10)
|
|
|
|
# Remove syncevo-dbus-server zombie process(es).
|
|
DBusUtil.pserver.wait()
|
|
DBusUtil.pserver = None
|
|
try:
|
|
while True:
|
|
res = os.waitpid(-1, os.WNOHANG)
|
|
if res[0]:
|
|
logging.printf('got status %d for pid %d', res[1], res[0])
|
|
else:
|
|
break
|
|
except OSError, ex:
|
|
if ex.errno != errno.ECHILD:
|
|
raise ex
|
|
|
|
# Now no processes should be left in the process group
|
|
# of the syncevo-dbus-server.
|
|
self.assertEqual({}, self.getChildren())
|
|
|
|
# check status reports
|
|
self.assertSyncStatus('server', 22002, 'synchronization process died prematurely')
|
|
# Local sync helper cannot tell for sure what happened.
|
|
# A generic error is okay. If the ForkExecChild::m_onQuit signal is
|
|
# triggered first, it'll report the "aborted" error.
|
|
status, error = self.getSyncStatus('remote@client')
|
|
self.assertTrue(status in (10500, 20017))
|
|
self.assertTrue(error in ('retrieving password failed: The connection is closed',
|
|
'sync parent quit unexpectedly'))
|
|
|
|
@timeout(200)
|
|
@property("ENV", "LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
def testChildFailure(self):
|
|
"""TestLocalSync.testChildFailure - check that server and D-Bus sync helper detect when local sync helper dies"""
|
|
self.setUpConfigs(childPassword="-")
|
|
self.setUpListeners(self.sessionpath)
|
|
self.lastState = "unknown"
|
|
def infoRequest(id, session, state, handler, type, params):
|
|
if state == "request":
|
|
self.assertEqual(self.lastState, "unknown")
|
|
self.lastState = "request"
|
|
# kill syncevo-dbus-helper
|
|
for pid, (name, cmdline) in self.getChildren().iteritems():
|
|
if 'syncevo-local-sync' in cmdline:
|
|
logging.printf('killing syncevo-local-sync with pid %d', pid)
|
|
os.kill(pid, signal.SIGKILL)
|
|
break
|
|
dbusSignal = bus.add_signal_receiver(infoRequest,
|
|
'InfoRequest',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
try:
|
|
self.session.Sync("slow", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
# TODO: this shouldn't be 20043 'external transport failure'
|
|
self.checkSync(expectedError=20043, expectedResult=20043)
|
|
finally:
|
|
dbusSignal.remove()
|
|
|
|
# kill syncevo-dbus-server (and not the wrapper, because that would
|
|
# also kill the process group)
|
|
pid = self.serverPid()
|
|
logging.printf('killing syncevo-dbus-server with pid %d', pid)
|
|
os.kill(pid, signal.SIGKILL)
|
|
|
|
# Give syncevo-local-sync some time to shut down.
|
|
time.sleep(usingValgrind() and 60 or 10)
|
|
|
|
# Remove syncevo-dbus-server zombie process(es).
|
|
DBusUtil.pserver.wait()
|
|
DBusUtil.pserver = None
|
|
try:
|
|
while True:
|
|
res = os.waitpid(-1, os.WNOHANG)
|
|
if res[0]:
|
|
logging.printf('got status %d for pid %d', res[1], res[0])
|
|
else:
|
|
break
|
|
except OSError, ex:
|
|
if ex.errno != errno.ECHILD:
|
|
raise ex
|
|
|
|
# Now no processes should be left in the process group
|
|
# of the syncevo-dbus-server.
|
|
self.assertEqual({}, self.getChildren())
|
|
|
|
# check status reports
|
|
status, error = self.getSyncStatus('server')
|
|
self.assertSyncStatus('server', 20043, ('child process quit because of signal 9',
|
|
'sending message to child failed: The connection is closed'))
|
|
self.assertSyncStatus('remote@client', 22002, 'synchronization process died prematurely')
|
|
|
|
@timeout(600)
|
|
def testServerFailure(self):
|
|
"""TestLocalSync.testServerFailure - check that D-Bus helper detects when server dies"""
|
|
self.setUpConfigs(childPassword="-")
|
|
self.setUpListeners(self.sessionpath)
|
|
self.lastState = "unknown"
|
|
pid = self.serverPid()
|
|
serverPid = DBusUtil.pserver.pid
|
|
def infoRequest(id, session, state, handler, type, params):
|
|
if state == "request":
|
|
self.assertEqual(self.lastState, "unknown")
|
|
self.lastState = "request"
|
|
# kill syncevo-dbus-server
|
|
if pid != serverPid:
|
|
logging.printf('killing syncevo-dbus-server wrapper with pid %d', serverPid)
|
|
os.kill(serverPid, signal.SIGKILL)
|
|
logging.printf('killing syncevo-dbus-server with pid %d', pid)
|
|
os.kill(pid, signal.SIGKILL)
|
|
loop.quit()
|
|
dbusSignal = bus.add_signal_receiver(infoRequest,
|
|
'InfoRequest',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
try:
|
|
self.session.Sync("slow", {})
|
|
loop.run()
|
|
finally:
|
|
dbusSignal.remove()
|
|
|
|
# Give syncevo-dbus-helper and syncevo-local-sync some time to shut down.
|
|
time.sleep(usingValgrind() and 60 or 10)
|
|
|
|
# Remove syncevo-dbus-server zombie process(es).
|
|
DBusUtil.pserver.wait()
|
|
DBusUtil.pserver = None
|
|
try:
|
|
while True:
|
|
res = os.waitpid(-1, os.WNOHANG)
|
|
if res[0]:
|
|
logging.printf('got status %d for pid %d', res[1], res[0])
|
|
else:
|
|
break
|
|
except OSError, ex:
|
|
if ex.errno != errno.ECHILD:
|
|
raise ex
|
|
|
|
# Now no processes should be left in the process group
|
|
# of the syncevo-dbus-server.
|
|
self.assertEqual({}, self.getChildren())
|
|
|
|
# Sync should have failed with an explanation that it was
|
|
# because of the password.
|
|
self.assertSyncStatus('server', 22003, "error code from SyncEvolution password request timed out (local, status 22003): Could not get the 'addressbook backend' password from user.")
|
|
|
|
@timeout(200)
|
|
@property("ENV", "SYNCEVOLUTION_SYNC_DELAY=10") # allow killing syncevo-dbus-server in middle of sync
|
|
def testNoParent(self):
|
|
"""TestLocalSync.testNoParent - check that sync helper can continue without parent"""
|
|
existingProcesses = self.getChildren()
|
|
logging.printf('Existing processes: %s', str(existingProcesses))
|
|
self.setUpConfigs()
|
|
self.setUpListeners(self.sessionpath)
|
|
pid = self.serverPid()
|
|
serverPid = DBusUtil.pserver.pid
|
|
self.killTimeout = None
|
|
self.syncProcesses = {}
|
|
def killServer():
|
|
self.syncProcesses = dict([ (p, props) for p, props in self.getChildren().iteritems() if not existingProcesses.has_key(p)])
|
|
logging.printf('Sync processes: %s', str(self.syncProcesses))
|
|
if pid != serverPid:
|
|
logging.printf('killing syncevo-dbus-server wrapper with pid %d', serverPid)
|
|
os.kill(serverPid, signal.SIGKILL)
|
|
if serverPid in self.syncProcesses:
|
|
del self.syncProcesses[serverPid]
|
|
logging.printf('killing syncevo-dbus-server with pid %d', pid)
|
|
try:
|
|
os.kill(pid, signal.SIGKILL)
|
|
except OSError, ex:
|
|
if ex.errno != errno.ESRCH:
|
|
logging.printf('unexpected error killing syncevo-dbus-server with pid %d: %s', pid, ex)
|
|
if pid in self.syncProcesses:
|
|
del self.syncProcesses[pid]
|
|
DBusUtil.pserver.wait()
|
|
DBusUtil.pserver = None
|
|
loop.quit()
|
|
def output(path, level, text, procname):
|
|
if self.running and DBusUtil.pserver and text == 'ready to sync':
|
|
killServer()
|
|
receiver = bus.add_signal_receiver(output,
|
|
'LogOutput',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
try:
|
|
self.session.Sync("slow", {})
|
|
loop.run()
|
|
finally:
|
|
receiver.remove()
|
|
|
|
# Should have killed server.
|
|
self.assertFalse(DBusUtil.pserver)
|
|
|
|
# Wait for syncevo-dbus-helper and syncevo-local-sync to shut down.
|
|
logging.printf('waiting for sync process(es): %s', str(self.syncProcesses))
|
|
while self.syncProcesses:
|
|
for pid, process in self.syncProcesses.items():
|
|
logging.printf('checking sync process %d', pid)
|
|
try:
|
|
os.kill(pid, 0)
|
|
except OSError, ex:
|
|
if ex.errno == errno.ESRCH:
|
|
logging.printf('has terminated: sync process %d = %s', pid, process)
|
|
del self.syncProcesses[pid]
|
|
else:
|
|
raise
|
|
time.sleep(2)
|
|
|
|
# Sync should have succeeded.
|
|
self.assertSyncStatus('server', 200, None)
|
|
|
|
class TestLocalCache(unittest.TestCase, DBusUtil):
|
|
"""Tests involving local sync and the local-cache mode."""
|
|
|
|
serverDB = os.path.join(xdg_root, "server")
|
|
clientDB = os.path.join(xdg_root, "client")
|
|
johnVCard = '''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:John Doe
|
|
N:Doe;John
|
|
ORG:Test Inc.
|
|
END:VCARD'''
|
|
# The data must be stored exactly as generated by the Synthesis engine.
|
|
# Otherwise we risk that parsing it directly and comparing against
|
|
# an item data that first went through a parsing/generating step
|
|
# will find irrelevant differences, which breaks the tests which assume
|
|
# that client and server already have the same data.
|
|
#
|
|
# Perhaps a better solution would be to not create the data on the client
|
|
# directly and instead create it via syncing.
|
|
johnComplexVCard = r'''BEGIN:VCARD
|
|
VERSION:3.0
|
|
PRODID:-//Synthesis AG//NONSGML SyncML Engine V3.4.0.47//EN
|
|
REV:20100328T103410Z
|
|
N:Doe;John;;;
|
|
FN:John Doe
|
|
X-EVOLUTION-FILE-AS:Doe\, John
|
|
X-GENDER:
|
|
NICKNAME:user1
|
|
TITLE:Senior Tester
|
|
CATEGORIES:TEST
|
|
ORG:Test Inc.;Testing;test#1;
|
|
ROLE:professional test case
|
|
TEL;TYPE=WORK,VOICE;X-EVOLUTION-UI-SLOT=1:business 1
|
|
TEL;TYPE=HOME,VOICE;X-EVOLUTION-UI-SLOT=2:home 2
|
|
TEL;TYPE=CELL;X-EVOLUTION-UI-SLOT=3:mobile 3
|
|
TEL;TYPE=WORK,FAX;X-EVOLUTION-UI-SLOT=4:businessfax 4
|
|
TEL;TYPE=HOME,FAX;X-EVOLUTION-UI-SLOT=5:homefax 5
|
|
TEL;TYPE=PAGER;X-EVOLUTION-UI-SLOT=6:pager 6
|
|
TEL;TYPE=CAR;X-EVOLUTION-UI-SLOT=7:car 7
|
|
TEL;TYPE=PREF;X-EVOLUTION-UI-SLOT=8:primary 8
|
|
EMAIL;TYPE=WORK;X-EVOLUTION-UI-SLOT=1:john.doe@work.com
|
|
EMAIL;TYPE=HOME;X-EVOLUTION-UI-SLOT=2:john.doe@home.priv
|
|
EMAIL;X-EVOLUTION-UI-SLOT=3:john.doe@other.world
|
|
EMAIL;X-EVOLUTION-UI-SLOT=4:john.doe@yet.another.world
|
|
URL:http://john.doe.com
|
|
CALURI:calender
|
|
FBURL:free/busy
|
|
X-EVOLUTION-BLOG-URL:web log
|
|
X-EVOLUTION-VIDEO-URL:chat
|
|
X-EVOLUTION-MANAGER:John Doe Senior
|
|
X-MANAGER:John Doe Senior
|
|
X-EVOLUTION-ASSISTANT:John Doe Junior
|
|
X-ASSISTANT:John Doe Junior
|
|
X-EVOLUTION-SPOUSE:Joan Doe
|
|
X-SPOUSE:Joan Doe
|
|
X-EVOLUTION-ANNIVERSARY:20060109
|
|
X-ANNIVERSARY:20060109
|
|
X-AIM;X-EVOLUTION-UI-SLOT=1:AIM JOHN
|
|
X-GADUGADU:GADUGADU DOE
|
|
X-GROUPWISE;X-EVOLUTION-UI-SLOT=4:GROUPWISE DOE
|
|
X-ICQ;X-EVOLUTION-UI-SLOT=3:ICQ JD
|
|
X-JABBER:JABBER DOE
|
|
X-MSN:MSN DOE
|
|
X-YAHOO;X-EVOLUTION-UI-SLOT=2:YAHOO JDOE
|
|
X-SKYPE:SKYPE DOE
|
|
X-SIP:SIP DOE
|
|
X-MOZILLA-HTML:TRUE
|
|
ADR;TYPE=WORK:Test Box #2;;Test Drive 2;Test Town;Upper Test County;12346;Old Testovia
|
|
ADR;TYPE=HOME:Test Box #1;;Test Drive 1;Test Village;Lower Test County;12345;Testovia
|
|
ADR:Test Box #3;;Test Drive 3;Test Megacity;Test County;12347;New Testonia
|
|
BDAY:20060108
|
|
NOTE:This is a test case which uses almost all Evolution fields.
|
|
PHOTO;TYPE=JPEG;ENCODING=B:/9j/4AAQSkZJRgABAQEASABIAAD/4QAWRXhpZgAATU0AKgAAAAgAAAAAAAD//gAXQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q/9sAQwAFAwQEBAMFBAQEBQUFBgcMCAcHBwcPCwsJDBEPEhIRDxERExYcFxMUGhURERghGBodHR8fHxMXIiQiHiQcHh8e/9sAQwEFBQUHBgcOCAgOHhQRFB4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e/8AAEQgAFwAkAwEiAAIRAQMRAf/EABkAAQADAQEAAAAAAAAAAAAAAAAGBwgEBf/EADIQAAECBQMCAwQLAAAAAAAAAAECBAADBQYRBxIhEzEUFSIIFjNBGCRHUVZ3lqXD0+P/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AuX6UehP45/aXv9MTPTLVKxNSvMPcqu+a+XdLxf1SfJ6fU37PioTnOxfbOMc/KIZ7U/2VfmTR/wCaKlu6+blu/Ui72zxWtUmmUOrTaWwkWDT09FPR4K587OVrUfVsIwElPPPAbAjxr2umhWXbDu5rmfeApLPZ4hx0lzNm9aUJ9KAVHKlJHAPf7ozPLqWt9y6Z0EPGmoLNjTq48a1iaybJYV52yEtCms5KJmAT61JXtJyUdyQTEc1WlMql7N1/oZ6jagVZVFfUyZPpFy5lvWcxU7Z03BUkGZLWJqVhPYLkIIPBEBtSEUyNAsjI1q1m/VP+UICwL/sqlXp7v+aOHsnyGttq218MtKd8+Ru2JXuScoO45Awe2CIi96aKW1cVyubkYVy6rTqz0J8a5t2qqZl0UjAMwYKScfPAJ+cIQHHP0DthVFaMWt0XwxetnM50Ks2rsxL6ZMnJlJmb5hBBBEiVxjA28dznqo+hdksbQuS3Hs6tVtNzdM1Z/VH5nO3Bl/CJmYHKDynjv3zCEB5rLQNo0bIbydWNWxKljbLQLoWkISOAkBKAABCEID//2Q==
|
|
GEO:;
|
|
X-FOOBAR-EXTENSION;X-FOOBAR-PARAMETER=foobar:has to be stored internally by engine and preserved in testExtensions test\; never sent to a peer
|
|
X-TEST;PARAMETER1=nonquoted;PARAMETER2="quoted because of spaces":Content with\nMultiple\nText lines\nand national chars: äöü
|
|
END:VCARD'''
|
|
joanVCard = '''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:Joan Doe
|
|
N:Doe;Joan
|
|
ORG:Test Inc.
|
|
END:VCARD'''
|
|
vcardFormat = '''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:John_%(index)02d Doe
|
|
N:Doe;John_%(index)02d
|
|
ORG:Test Inc.
|
|
END:VCARD'''
|
|
itemName = "test-dbus.vcf"
|
|
itemNameFormat = "test-dbus-%d.vcf"
|
|
|
|
def run(self, result):
|
|
self.runTest(result)
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
|
|
def setUpConfigs(self, childPassword=None):
|
|
self.setUpLocalSyncConfigs(childPassword, preventSlowSync=False)
|
|
|
|
def checkInSync(self, numReports=2):
|
|
'''verify that client and server do not need to transmit anything in an incremental sync'''
|
|
self.sessionpath, self.session = self.createSession("server", True)
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("local-cache-incremental", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
report = self.checkSync(numReports=numReports)
|
|
self.assertEqual("local-cache-incremental", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
DBusUtil.quit_events = []
|
|
self.sessionpath, self.session = self.createSession("remote@client", True)
|
|
reports = self.session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(numReports, len(reports))
|
|
report = reports[0]
|
|
self.assertEqual("two-way", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
|
|
@timeout(usingValgrind() and 200 or 100)
|
|
def testItemRemoval(self):
|
|
"""TestLocalCache.testItemRemoval - ensure that extra item on server gets removed"""
|
|
self.setUpConfigs()
|
|
os.makedirs(self.serverDB)
|
|
output = open(os.path.join(self.serverDB, self.itemName), "w")
|
|
output.write(self.johnVCard)
|
|
output.close()
|
|
self.setUpListeners(self.sessionpath)
|
|
# ask for incremental caching, expecting it do be done in slow mode
|
|
self.session.Sync("local-cache-incremental", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
# check sync from server perspective
|
|
report = self.checkSync()
|
|
self.assertEqual("local-cache-slow", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("1", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.assertEqual(0, len(os.listdir(self.serverDB)))
|
|
self.assertEqual(0, len(os.listdir(self.clientDB)))
|
|
# check client report
|
|
self.session.Detach()
|
|
DBusUtil.quit_events = []
|
|
self.sessionpath, self.session = self.createSession("remote@client", True)
|
|
reports = self.session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(1, len(reports))
|
|
report = reports[0]
|
|
self.assertEqual("slow", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
|
|
self.checkInSync()
|
|
|
|
def doItemChange(self, numAdditional=0, syncFirst=False, change="Add"):
|
|
self.setUpConfigs()
|
|
entries = []
|
|
os.makedirs(self.clientDB)
|
|
os.makedirs(self.serverDB)
|
|
|
|
numReports = 1
|
|
added = 0
|
|
updated = 0
|
|
deleted = 0
|
|
|
|
# Create additional items in client and server, before initial
|
|
# sync. Creating items on the server later would violate the
|
|
# rule that items on the server are only written during a
|
|
# sync.
|
|
for i in range(0, numAdditional):
|
|
filename = self.itemNameFormat % i
|
|
data = self.vcardFormat % { 'index': i }
|
|
entries.append(filename)
|
|
output = open(os.path.join(self.clientDB, filename), "w")
|
|
output.write(data)
|
|
output.close()
|
|
output = open(os.path.join(self.serverDB, filename), "w")
|
|
output.write(data)
|
|
output.close()
|
|
|
|
if syncFirst:
|
|
# get client and server into sync with empty databases
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("local-cache-incremental", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
report = self.checkSync(numReports=numReports)
|
|
self.assertEqual("local-cache-slow", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
DBusUtil.quit_events = []
|
|
self.sessionpath, self.session = self.createSession("server", True)
|
|
numReports = numReports + 1
|
|
|
|
# create named contact on client
|
|
entries.append(self.itemName)
|
|
output = open(os.path.join(self.clientDB, self.itemName), "w")
|
|
output.write(self.johnVCard)
|
|
output.close()
|
|
|
|
# ask for incremental caching, expecting it do be done in slow mode
|
|
# or incremental, depending on whether both sides were in sync
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("local-cache-incremental", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
|
|
# check sync from server perspective
|
|
report = self.checkSync(numReports=numReports)
|
|
self.assertEqual(syncFirst and "local-cache-incremental" or "local-cache-slow",
|
|
report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("1", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.assertEqual(1 + numAdditional, len(os.listdir(self.serverDB)))
|
|
clientDBEntries = os.listdir(self.clientDB)
|
|
clientDBEntries.sort()
|
|
entries.sort()
|
|
self.assertEqual(entries, clientDBEntries)
|
|
|
|
# check client report
|
|
self.session.Detach()
|
|
DBusUtil.quit_events = []
|
|
self.sessionpath, self.session = self.createSession("remote@client", True)
|
|
reports = self.session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(numReports, len(reports))
|
|
report = reports[0]
|
|
self.assertEqual(syncFirst and "two-way" or "slow", report.get('source-addressbook-mode'))
|
|
self.assertEqual(str(1 + (not syncFirst and numAdditional or 0)), report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
|
|
numReports = numReports + 1
|
|
|
|
if change == "Add" or change == "Add+Slow":
|
|
# client and server are now in sync
|
|
self.checkInSync(numReports=numReports)
|
|
elif change == "Update":
|
|
# update item to something completely, using an incremental sync
|
|
serverContent = os.listdir(self.serverDB)
|
|
output = open(os.path.join(self.clientDB, self.itemName), "w")
|
|
output.write(self.joanVCard)
|
|
output.close()
|
|
self.sessionpath, self.session = self.createSession("server", True)
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("local-cache-incremental", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
report = self.checkSync(numReports=numReports)
|
|
self.assertEqual("local-cache-incremental", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("1", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
self.assertEqual(serverContent, os.listdir(self.serverDB))
|
|
clientDBEntries = os.listdir(self.clientDB)
|
|
clientDBEntries.sort()
|
|
self.assertEqual(entries, clientDBEntries)
|
|
DBusUtil.quit_events = []
|
|
self.sessionpath, self.session = self.createSession("remote@client", True)
|
|
reports = self.session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(numReports, len(reports))
|
|
report = reports[0]
|
|
self.assertEqual("two-way", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("1", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
elif change == "Delete":
|
|
# remove item, using an incremental sync
|
|
os.unlink(os.path.join(self.clientDB, self.itemName))
|
|
self.sessionpath, self.session = self.createSession("server", True)
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("local-cache-incremental", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
report = self.checkSync(numReports=numReports)
|
|
self.assertEqual("local-cache-incremental", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("1", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
self.assertEqual(numAdditional, len(os.listdir(self.serverDB)))
|
|
clientDBEntries = os.listdir(self.clientDB)
|
|
clientDBEntries.sort()
|
|
entries.remove(self.itemName)
|
|
self.assertEqual(entries, clientDBEntries)
|
|
DBusUtil.quit_events = []
|
|
self.sessionpath, self.session = self.createSession("remote@client", True)
|
|
reports = self.session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(numReports, len(reports))
|
|
report = reports[0]
|
|
self.assertEqual("two-way", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("1", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
|
|
numReports = numReports + 1
|
|
|
|
if change == "Add+Slow":
|
|
# explicitly request a slow sync
|
|
DBusUtil.quit_events = []
|
|
self.sessionpath, self.session = self.createSession("server", True)
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("local-cache-slow", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
report = self.checkSync(numReports=numReports)
|
|
self.assertEqual("local-cache-slow", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
numReports = numReports + 1
|
|
|
|
|
|
@timeout(usingValgrind() and 200 or 100)
|
|
def testItemAdd(self):
|
|
"""TestLocalCache.testItemAdd - ensure that new item from client gets added in initial slow sync"""
|
|
self.doItemChange()
|
|
|
|
@timeout(usingValgrind() and 400 or 200)
|
|
def testItemAdd100(self):
|
|
"""TestLocalCache.testItemAdd100 - ensure that new item from client gets added in initial slow sync while leaving 100 items unchanged"""
|
|
self.doItemChange(numAdditional=100)
|
|
|
|
@timeout(usingValgrind() and 200 or 100)
|
|
def testSyncMode(self):
|
|
"""TestLocalCache.testSyncMode - ensure that requesting specific caching sync works"""
|
|
self.doItemChange(change="Add+Slow")
|
|
|
|
@timeout(usingValgrind() and 300 or 100)
|
|
def testItemAddIncremental(self):
|
|
"""TestLocalCache.testItemAddIncremental - ensure that new item from client gets added in incremental sync"""
|
|
self.doItemChange(syncFirst=True)
|
|
|
|
@timeout(usingValgrind() and 400 or 200)
|
|
def testItemAdd100Incremental(self):
|
|
"""TestLocalCache.testItemAdd100Incremental - ensure that new item from client gets added in incremental while leaving 100 items unchanged"""
|
|
self.doItemChange(numAdditional=100, syncFirst=True)
|
|
|
|
@timeout(usingValgrind() and 200 or 100)
|
|
def testItemUpdate(self):
|
|
"""TestLocalCache.testItemUpdate - ensure that an item can be updated incrementally"""
|
|
self.doItemChange(change="Update")
|
|
|
|
@timeout(usingValgrind() and 400 or 200)
|
|
def testItemUpdate100(self):
|
|
"""TestLocalCache.testItemUpdate100 - ensure that an item can be updated incrementally while leaving 100 items unchanged"""
|
|
self.doItemChange(change="Update", numAdditional=100)
|
|
|
|
@timeout(usingValgrind() and 200 or 100)
|
|
def testItemDelete(self):
|
|
"""TestLocalCache.testItemUpdate - ensure that an item can be deleted incrementally"""
|
|
self.doItemChange(change="Delete")
|
|
|
|
@timeout(usingValgrind() and 400 or 200)
|
|
def testItemDelete100(self):
|
|
"""TestLocalCache.testItemUpdate100 - ensure that an item can be deleted incrementally while leaving 100 items unchanged"""
|
|
self.doItemChange(change="Delete", numAdditional=100)
|
|
|
|
def doPropertyRemoval(self, step=0, numAdditional=0):
|
|
"""ensure that obsolete items of an item get removed, either during initial slow sync, second slow sync or incremental sync"""
|
|
self.setUpConfigs()
|
|
entries = []
|
|
os.makedirs(self.clientDB)
|
|
os.makedirs(self.serverDB)
|
|
output = open(os.path.join(self.serverDB, self.itemName), "w")
|
|
output.write(self.johnComplexVCard)
|
|
output.close()
|
|
entries.append(self.itemName)
|
|
output = open(os.path.join(self.clientDB, self.itemName), "w")
|
|
if step == 0:
|
|
# Client has simple version of John,
|
|
# slow sync applies update.
|
|
data = self.johnVCard
|
|
else:
|
|
# Client has same data as on server,
|
|
# slow sync changes nothing.
|
|
data = self.johnComplexVCard
|
|
output.write(data)
|
|
output.close()
|
|
|
|
# create additional items in client and server
|
|
for i in range(0, numAdditional):
|
|
filename = self.itemNameFormat % i
|
|
data = self.vcardFormat % { 'index': i }
|
|
entries.append(filename)
|
|
output = open(os.path.join(self.clientDB, filename), "w")
|
|
output.write(data)
|
|
output.close()
|
|
output = open(os.path.join(self.serverDB, filename), "w")
|
|
output.write(data)
|
|
output.close()
|
|
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("local-cache-incremental", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
# check sync from server perspective
|
|
report = self.checkSync()
|
|
self.assertEqual("local-cache-slow", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual((step == 0) and "1" or "0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.assertEqual(1 + numAdditional, len(os.listdir(self.serverDB)))
|
|
clientDBEntries = os.listdir(self.clientDB)
|
|
clientDBEntries.sort()
|
|
entries.sort()
|
|
self.assertEqual(entries, clientDBEntries)
|
|
# check client report
|
|
self.session.Detach()
|
|
DBusUtil.quit_events = []
|
|
self.sessionpath, self.session = self.createSession("remote@client", True)
|
|
reports = self.session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(1, len(reports))
|
|
report = reports[0]
|
|
self.assertEqual("slow", report.get('source-addressbook-mode'))
|
|
self.assertEqual(str(1 + numAdditional), report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
|
|
if step == 0:
|
|
# Work done already, just check that in sync.
|
|
self.checkInSync()
|
|
else:
|
|
# update item to simple version, using an incremental sync
|
|
# or another slow sync
|
|
if step == 1:
|
|
# force slow sync by removing client-side meta data
|
|
shutil.rmtree(os.path.join(xdg_root, 'config', 'syncevolution', 'default', 'peers', 'server', '.remote@client', '.synthesis'))
|
|
serverContent = os.listdir(self.serverDB)
|
|
output = open(os.path.join(self.clientDB, self.itemName), "w")
|
|
output.write(self.johnVCard)
|
|
output.close()
|
|
self.sessionpath, self.session = self.createSession("server", True)
|
|
self.setUpListeners(self.sessionpath)
|
|
self.session.Sync("local-cache-incremental", {})
|
|
loop.run()
|
|
self.assertEqual(DBusUtil.quit_events, ["session " + self.sessionpath + " done"])
|
|
report = self.checkSync(numReports=2)
|
|
self.assertEqual(step == 1 and "local-cache-slow" or "local-cache-incremental", report.get('source-addressbook-mode'))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("1", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
self.assertEqual(serverContent, os.listdir(self.serverDB))
|
|
clientDBEntries = os.listdir(self.clientDB)
|
|
clientDBEntries.sort()
|
|
self.assertEqual(entries, clientDBEntries)
|
|
DBusUtil.quit_events = []
|
|
self.sessionpath, self.session = self.createSession("remote@client", True)
|
|
reports = self.session.GetReports(0, 100, utf8_strings=True)
|
|
self.assertEqual(2, len(reports))
|
|
report = reports[0]
|
|
self.assertEqual(step == 1 and "slow" or "two-way", report.get('source-addressbook-mode'))
|
|
self.assertEqual(step == 1 and str(1 + numAdditional) or "0", report.get('source-addressbook-stat-remote-added-total', "0"))
|
|
self.assertEqual(step == 1 and "0" or "1", report.get('source-addressbook-stat-remote-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-remote-removed-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-added-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-updated-total', "0"))
|
|
self.assertEqual("0", report.get('source-addressbook-stat-local-removed-total', "0"))
|
|
self.session.Detach()
|
|
|
|
if step == 1:
|
|
# second sync was a slow sync, now we should be in sync
|
|
self.checkInSync(numReports=3)
|
|
|
|
# Server item should be the simple one now, as in the client.
|
|
sub = subprocess.Popen(['synccompare', self.clientDB, self.serverDB],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
stdout, stderr = sub.communicate()
|
|
self.assertEqual(0, sub.returncode,
|
|
msg=stdout)
|
|
|
|
@timeout(usingValgrind() and 300 or 100)
|
|
def testPropertyRemovalSlow(self):
|
|
"""TestLocalCache.testPropertyRemovalSlow - ensure that obsolete item properties are removed during slow sync"""
|
|
self.doPropertyRemoval()
|
|
|
|
@timeout(usingValgrind() and 600 or 200)
|
|
def testPropertyRemovalSlow100(self):
|
|
"""TestLocalCache.testPropertyRemovalSlow100 - ensure that obsolete item properties are removed during slow sync while leaving 100 items unchanged"""
|
|
self.doPropertyRemoval(numAdditional=100)
|
|
|
|
@timeout(usingValgrind() and 300 or 100)
|
|
def testPropertyRemovalSecondSlow(self):
|
|
"""TestLocalCache.testPropertyRemovalSecondSlow - ensure that obsolete item properties are removed during non-initial slow sync"""
|
|
self.doPropertyRemoval(step=1)
|
|
|
|
@timeout(usingValgrind() and 600 or 200)
|
|
def testPropertyRemovalSecondSlow100(self):
|
|
"""TestLocalCache.testPropertyRemovalSecondSlow100 - ensure that obsolete item properties are removed during non-initial slow sync while leaving 100 items unchanged"""
|
|
self.doPropertyRemoval(step=1, numAdditional=100)
|
|
|
|
@timeout(usingValgrind() and 300 or 100)
|
|
def testPropertyRemovalIncremental(self):
|
|
"""TestLocalCache.testPropertyRemovalIncremental - ensure that obsolete item properties are removed during incremental sync"""
|
|
self.doPropertyRemoval(step=2)
|
|
|
|
@timeout(usingValgrind() and 600 or 200)
|
|
def testPropertyRemovalIncremental100(self):
|
|
"""TestLocalCache.testPropertyRemovalIncremental100 - ensure that obsolete item properties are removed during incremental sync while leaving 100 items unchanged"""
|
|
self.doPropertyRemoval(step=2, numAdditional=100)
|
|
|
|
|
|
class TestFileNotify(unittest.TestCase, DBusUtil):
|
|
"""syncevo-dbus-server must stop if one of its files mapped into
|
|
memory (executable, libraries) change. Furthermore it must restart
|
|
if automatic syncs are enabled. This class simulates such file changes
|
|
by starting the server, identifying the location of the main executable,
|
|
and renaming it back and forth."""
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
self.serverexe = self.serverExecutable()
|
|
|
|
def tearDown(self):
|
|
if os.path.isfile(self.serverexe + ".bak"):
|
|
os.rename(self.serverexe + ".bak", self.serverexe)
|
|
|
|
def which(self, program):
|
|
def is_exe(fpath):
|
|
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
|
|
|
for path in os.environ['PATH'].split(os.pathsep):
|
|
path = path.strip('"')
|
|
exe_file = os.path.join(path, program)
|
|
if is_exe(exe_file):
|
|
return exe_file
|
|
return None
|
|
|
|
def run(self, result):
|
|
# Ensure that we use a private copy of syncevo-dbus-server, for two reasons:
|
|
# - when using the installed version, we might not be able to rename it.
|
|
# - when other tests run concurrently, they too would see the modification and shut down.
|
|
tmppath = os.path.abspath('TestFileNotifyDir')
|
|
oldpath = os.environ['PATH']
|
|
try:
|
|
os.makedirs(tmppath)
|
|
shutil.copy(self.which('syncevo-dbus-server'), tmppath)
|
|
os.environ['PATH'] = '%s:%s' % (tmppath, oldpath)
|
|
self.runTest(result)
|
|
finally:
|
|
os.environ['PATH'] = oldpath
|
|
if os.path.exists(tmppath):
|
|
shutil.rmtree(tmppath)
|
|
|
|
def modifyServerFile(self):
|
|
"""rename server executable to trigger shutdown"""
|
|
logging.printf('touching server file %s to trigger shutdown', self.serverexe)
|
|
os.rename(self.serverexe, self.serverexe + ".bak")
|
|
os.rename(self.serverexe + ".bak", self.serverexe)
|
|
|
|
@timeout(100)
|
|
def testShutdown(self):
|
|
"""TestFileNotify.testShutdown - update server binary for 30 seconds, check that it shuts down at most 15 seconds after last mod"""
|
|
self.assertTrue(self.isServerRunning())
|
|
i = 0
|
|
# Server must not shut down immediately, more changes might follow.
|
|
# Simulate that.
|
|
while i < 6:
|
|
self.modifyServerFile()
|
|
time.sleep(5)
|
|
i = i + 1
|
|
self.assertTrue(self.isServerRunning())
|
|
if usingValgrind():
|
|
# valgrind shutdown takes time
|
|
time.sleep(20)
|
|
else:
|
|
time.sleep(10)
|
|
self.assertFalse(self.isServerRunning())
|
|
|
|
@timeout(100)
|
|
def testSession(self):
|
|
"""TestFileNotify.testSession - create session, shut down directly after closing it"""
|
|
self.assertTrue(self.isServerRunning())
|
|
self.setUpSession("")
|
|
self.modifyServerFile()
|
|
time.sleep(15)
|
|
self.assertTrue(self.isServerRunning())
|
|
self.session.Detach()
|
|
# should shut down almost immediately,
|
|
# except when using valgrind
|
|
if usingValgrind():
|
|
time.sleep(20)
|
|
else:
|
|
time.sleep(1)
|
|
self.assertFalse(self.isServerRunning())
|
|
|
|
@timeout(100)
|
|
def testSession2(self):
|
|
"""TestFileNotify.testSession2 - create session, shut down after quiescence period after closing it"""
|
|
self.assertTrue(self.isServerRunning())
|
|
self.setUpSession("")
|
|
self.modifyServerFile()
|
|
self.assertTrue(self.isServerRunning())
|
|
self.session.Detach()
|
|
time.sleep(8)
|
|
self.assertTrue(self.isServerRunning())
|
|
if usingValgrind():
|
|
time.sleep(20)
|
|
else:
|
|
time.sleep(4)
|
|
self.assertFalse(self.isServerRunning())
|
|
|
|
@timeout(100)
|
|
def testSession3(self):
|
|
"""TestFileNotify.testSession3 - shut down after quiescence period without activating a pending session request"""
|
|
self.assertTrue(self.isServerRunning())
|
|
self.modifyServerFile()
|
|
self.assertTrue(self.isServerRunning())
|
|
time.sleep(8)
|
|
self.assertTrue(self.isServerRunning())
|
|
self.session = None
|
|
try:
|
|
# this should not succeed: the server should rejects the
|
|
# session request because it is shutting down
|
|
self.setUpSession("")
|
|
except dbus.DBusException, ex:
|
|
self.assertEqual(str(ex),
|
|
"org.syncevolution.Exception: server shutting down")
|
|
else:
|
|
self.fail("no exception thrown")
|
|
|
|
@timeout(100)
|
|
def testRestart(self):
|
|
"""TestFileNotify.testRestart - set up auto sync, then check that server restarts"""
|
|
self.assertTrue(self.isServerRunning())
|
|
self.setUpSession("memotoo")
|
|
config = self.session.GetConfig(True, utf8_strings=True)
|
|
config[""]["autoSync"] = "1"
|
|
self.session.SetConfig(False, False, config)
|
|
self.assertTrue(self.isServerRunning())
|
|
self.session.Detach()
|
|
self.modifyServerFile()
|
|
bus_name = self.server.bus_name
|
|
# give server time to restart
|
|
if usingValgrind():
|
|
time.sleep(40)
|
|
else:
|
|
time.sleep(15)
|
|
self.setUpServer()
|
|
self.assertNotEqual(bus_name, self.server.bus_name)
|
|
# serverExecutable() will fail if the service wasn't properly
|
|
# with execve() because then the old process is dead.
|
|
self.assertEqual(self.serverexe, self.serverExecutable())
|
|
|
|
bt_mac = "D4:5D:42:73:E4:6C"
|
|
bt_fingerprint = "Nokia 5230"
|
|
bt_name = "My Nokia 5230"
|
|
bt_template = "Bluetooth_%s_1" % (bt_mac)
|
|
bt_device = "%s/dev_%s" % (bt_adaptor, string.replace(bt_mac, ':', '_'))
|
|
|
|
class BluezAdapter (dbus.service.Object):
|
|
def __init__(self):
|
|
self.SUPPORTS_MULTIPLE_OBJECT_PATHS = True
|
|
bus_name = dbus.service.BusName('org.bluez', bus)
|
|
dbus.service.Object.__init__(self, bus_name, bt_adaptor)
|
|
|
|
@dbus.service.signal(dbus_interface='org.bluez.Adapter', signature='o')
|
|
def DeviceCreated(self, obj):
|
|
return bt_adaptor
|
|
|
|
@dbus.service.signal(dbus_interface='org.bluez.Adapter', signature='o')
|
|
def DeviceRemoved(self, obj):
|
|
return bt_device
|
|
|
|
@dbus.service.method(dbus_interface='org.bluez.Adapter', in_signature='', out_signature='ao')
|
|
def ListDevices(self):
|
|
return [bt_device]
|
|
|
|
class BluezDevice (dbus.service.Object):
|
|
def __init__(self):
|
|
self.SUPPORTS_MULTIPLE_OBJECT_PATHS = True
|
|
bus_name = dbus.service.BusName('org.bluez', bus)
|
|
dbus.service.Object.__init__(self, bus_name, bt_device)
|
|
|
|
@dbus.service.method(dbus_interface='org.bluez.Device', in_signature='', out_signature='a{sv}')
|
|
def GetProperties(self):
|
|
return {"Name": bt_name,
|
|
"Address": bt_mac,
|
|
"UUIDs": ['00000002-0000-1000-8000-0002ee000002',
|
|
'00001000-0000-1000-8000-00805f9b34fb',
|
|
'00001101-0000-1000-8000-00805f9b34fb',
|
|
'00001103-0000-1000-8000-00805f9b34fb',
|
|
'00001105-0000-1000-8000-00805f9b34fb',
|
|
'00001106-0000-1000-8000-00805f9b34fb',
|
|
'0000110a-0000-1000-8000-00805f9b34fb',
|
|
'0000110c-0000-1000-8000-00805f9b34fb',
|
|
'0000110e-0000-1000-8000-00805f9b34fb',
|
|
'00001112-0000-1000-8000-00805f9b34fb',
|
|
'0000111b-0000-1000-8000-00805f9b34fb',
|
|
'0000111f-0000-1000-8000-00805f9b34fb',
|
|
'0000112d-0000-1000-8000-00805f9b34fb',
|
|
'0000112f-0000-1000-8000-00805f9b34fb',
|
|
'00001200-0000-1000-8000-00805f9b34fb',
|
|
'00005005-0000-1000-8000-0002ee000001',
|
|
'00005557-0000-1000-8000-0002ee000001',
|
|
'00005601-0000-1000-8000-0002ee000001']}
|
|
|
|
@dbus.service.method(dbus_interface='org.bluez.Device', in_signature='s', out_signature='a{us}')
|
|
def DiscoverServices(self, ignore):
|
|
# This should be the last method to call. So, we need to quit the loop to exit.
|
|
loop.quit()
|
|
return { 65569L: '<?xml version="1.0" encoding="UTF-8" ?><record><attribute id="0x0000"><uint32 value="0x00010021" /></attribute><attribute id="0x0001"><sequence><uuid value="0x1200" /></sequence></attribute><attribute id="0x0005"><sequence><uuid value="0x1002" /></sequence></attribute><attribute id="0x0006"><sequence><uint16 value="0x454e" /><uint16 value="0x006a" /><uint16 value="0x0100" /></sequence></attribute><attribute id="0x0100"><text value="PnP Information" /></attribute><attribute id="0x0200"><uint16 value="0x0102" /></attribute><attribute id="0x0201"><uint16 value="0x0001" /></attribute><attribute id="0x0202"><uint16 value="0x00e7" /></attribute><attribute id="0x0203"><uint16 value="0x0000" /></attribute><attribute id="0x0204"><boolean value="true" /></attribute><attribute id="0x0205"><uint16 value="0x0001" /></attribute></record>'}
|
|
|
|
@dbus.service.signal(dbus_interface='org.bluez.Device', signature='sv')
|
|
def PropertyChanged(self, key, value):
|
|
if(key == "Name"):
|
|
bt_name = value
|
|
|
|
def emitSignal(self):
|
|
""" Change the device name. """
|
|
self.PropertyChanged("Name", [string.replace(bt_name, "My", "Changed")])
|
|
return
|
|
|
|
class TestBluetooth(unittest.TestCase, DBusUtil):
|
|
"""Tests that Bluetooth works properly."""
|
|
|
|
def setUp(self):
|
|
self.adp_conn = BluezAdapter()
|
|
self.dev_conn = BluezDevice()
|
|
loop.run()
|
|
self.setUpServer()
|
|
|
|
def tearDown(self):
|
|
self.adp_conn.remove_from_connection()
|
|
self.dev_conn.remove_from_connection()
|
|
|
|
def run(self, result):
|
|
self.runTest(result)
|
|
|
|
@property("ENV", "DBUS_TEST_BLUETOOTH=session")
|
|
@timeout(100)
|
|
def testBluetoothTemplates(self):
|
|
"""TestBluetooth.testBluetoothTemplates - check for the bluetooth device's template"""
|
|
configs = self.server.GetConfigs(True, utf8_strings=True)
|
|
config = next((config for config in configs if config == bt_template), None)
|
|
self.failUnless(config)
|
|
|
|
@property("ENV", "DBUS_TEST_BLUETOOTH=session")
|
|
@timeout(100)
|
|
def testBluetoothNames(self):
|
|
"""TestBluetooth.testBluetoothNames - check that fingerPrint/peerName/deviceName/hardwareName are set correctly"""
|
|
# This needs to be called before we can fetch the single config.
|
|
configs = self.server.GetConfigs(True, utf8_strings=True)
|
|
config = self.server.GetConfig(bt_template, True, utf8_strings=True)
|
|
# user-configurable name
|
|
self.failUnlessEqual(config['']["deviceName"], bt_name)
|
|
# must not be set
|
|
self.assertNotIn("peerName", config[''])
|
|
# all of the possible strings in the template, must include the hardware name of this example device
|
|
self.failIf(string.find(config['']["fingerPrint"], bt_fingerprint) < 0)
|
|
# real hardware information
|
|
self.failUnlessEqual(config['']["hardwareName"], bt_fingerprint)
|
|
|
|
def createFiles(root, content, append = False):
|
|
'''create directory hierarchy, overwriting previous content'''
|
|
if not append:
|
|
shutil.rmtree(root, True)
|
|
|
|
entries = content.split("\n")
|
|
outname = ''
|
|
outfile = None
|
|
for entry in entries:
|
|
if not entry:
|
|
continue
|
|
parts = entry.split(":", 1)
|
|
newname = parts[0]
|
|
line = parts[1]
|
|
if newname != outname:
|
|
fullpath = root + "/" + newname
|
|
try:
|
|
os.makedirs(fullpath[0:fullpath.rindex("/")])
|
|
except:
|
|
pass
|
|
mode = "w"
|
|
if append:
|
|
mode = "a"
|
|
outfile = open(fullpath, mode)
|
|
outname = newname
|
|
outfile.write(line + "\n")
|
|
outfile.close()
|
|
|
|
isPropRegEx = re.compile(r'^([a-zA-Z]+) = ')
|
|
def isPropAssignment (line):
|
|
'''true if "<word> = "'''
|
|
m = isPropRegEx.search(line)
|
|
if not m:
|
|
return False
|
|
# exclude some false positives
|
|
if m.group(1) in ('KCalExtended', 'mkcal', 'QtContacts'):
|
|
return False
|
|
return True
|
|
|
|
def scanFiles(root, peer = '', onlyProps = True, directory = ''):
|
|
'''turn directory hierarchy into string
|
|
root - root path in file system
|
|
peer - if non-empty, then ignore all <root>/peers/<foo>
|
|
directories where <foo> != peer
|
|
onlyProps - ignore lines which are comments
|
|
directory - a subdirectory of root (used for recursion)'''
|
|
newroot = root + '/' + directory
|
|
out = ''
|
|
|
|
for entry in sorted(os.listdir(newroot)):
|
|
fullEntry = newroot + "/" + entry
|
|
if os.path.isdir(fullEntry):
|
|
if not (newroot.endswith("/peers") and peer and entry != peer):
|
|
if directory:
|
|
newdir = directory + '/' + entry
|
|
else:
|
|
newdir = entry
|
|
out += scanFiles(root, peer, onlyProps, newdir)
|
|
else:
|
|
infile = open (fullEntry)
|
|
for line in infile:
|
|
line = line.rstrip("\r\n")
|
|
if (line):
|
|
takeIt = False
|
|
if (line.startswith("# ")):
|
|
takeIt = isPropAssignment(line[2:])
|
|
else:
|
|
takeIt = True
|
|
if (not onlyProps or takeIt):
|
|
if (directory):
|
|
out += directory + "/"
|
|
out += entry + ':' + line + "\n"
|
|
return out
|
|
|
|
def sortConfig(config):
|
|
'''sort lines by file, preserving order inside each line'''
|
|
lines = config.splitlines()
|
|
linenr = -1
|
|
|
|
unsorted = []
|
|
for line in lines:
|
|
linenr += 1
|
|
if not line:
|
|
continue
|
|
parts = line.split(":", 1)
|
|
element = parts[0], linenr, parts[1]
|
|
unsorted.append(element)
|
|
|
|
# stable sort because of line number
|
|
# probably it would be stable without it
|
|
# but better be safe than sorry
|
|
lines = sorted(unsorted)
|
|
unsorted = []
|
|
newconfig = ""
|
|
for line in lines:
|
|
newconfig += line[0] + ":" + line[2] + "\n"
|
|
|
|
return newconfig
|
|
|
|
def lastLine(string):
|
|
return string.splitlines(True)[-1]
|
|
|
|
def stripOutput(string):
|
|
# strip debug output, if it was enabled via env var
|
|
if os.environ.get('SYNCEVOLUTION_DEBUG', None) != None:
|
|
string = re.sub(r'\[DEBUG *\S*?\].*?\n', '', string)
|
|
# remove time
|
|
r = re.compile(r'^\[(\w+)\s+\d\d:\d\d:\d\d\]', re.MULTILINE)
|
|
string = r.sub(r'[\1]', string)
|
|
return string
|
|
|
|
def injectValues(config):
|
|
res = config
|
|
|
|
if False:
|
|
# username/password not set in templates, only in configs
|
|
# created via the command line - not anymore, but if it ever
|
|
# comes back, here's the place for it
|
|
res = res.replace("# username = ",
|
|
"username = your SyncML server account name",
|
|
1)
|
|
res = res.replace("# password = ",
|
|
"password = your SyncML server password",
|
|
1)
|
|
|
|
return res
|
|
|
|
def filterConfig(config):
|
|
'''remove pure comment lines from buffer, also empty lines, also
|
|
defaultPeer/keyring (because reference properties do not include global
|
|
props)'''
|
|
config_lines = config.splitlines()
|
|
out = ''
|
|
|
|
for line in config_lines:
|
|
if line and \
|
|
"defaultPeer =" not in line and \
|
|
"keyring =" not in line and \
|
|
(line.startswith("# ") == False or \
|
|
isPropAssignment(line[2:])):
|
|
out += line + "\n"
|
|
|
|
return out
|
|
|
|
def internalToIni(config):
|
|
'''convert the internal config dump to .ini style (--print-config)'''
|
|
config_lines = config.splitlines()
|
|
ini = ''
|
|
section = ''
|
|
|
|
for line in config_lines:
|
|
if not line:
|
|
continue
|
|
colon = line.find(':')
|
|
prefix = line[0:colon]
|
|
# internal values are not part of the --print-config output
|
|
if ".internal.ini" in prefix or "= internal value" in line:
|
|
continue
|
|
|
|
# --print-config also does not duplicate the "type" property
|
|
# remove the shared property
|
|
if ":type" in line and line.startswith("sources/"):
|
|
continue
|
|
|
|
# sources/<name>/config.ini or spds/sources/<name>/config.ini
|
|
endslash = prefix.rfind("/")
|
|
if endslash > 1:
|
|
slash = prefix.rfind("/", 0, endslash)
|
|
if slash != -1:
|
|
newsource = prefix[slash + 1:endslash]
|
|
if newsource != section:
|
|
sources = prefix.find("/sources/")
|
|
if sources != -1 and newsource != "syncml":
|
|
ini += "[" + newsource + "]\n"
|
|
section = newsource
|
|
|
|
assignment = line[colon + 1:]
|
|
# substitute aliases with generic values
|
|
assignment = assignment.replace("= syncml:auth-md5", "= md5", 1)
|
|
assignment = assignment.replace("= syncml:auth-basic", "= basic", 1)
|
|
|
|
ini += assignment + "\n"
|
|
|
|
return ini
|
|
|
|
# result of removeComments(self.removeRandomUUID(filterConfig())) for
|
|
# Google Calendar template/config
|
|
google = '''syncURL = https://apidata.googleusercontent.com/caldav/v2 https://www.googleapis.com/.well-known/carddav https://www.google.com/calendar/dav
|
|
printChanges = 0
|
|
dumpData = 0
|
|
deviceId = fixed-devid
|
|
IconURI = image://themedimage/icons/services/google
|
|
ConsumerReady = 1
|
|
peerType = WebDAV
|
|
[addressbook]
|
|
sync = two-way
|
|
backend = CardDAV
|
|
[calendar]
|
|
sync = two-way
|
|
backend = CalDAV
|
|
'''
|
|
|
|
# result of removeComments(self.removeRandomUUID(filterConfig())) for
|
|
# Yahoo Calendar + Contacts
|
|
yahoo = '''printChanges = 0
|
|
dumpData = 0
|
|
deviceId = fixed-devid
|
|
IconURI = image://themedimage/icons/services/yahoo
|
|
ConsumerReady = 1
|
|
peerType = WebDAV
|
|
[addressbook]
|
|
sync = disabled
|
|
backend = CardDAV
|
|
[calendar]
|
|
sync = two-way
|
|
backend = CalDAV
|
|
'''
|
|
|
|
def removeComments(config):
|
|
lines = config.splitlines()
|
|
|
|
out = ''
|
|
for line in lines:
|
|
if line and not line.startswith("#"):
|
|
out += line + "\n"
|
|
|
|
return out
|
|
|
|
def filterIndented(config):
|
|
'''remove lines indented with spaces'''
|
|
lines = config.splitlines()
|
|
|
|
out = ''
|
|
first = True
|
|
for line in lines:
|
|
if not line.startswith(" "):
|
|
if first:
|
|
first = False
|
|
else:
|
|
out += "\n"
|
|
out += line
|
|
|
|
return out
|
|
|
|
def filterFiles(config):
|
|
'''remove comment lines from scanFiles() output'''
|
|
out = ''
|
|
lines = config.splitlines()
|
|
|
|
for line in lines:
|
|
if line.find(":#") == -1:
|
|
out += line + "\n"
|
|
# strip last newline
|
|
# if out:
|
|
# return out[:-1]
|
|
return out
|
|
|
|
class CmdlineUtil(DBusUtil):
|
|
"""Helper methods for running syncevolution command line tool."""
|
|
|
|
def statusChanged(self, *args, **keywords):
|
|
'''remember the command line session'''
|
|
if args[0] == 'idle' and self.session == None:
|
|
self.session = dbus.Interface(bus.get_object(self.server.bus_name,
|
|
keywords['path']),
|
|
'org.syncevolution.Session')
|
|
|
|
def runCmdline(self, args, env=None, expectSuccess=True, preserveOutputOrder=False,
|
|
sessionFlags=['no-sync'],
|
|
testInstance=None):
|
|
'''Run the 'syncevolution' command line (from PATH) with the
|
|
given arguments (list or tuple of strings). Uses environment
|
|
used to run syncevo-dbus-server unless one is set
|
|
explicitly. Unless told otherwise, the result of the command
|
|
is checked for success. Usually stdout and stderr are captured
|
|
separately, in which case relative order of messages from
|
|
different streams cannot be tested. When that is relevant, set
|
|
preserveOutputOrder=True and look only at the stdout.
|
|
|
|
Returns tuple with stdout, stderr and result code. DBusUtil.events
|
|
contains the status and progress events seen while the command line
|
|
ran. self.session is the proxy for that session.'''
|
|
s = self.startCmdline(args, env, preserveOutputOrder, testInstance)
|
|
return self.finishCmdline(s, expectSuccess, sessionFlags)
|
|
|
|
def startCmdline(self, args, env=None, preserveOutputOrder=False, testInstance=None):
|
|
# Watch all future events, ignore old ones.
|
|
while loop.get_context().iteration(False):
|
|
pass
|
|
DBusUtil.events = []
|
|
self.session = None
|
|
a = [ 'syncevolution' ]
|
|
a.extend(args)
|
|
if testInstance == None:
|
|
testInstance = self
|
|
# Explicitly pass an environment. Otherwise subprocess.Popen()
|
|
# from Python 2.6 uses the environment passed to a previous
|
|
# call to subprocess.Popen() (which will fail if the previous
|
|
# test ran with an environment which had SYNCEVOLUTION_DEBUG
|
|
# set).
|
|
if env == None:
|
|
cmdline_env = testInstance.storedenv
|
|
else:
|
|
cmdline_env = env
|
|
if not 'cmdlineCounter' in dir(testInstance):
|
|
testInstance.cmdlineCounter = 0
|
|
testInstance.cmdlineCounter = testInstance.cmdlineCounter + 1
|
|
if preserveOutputOrder:
|
|
stdoutName = testInstance.testname + (".cmdline.%d.outerr.log" % testInstance.cmdlineCounter)
|
|
stderrName = None
|
|
stdout = open(stdoutName, "w")
|
|
stderr = subprocess.STDOUT
|
|
else:
|
|
stdoutName = testInstance.testname + (".cmdline.%d.out.log" % testInstance.cmdlineCounter)
|
|
stdout = open(stdoutName, "w")
|
|
stderrName = testInstance.testname + (".cmdline.%d.err.log" % testInstance.cmdlineCounter)
|
|
stderr = open(stderrName, "w")
|
|
|
|
s = subprocess.Popen(a, stdout=stdout, stderr=stderr,
|
|
env=cmdline_env)
|
|
s.stdoutName = stdoutName
|
|
s.stderrName = stderrName
|
|
return s
|
|
|
|
def finishCmdline(self, s, expectSuccess=True, sessionFlags=['no-sync']):
|
|
try:
|
|
s.wait()
|
|
except Exception, ex:
|
|
message = str(ex)
|
|
message = message + (s.stderrName and "\nStdout:\n" or "\nStdout + Stderr:\n")
|
|
message = message + open(s.stdoutName).read()
|
|
if s.stderrName:
|
|
message = message + "\nStderr:\n"
|
|
message = message + open(s.stderrName).read()
|
|
raise Exception(message)
|
|
s.wait()
|
|
out = open(s.stdoutName).read()
|
|
if s.stderrName:
|
|
err = open(s.stderrName).read()
|
|
else:
|
|
err = None
|
|
doFail = False
|
|
if expectSuccess and s.returncode != 0:
|
|
result = 'syncevolution command failed.'
|
|
doFail = True
|
|
elif not expectSuccess and s.returncode == 0:
|
|
result = 'syncevolution was expected to fail, but it succeeded.'
|
|
doFail = True
|
|
if doFail:
|
|
result += '\nOutput:\n%s' % out
|
|
if s.stderrName != None:
|
|
result += '\nSeparate stderr:\n%s' % err
|
|
self.fail(result)
|
|
|
|
# Collect D-Bus events, check session.
|
|
while loop.get_context().iteration(False):
|
|
pass
|
|
if sessionFlags != None:
|
|
self.assertTrue(self.session)
|
|
self.assertEqual(self.session.GetFlags(), sessionFlags)
|
|
|
|
return (out, err, s.returncode)
|
|
|
|
defSourceOutput = ("checking usability...", "configuring datastore with sync mode 'two-way'")
|
|
|
|
def sourceCheckOutput(self, sources=None):
|
|
'''returns the output produced by --configure when checking sources'''
|
|
if sources is None:
|
|
sources = [(source, self.defSourceOutput) for source in ('addressbook', 'calendar', 'memo', 'todo')]
|
|
elif not (isinstance(sources, type([])) or isinstance(sources, type(()))):
|
|
sources = [ (sources, self.defSourceOutput) ]
|
|
res = []
|
|
for i in sources:
|
|
source = i[0]
|
|
result = i[1]
|
|
if not (isinstance(result, type([])) or isinstance(result, type(()))):
|
|
result = [result]
|
|
for e in result:
|
|
res.append('[INFO] %s: %s\n' % (source, e))
|
|
return ''.join(res)
|
|
|
|
def assertNoErrors(self, err):
|
|
'''check that error output is empty'''
|
|
self.assertEqualDiff('', err)
|
|
|
|
|
|
class TestCmdline(CmdlineUtil, unittest.TestCase):
|
|
"""Tests cmdline by Session::Execute()."""
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
self.setUpListeners(None)
|
|
# All tests run with their own XDG root hierarchy.
|
|
# Here are the config files.
|
|
self.configdir = xdg_root + "/config/syncevolution"
|
|
|
|
def run(self, result):
|
|
# Runtime varies a lot when using valgrind, because
|
|
# of the need to check an additional process. Allow
|
|
# a lot more time when running under valgrind.
|
|
self.runTest(result, own_xdg=True, own_home=True,
|
|
defTimeout=usingValgrind() and 600 or 20)
|
|
|
|
cachedSSLServerCertificates = None
|
|
def getSSLServerCertificates(self):
|
|
'''Default SSLServerCertificates path as compiled into the SyncEvolution
|
|
binaries. Determined once by asking for a template.'''
|
|
if TestCmdline.cachedSSLServerCertificates == None:
|
|
out, err, code = self.runCmdline(['--template', 'default',
|
|
'--print-config'])
|
|
self.assertNoErrors(err)
|
|
m = re.search(r'^# SSLServerCertificates = (.*)\n', out, re.MULTILINE)
|
|
self.assertTrue(m)
|
|
TestCmdline.cachedSSLServerCertificates = m.group(1)
|
|
return TestCmdline.cachedSSLServerCertificates
|
|
|
|
cachedVersions = {}
|
|
def getVersion(self, versionName):
|
|
'''Parses the SyncConfig.h file to get a version number
|
|
described by versionName.'''
|
|
if not versionName in TestCmdline.cachedVersions:
|
|
# Get the path to SyncConfig.h. Start with current directory,
|
|
# where it will be in the installed test suite.
|
|
header = './SyncConfig.h'
|
|
if not os.path.exists(header):
|
|
# Fall back to uninstalled source, found via the test-dbus.py path.
|
|
scriptpath = os.path.abspath(os.path.expanduser(os.path.expandvars(sys.argv[0])))
|
|
header = os.path.join(os.path.dirname(scriptpath), '..', 'src', 'syncevo', 'SyncConfig.h')
|
|
# do not care about escaping the versionName variable.
|
|
# this function is only used from some version getters and
|
|
# those are passing some simple strings with no regexp
|
|
# metacharacters.
|
|
prog = re.compile("^\s*static\s+const\s+int\s+{0}\s*=\s*(\d+);".format(versionName))
|
|
found = False
|
|
# will throw IOError if opening header fails
|
|
for line in open(header):
|
|
m = prog.match(line)
|
|
if m:
|
|
TestCmdline.cachedVersions[versionName] = m.group(1)
|
|
found = True
|
|
break
|
|
if not found:
|
|
self.fail(versionname + " not found in SyncConfig.h")
|
|
return TestCmdline.cachedVersions[versionName]
|
|
|
|
def getRootMinVersion(self):
|
|
return self.getVersion('CONFIG_ROOT_MIN_VERSION')
|
|
|
|
def getRootCurVersion(self):
|
|
return self.getVersion('CONFIG_ROOT_CUR_VERSION')
|
|
|
|
def getContextMinVersion(self):
|
|
return self.getVersion('CONFIG_CONTEXT_MIN_VERSION')
|
|
|
|
def getContextCurVersion(self):
|
|
return self.getVersion('CONFIG_CONTEXT_CUR_VERSION')
|
|
|
|
def getPeerMinVersion(self):
|
|
return self.getVersion('CONFIG_PEER_MIN_VERSION')
|
|
|
|
def getPeerCurVersion(self):
|
|
return self.getVersion('CONFIG_PEER_CUR_VERSION')
|
|
|
|
def ScheduleWorldConfig(self,
|
|
peerMinVersion = None,
|
|
peerCurVersion = None,
|
|
contextMinVersion = None,
|
|
contextCurVersion = None):
|
|
'''properties sorted by the order in which they are defined in
|
|
the sync and sync source property registry'''
|
|
if peerMinVersion == None:
|
|
peerMinVersion = self.getPeerMinVersion()
|
|
if peerCurVersion == None:
|
|
peerCurVersion = self.getPeerCurVersion()
|
|
if contextMinVersion == None:
|
|
contextMinVersion = self.getContextMinVersion()
|
|
if contextCurVersion == None:
|
|
contextCurVersion = self.getContextCurVersion()
|
|
|
|
return '''peers/scheduleworld/.internal.ini:peerMinVersion = {0}
|
|
peers/scheduleworld/.internal.ini:peerCurVersion = {1}
|
|
peers/scheduleworld/.internal.ini:# HashCode = 0
|
|
peers/scheduleworld/.internal.ini:# ConfigDate =
|
|
peers/scheduleworld/.internal.ini:# lastNonce =
|
|
peers/scheduleworld/.internal.ini:# deviceData =
|
|
peers/scheduleworld/.internal.ini:# webDAVCredentialsOkay = 0
|
|
peers/scheduleworld/config.ini:syncURL = http://sync.scheduleworld.com/funambol/ds
|
|
peers/scheduleworld/config.ini:# username =
|
|
peers/scheduleworld/config.ini:# password =
|
|
.internal.ini:contextMinVersion = {2}
|
|
.internal.ini:contextCurVersion = {3}
|
|
config.ini:# logdir =
|
|
peers/scheduleworld/config.ini:# loglevel = 0
|
|
peers/scheduleworld/config.ini:# notifyLevel = 3
|
|
peers/scheduleworld/config.ini:# printChanges = 1
|
|
peers/scheduleworld/config.ini:# dumpData = 1
|
|
config.ini:# maxlogdirs = 10
|
|
peers/scheduleworld/config.ini:# autoSync = 0
|
|
peers/scheduleworld/config.ini:# autoSyncInterval = 30M
|
|
peers/scheduleworld/config.ini:# autoSyncDelay = 5M
|
|
peers/scheduleworld/config.ini:# preventSlowSync = 1
|
|
peers/scheduleworld/config.ini:# useProxy = 0
|
|
peers/scheduleworld/config.ini:# proxyHost =
|
|
peers/scheduleworld/config.ini:# proxyUsername =
|
|
peers/scheduleworld/config.ini:# proxyPassword =
|
|
peers/scheduleworld/config.ini:# clientAuthType = md5
|
|
peers/scheduleworld/config.ini:# RetryDuration = 5M
|
|
peers/scheduleworld/config.ini:# RetryInterval = 2M
|
|
peers/scheduleworld/config.ini:# remoteIdentifier =
|
|
peers/scheduleworld/config.ini:# PeerIsClient = 0
|
|
peers/scheduleworld/config.ini:# SyncMLVersion =
|
|
peers/scheduleworld/config.ini:PeerName = ScheduleWorld
|
|
config.ini:deviceId = fixed-devid
|
|
peers/scheduleworld/config.ini:# remoteDeviceId =
|
|
peers/scheduleworld/config.ini:# enableWBXML = 1
|
|
peers/scheduleworld/config.ini:# enableRefreshSync = 0
|
|
peers/scheduleworld/config.ini:# maxMsgSize = 150000
|
|
peers/scheduleworld/config.ini:# maxObjSize = 4000000
|
|
peers/scheduleworld/config.ini:# SSLServerCertificates = {4}
|
|
peers/scheduleworld/config.ini:# SSLVerifyServer = 1
|
|
peers/scheduleworld/config.ini:# SSLVerifyHost = 1
|
|
peers/scheduleworld/config.ini:WebURL = http://www.scheduleworld.com
|
|
peers/scheduleworld/config.ini:IconURI = image://themedimage/icons/services/scheduleworld
|
|
peers/scheduleworld/config.ini:# ConsumerReady = 0
|
|
peers/scheduleworld/config.ini:# peerType =
|
|
peers/scheduleworld/sources/addressbook/.internal.ini:# adminData =
|
|
peers/scheduleworld/sources/addressbook/.internal.ini:# synthesisID = 0
|
|
peers/scheduleworld/sources/addressbook/config.ini:sync = two-way
|
|
peers/scheduleworld/sources/addressbook/config.ini:uri = card3
|
|
sources/addressbook/config.ini:backend = addressbook
|
|
peers/scheduleworld/sources/addressbook/config.ini:syncFormat = text/vcard
|
|
peers/scheduleworld/sources/addressbook/config.ini:# forceSyncFormat = 0
|
|
sources/addressbook/config.ini:# database =
|
|
sources/addressbook/config.ini:# databaseFormat =
|
|
sources/addressbook/config.ini:# databaseUser =
|
|
sources/addressbook/config.ini:# databasePassword =
|
|
peers/scheduleworld/sources/calendar/.internal.ini:# adminData =
|
|
peers/scheduleworld/sources/calendar/.internal.ini:# synthesisID = 0
|
|
peers/scheduleworld/sources/calendar/config.ini:sync = two-way
|
|
peers/scheduleworld/sources/calendar/config.ini:uri = cal2
|
|
sources/calendar/config.ini:backend = calendar
|
|
peers/scheduleworld/sources/calendar/config.ini:# syncFormat =
|
|
peers/scheduleworld/sources/calendar/config.ini:# forceSyncFormat = 0
|
|
sources/calendar/config.ini:# database =
|
|
sources/calendar/config.ini:# databaseFormat =
|
|
sources/calendar/config.ini:# databaseUser =
|
|
sources/calendar/config.ini:# databasePassword =
|
|
peers/scheduleworld/sources/memo/.internal.ini:# adminData =
|
|
peers/scheduleworld/sources/memo/.internal.ini:# synthesisID = 0
|
|
peers/scheduleworld/sources/memo/config.ini:sync = two-way
|
|
peers/scheduleworld/sources/memo/config.ini:uri = note
|
|
sources/memo/config.ini:backend = memo
|
|
peers/scheduleworld/sources/memo/config.ini:# syncFormat =
|
|
peers/scheduleworld/sources/memo/config.ini:# forceSyncFormat = 0
|
|
sources/memo/config.ini:# database =
|
|
sources/memo/config.ini:# databaseFormat =
|
|
sources/memo/config.ini:# databaseUser =
|
|
sources/memo/config.ini:# databasePassword =
|
|
peers/scheduleworld/sources/todo/.internal.ini:# adminData =
|
|
peers/scheduleworld/sources/todo/.internal.ini:# synthesisID = 0
|
|
peers/scheduleworld/sources/todo/config.ini:sync = two-way
|
|
peers/scheduleworld/sources/todo/config.ini:uri = task2
|
|
sources/todo/config.ini:backend = todo
|
|
peers/scheduleworld/sources/todo/config.ini:# syncFormat =
|
|
peers/scheduleworld/sources/todo/config.ini:# forceSyncFormat = 0
|
|
sources/todo/config.ini:# database =
|
|
sources/todo/config.ini:# databaseFormat =
|
|
sources/todo/config.ini:# databaseUser =
|
|
sources/todo/config.ini:# databasePassword = '''.format(
|
|
peerMinVersion, peerCurVersion,
|
|
contextMinVersion, contextCurVersion,
|
|
self.getSSLServerCertificates())
|
|
|
|
def DefaultConfig(self):
|
|
config = self.ScheduleWorldConfig()
|
|
config = config.replace("syncURL = http://sync.scheduleworld.com/funambol/ds",
|
|
"syncURL = http://yourserver:port",
|
|
1)
|
|
config = config.replace("http://www.scheduleworld.com",
|
|
"http://www.syncevolution.org",
|
|
1)
|
|
config = config.replace("ScheduleWorld",
|
|
"SyncEvolution")
|
|
config = config.replace("scheduleworld",
|
|
"syncevolution")
|
|
config = config.replace("PeerName = SyncEvolution",
|
|
"# PeerName = ",
|
|
1)
|
|
config = config.replace("# ConsumerReady = 0",
|
|
"ConsumerReady = 1",
|
|
1)
|
|
config = config.replace("uri = card3",
|
|
"uri = addressbook",
|
|
1)
|
|
config = config.replace("uri = cal2",
|
|
"uri = calendar",
|
|
1)
|
|
config = config.replace("uri = task2",
|
|
"uri = todo",
|
|
1)
|
|
config = config.replace("uri = note",
|
|
"uri = memo",
|
|
1)
|
|
config = config.replace("syncFormat = text/vcard",
|
|
"# syncFormat = ",
|
|
1)
|
|
return config
|
|
|
|
def FunambolConfig(self):
|
|
config = self.ScheduleWorldConfig()
|
|
config = config.replace("/scheduleworld/",
|
|
"/funambol/")
|
|
config = config.replace("PeerName = ScheduleWorld",
|
|
"PeerName = Funambol")
|
|
config = config.replace("syncURL = http://sync.scheduleworld.com/funambol/ds",
|
|
"syncURL = https://onemediahub.com/sync",
|
|
1)
|
|
config = config.replace("WebURL = http://www.scheduleworld.com",
|
|
"WebURL = https://onemediahub.com",
|
|
1)
|
|
config = config.replace("IconURI = image://themedimage/icons/services/scheduleworld",
|
|
"IconURI = image://themedimage/icons/services/funambol",
|
|
1)
|
|
config = config.replace("# ConsumerReady = 0",
|
|
"ConsumerReady = 1",
|
|
1)
|
|
config = config.replace("# enableWBXML = 1",
|
|
"enableWBXML = 0",
|
|
1)
|
|
config = config.replace("# enableRefreshSync = 0",
|
|
"enableRefreshSync = 1",
|
|
1)
|
|
config = config.replace("# RetryInterval = 2M",
|
|
"RetryInterval = 0",
|
|
1)
|
|
config = config.replace("addressbook/config.ini:uri = card3",
|
|
"addressbook/config.ini:uri = card",
|
|
1)
|
|
config = config.replace("addressbook/config.ini:syncFormat = text/vcard",
|
|
"addressbook/config.ini:# syncFormat = ")
|
|
config = config.replace("calendar/config.ini:uri = cal2",
|
|
"calendar/config.ini:uri = event",
|
|
1)
|
|
config = config.replace("calendar/config.ini:# syncFormat = ",
|
|
"calendar/config.ini:syncFormat = text/calendar")
|
|
config = config.replace("calendar/config.ini:# forceSyncFormat = 0",
|
|
"calendar/config.ini:forceSyncFormat = 1")
|
|
config = config.replace("todo/config.ini:uri = task2",
|
|
"todo/config.ini:uri = task",
|
|
1)
|
|
config = config.replace("todo/config.ini:# syncFormat = ",
|
|
"todo/config.ini:syncFormat = text/calendar")
|
|
config = config.replace("todo/config.ini:# forceSyncFormat = 0",
|
|
"todo/config.ini:forceSyncFormat = 1")
|
|
return config
|
|
|
|
def SynthesisConfig(self):
|
|
config = self.ScheduleWorldConfig()
|
|
config = config.replace("/scheduleworld/",
|
|
"/synthesis/")
|
|
config = config.replace("PeerName = ScheduleWorld",
|
|
"PeerName = Synthesis")
|
|
config = config.replace("syncURL = http://sync.scheduleworld.com/funambol/ds",
|
|
"syncURL = http://www.synthesis.ch/sync",
|
|
1)
|
|
config = config.replace("WebURL = http://www.scheduleworld.com",
|
|
"WebURL = http://www.synthesis.ch",
|
|
1)
|
|
config = config.replace("IconURI = image://themedimage/icons/services/scheduleworld",
|
|
"IconURI = image://themedimage/icons/services/synthesis",
|
|
1)
|
|
config = config.replace("addressbook/config.ini:uri = card3",
|
|
"addressbook/config.ini:uri = contacts",
|
|
1)
|
|
config = config.replace("addressbook/config.ini:syncFormat = text/vcard",
|
|
"addressbook/config.ini:# syncFormat = ")
|
|
config = config.replace("calendar/config.ini:uri = cal2",
|
|
"calendar/config.ini:uri = events",
|
|
1)
|
|
config = config.replace("calendar/config.ini:sync = two-way",
|
|
"calendar/config.ini:sync = disabled",
|
|
1)
|
|
config = config.replace("memo/config.ini:uri = note",
|
|
"memo/config.ini:uri = notes",
|
|
1)
|
|
config = config.replace("todo/config.ini:uri = task2",
|
|
"todo/config.ini:uri = tasks",
|
|
1)
|
|
config = config.replace("todo/config.ini:sync = two-way",
|
|
"todo/config.ini:sync = disabled",
|
|
1)
|
|
return config
|
|
|
|
def OldScheduleWorldConfig(self):
|
|
return '''spds/syncml/config.txt:syncURL = http://sync.scheduleworld.com/funambol/ds
|
|
spds/syncml/config.txt:# username =
|
|
spds/syncml/config.txt:# password =
|
|
spds/syncml/config.txt:# logdir =
|
|
spds/syncml/config.txt:# loglevel = 0
|
|
spds/syncml/config.txt:# notifyLevel = 3
|
|
spds/syncml/config.txt:# printChanges = 1
|
|
spds/syncml/config.txt:# dumpData = 1
|
|
spds/syncml/config.txt:# maxlogdirs = 10
|
|
spds/syncml/config.txt:# autoSync = 0
|
|
spds/syncml/config.txt:# autoSyncInterval = 30M
|
|
spds/syncml/config.txt:# autoSyncDelay = 5M
|
|
spds/syncml/config.txt:# preventSlowSync = 1
|
|
spds/syncml/config.txt:# useProxy = 0
|
|
spds/syncml/config.txt:# proxyHost =
|
|
spds/syncml/config.txt:# proxyUsername =
|
|
spds/syncml/config.txt:# proxyPassword =
|
|
spds/syncml/config.txt:# clientAuthType = md5
|
|
spds/syncml/config.txt:# RetryDuration = 5M
|
|
spds/syncml/config.txt:# RetryInterval = 2M
|
|
spds/syncml/config.txt:# remoteIdentifier =
|
|
spds/syncml/config.txt:# PeerIsClient = 0
|
|
spds/syncml/config.txt:# SyncMLVersion =
|
|
spds/syncml/config.txt:PeerName = ScheduleWorld
|
|
spds/syncml/config.txt:deviceId = fixed-devid
|
|
spds/syncml/config.txt:# remoteDeviceId =
|
|
spds/syncml/config.txt:# enableWBXML = 1
|
|
spds/syncml/config.txt:# enableRefreshSync = 0
|
|
spds/syncml/config.txt:# maxMsgSize = 150000
|
|
spds/syncml/config.txt:# maxObjSize = 4000000
|
|
spds/syncml/config.txt:# SSLServerCertificates = {0}
|
|
spds/syncml/config.txt:# SSLVerifyServer = 1
|
|
spds/syncml/config.txt:# SSLVerifyHost = 1
|
|
spds/syncml/config.txt:WebURL = http://www.scheduleworld.com
|
|
spds/syncml/config.txt:IconURI = image://themedimage/icons/services/scheduleworld
|
|
spds/syncml/config.txt:# ConsumerReady = 0
|
|
spds/sources/addressbook/config.txt:sync = two-way
|
|
spds/sources/addressbook/config.txt:type = addressbook:text/vcard
|
|
spds/sources/addressbook/config.txt:evolutionsource = xyz
|
|
spds/sources/addressbook/config.txt:uri = card3
|
|
spds/sources/addressbook/config.txt:evolutionuser = foo
|
|
spds/sources/addressbook/config.txt:evolutionpassword = bar
|
|
spds/sources/calendar/config.txt:sync = two-way
|
|
spds/sources/calendar/config.txt:type = calendar
|
|
spds/sources/calendar/config.txt:# database =
|
|
spds/sources/calendar/config.txt:uri = cal2
|
|
spds/sources/calendar/config.txt:# evolutionuser =
|
|
spds/sources/calendar/config.txt:# evolutionpassword =
|
|
spds/sources/memo/config.txt:sync = two-way
|
|
spds/sources/memo/config.txt:type = memo
|
|
spds/sources/memo/config.txt:# database =
|
|
spds/sources/memo/config.txt:uri = note
|
|
spds/sources/memo/config.txt:# evolutionuser =
|
|
spds/sources/memo/config.txt:# evolutionpassword =
|
|
spds/sources/todo/config.txt:sync = two-way
|
|
spds/sources/todo/config.txt:type = todo
|
|
spds/sources/todo/config.txt:# database =
|
|
spds/sources/todo/config.txt:uri = task2
|
|
spds/sources/todo/config.txt:# evolutionuser =
|
|
spds/sources/todo/config.txt:# evolutionpassword =
|
|
'''.format(self.getSSLServerCertificates())
|
|
|
|
def replaceLineInConfig(self, config, begin, to):
|
|
index = config.find(begin)
|
|
self.assertNotEqual(index, -1)
|
|
newline = config.find("\n", index + len(begin))
|
|
self.assertNotEqual(newline, -1)
|
|
return config[:index] + to + config[newline:]
|
|
|
|
def removeRandomUUID(self, config):
|
|
return self.replaceLineInConfig(config,
|
|
"deviceId = syncevolution-",
|
|
"deviceId = fixed-devid")
|
|
|
|
def expectUsageError(self, out, err, specific_error):
|
|
'''verify a short usage info was produced and specific error
|
|
message was printed'''
|
|
self.assertTrue(out.startswith("List and manipulate databases:\n"))
|
|
self.assertEqual(out.find("\nOptions:\n"), -1)
|
|
self.assertTrue(out.endswith("Remove item(s):\n" \
|
|
" syncevolution --delete-items [--] <config> <store> (<luid> ... | '*')\n\n"))
|
|
self.assertEqualDiff(specific_error, stripOutput(err))
|
|
|
|
@property('debug', False)
|
|
def testFramework(self):
|
|
"""TestCmdline.testFramework - tests whether utility functions work"""
|
|
content = "baz:line\n" \
|
|
"caz/subdir:booh\n" \
|
|
"caz/subdir2/sub:# comment\n" \
|
|
"caz/subdir2/sub:# foo = bar\n" \
|
|
"caz/subdir2/sub:# empty = \n" \
|
|
"caz/subdir2/sub:# another comment\n" \
|
|
"foo:bar1\n" \
|
|
"foo:\n" \
|
|
"foo: \n" \
|
|
"foo:bar2\n"
|
|
|
|
filtered = "baz:line\n" \
|
|
"caz/subdir:booh\n" \
|
|
"caz/subdir2/sub:# foo = bar\n" \
|
|
"caz/subdir2/sub:# empty = \n" \
|
|
"foo:bar1\n" \
|
|
"foo: \n" \
|
|
"foo:bar2\n"
|
|
|
|
createFiles(self.configdir, content)
|
|
res = scanFiles(self.configdir)
|
|
self.assertEqualDiff(filtered, res)
|
|
randomUUID = "deviceId = syncevolution-blabla\n"
|
|
fixedUUID = "deviceId = fixed-devid\n"
|
|
res = self.removeRandomUUID(randomUUID)
|
|
self.assertEqual(fixedUUID, res)
|
|
|
|
unsorted = "f:g\n" \
|
|
"f:j\n" \
|
|
"a:b\n" \
|
|
"f:a\n" \
|
|
"a/b:a\n"
|
|
expected = "a:b\n" \
|
|
"a/b:a\n" \
|
|
"f:g\n" \
|
|
"f:j\n" \
|
|
"f:a\n"
|
|
res = sortConfig(unsorted)
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
# test DBusUtil.assertEqualDiff()
|
|
try:
|
|
self.assertEqualDiff('foo\nbar\n', 'foo\nxxx\nbar\n')
|
|
except AssertionError, ex:
|
|
expected = '''differences between expected and actual text
|
|
|
|
foo
|
|
+ xxx
|
|
bar
|
|
'''
|
|
self.assertTrue(str(ex).endswith(expected), 'actual exception differs\n' + str(ex))
|
|
else:
|
|
self.fail('''DBusUtil.assertEqualDiff() did not detect diff''')
|
|
|
|
self.assertEqualDiff('foo\nbar', [ 'foo\n', 'bar' ])
|
|
self.assertEqualDiff([ 'foo\n', 'bar' ], 'foo\nbar')
|
|
self.assertEqualDiff([ 'foo\n', 'bar' ], [ 'foo\n', 'bar' ])
|
|
|
|
# test our own regex match
|
|
self.assertRegexpMatchesCustom('foo\nbar\nend', 'bar')
|
|
self.assertRegexpMatchesCustom('foo\nbar\nend', 'b.r')
|
|
self.assertRegexpMatchesCustom('foo\nbar\nend', re.compile('^b.r$', re.MULTILINE))
|
|
try:
|
|
self.assertRegexpMatchesCustom('foo\nbar\nend', 'xxx')
|
|
except AssertionError, ex:
|
|
expected = '''text does not match regex\n\nText:\nfoo\nbar\nend\n\nRegex:\nxxx'''
|
|
self.assertTrue(str(ex).endswith(expected), 'actual exception differs\n' + str(ex))
|
|
else:
|
|
self.fail('''DBusUtil.assertRegexpMatchesCustom() did not fail''')
|
|
self.assertRegexpMatches('foo\nbar\nend', 'bar')
|
|
|
|
haystack = {'in': None}
|
|
|
|
self.assertInCustom('in', 'inside')
|
|
self.assertNotInCustom('in', 'outside')
|
|
self.assertInCustom('in', haystack)
|
|
self.assertNotIn('out', haystack)
|
|
|
|
try:
|
|
self.assertInCustom('in', 'outside')
|
|
except AssertionError, ex:
|
|
expected = "'in' not found in 'outside'"
|
|
self.assertEqual(expected, str(ex))
|
|
else:
|
|
self.fail('''DBusUtil.assertInCustom() did not fail''')
|
|
|
|
try:
|
|
self.assertInCustom('out', haystack)
|
|
except AssertionError, ex:
|
|
expected = "'out' not found in '{'in': None}'"
|
|
self.assertEqual(expected, str(ex))
|
|
else:
|
|
self.fail('''DBusUtil.assertInCustom() did not fail''')
|
|
|
|
try:
|
|
self.assertNotInCustom('in', 'inside')
|
|
except AssertionError, ex:
|
|
expected = "'in' found in 'inside'"
|
|
self.assertEqual(expected, str(ex))
|
|
else:
|
|
self.fail('''DBusUtil.assertNotInCustom() did not fail''')
|
|
|
|
try:
|
|
self.assertNotInCustom('in', haystack)
|
|
except AssertionError, ex:
|
|
expected = "'in' found in '{'in': None}'"
|
|
self.assertEqual(expected, str(ex))
|
|
else:
|
|
self.fail('''DBusUtil.assertNotInCustom() did not fail''')
|
|
|
|
lines = "a\nb\nc\n"
|
|
lastline = "c\n"
|
|
res = lastLine(lines)
|
|
self.assertEqualDiff(lastline, res)
|
|
|
|
message = "[ERROR 12:34:56] msg\n"
|
|
stripped = "[ERROR] msg\n"
|
|
res = stripOutput(message)
|
|
self.assertEqualDiff(stripped, res)
|
|
|
|
# Run command without talking to server, separate streams.
|
|
out, err, code = self.runCmdline(['--foo-bar'],
|
|
sessionFlags=None,
|
|
expectSuccess=False)
|
|
self.assertEqualDiff('[ERROR] --foo-bar: unknown parameter\n', stripOutput(err))
|
|
self.assertRegexpMatches(out, '^List and manipulate databases:\n')
|
|
self.assertEqual(1, code)
|
|
|
|
# Run command without talking to server, joined streams.
|
|
out, err, code = self.runCmdline(['--foo-bar'],
|
|
sessionFlags=None,
|
|
expectSuccess=False,
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertRegexpMatches(stripOutput(out), r'^List and manipulate databases:\n(.*\n)*\[ERROR\] --foo-bar: unknown parameter\n$')
|
|
self.assertEqual(1, code)
|
|
|
|
peerMin = self.getPeerMinVersion()
|
|
peerCur = self.getPeerCurVersion()
|
|
contextMin = self.getContextMinVersion()
|
|
contextCur = self.getContextCurVersion()
|
|
rootMin = self.getRootMinVersion()
|
|
rootCur = self.getRootCurVersion()
|
|
|
|
self.assertRegexpMatches(peerMin, r'^\d+$', 'Peer min version is not a number.')
|
|
self.assertRegexpMatches(peerCur, r'^\d+$', 'Peer cur version is not a number.')
|
|
self.assertRegexpMatches(contextMin, r'^\d+$', 'Context min version is not a number.')
|
|
self.assertRegexpMatches(contextCur, r'^\d+$', 'Context cur version is not a number.')
|
|
self.assertRegexpMatches(rootMin, r'^\d+$', 'Peer min version is not a number.')
|
|
self.assertRegexpMatches(rootCur, r'^\d+$', 'Root cur version is not a number.')
|
|
|
|
def assertSilent(self, out, err, ignore=None):
|
|
if err != None and \
|
|
os.environ.get('SYNCEVOLUTION_DEBUG', None) == None:
|
|
if ignore:
|
|
err = err.replace(ignore, "", 1)
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff('', out)
|
|
|
|
def doSetupScheduleWorld(self, shared):
|
|
root = self.configdir + "/default"
|
|
peer = ""
|
|
|
|
if shared:
|
|
peer = root + "/peers/scheduleworld"
|
|
else:
|
|
peer = root
|
|
|
|
shutil.rmtree(peer, True)
|
|
out, err, code = self.runCmdline(['--configure',
|
|
'--sync-property', 'proxyHost = proxy',
|
|
'scheduleworld', 'addressbook'])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput([('addressbook', ["checking usability...",
|
|
"configuring datastore with sync mode 'two-way'"]),
|
|
('calendar', 'not selected'),
|
|
('memo', 'not selected'),
|
|
('todo', 'not selected')]))
|
|
res = sortConfig(scanFiles(root))
|
|
res = self.removeRandomUUID(res)
|
|
expected = self.ScheduleWorldConfig()
|
|
expected = sortConfig(expected)
|
|
expected = expected.replace("# proxyHost = ",
|
|
"proxyHost = proxy",
|
|
1)
|
|
expected = expected.replace("sync = two-way",
|
|
"sync = disabled")
|
|
expected = expected.replace("addressbook/config.ini:sync = disabled",
|
|
"addressbook/config.ini:sync = two-way",
|
|
1)
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
shutil.rmtree(peer, True)
|
|
out, err, code = self.runCmdline(['--configure',
|
|
'--sync-property', 'deviceId = fixed-devid',
|
|
'scheduleworld'])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput())
|
|
res = sortConfig(scanFiles(root))
|
|
expected = self.ScheduleWorldConfig()
|
|
expected = sortConfig(expected)
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
@property('debug', False)
|
|
def testSetupScheduleWorld(self):
|
|
"""TestCmdline.testSetupScheduleWorld - configure ScheduleWorld"""
|
|
self.doSetupScheduleWorld(False)
|
|
|
|
@property("debug", False)
|
|
def testSetupDefault(self):
|
|
"""TestCmdline.testSetupDefault - configure Default"""
|
|
root = self.configdir + '/default'
|
|
out, err, code = self.runCmdline(['--configure',
|
|
'--template', 'default',
|
|
'--sync-property', 'deviceId = fixed-devid',
|
|
'some-other-server'])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput())
|
|
res = scanFiles(root, 'some-other-server')
|
|
expected = sortConfig(self.DefaultConfig()).replace('/syncevolution/', '/some-other-server/')
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
@property("debug", False)
|
|
def testSetupRenamed(self):
|
|
"""TestCmdline.testSetupRenamed - configure ScheduleWorld with other name"""
|
|
root = self.configdir + "/default"
|
|
args = ["--configure",
|
|
"--template",
|
|
"scheduleworld",
|
|
"--sync-property",
|
|
"deviceId = fixed-devid",
|
|
"scheduleworld2"]
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--template", "scheduleworld",
|
|
"--sync-property", "deviceId = fixed-devid",
|
|
"scheduleworld2"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput())
|
|
res = scanFiles(root, "scheduleworld2")
|
|
expected = sortConfig(self.ScheduleWorldConfig()).replace("/scheduleworld/", "/scheduleworld2/")
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
def doSetupFunambol(self, shared):
|
|
root = self.configdir + "/default"
|
|
peer = ""
|
|
args = ["--configure"]
|
|
|
|
if shared:
|
|
peer = root + "/peers/funambol"
|
|
else:
|
|
peer = root
|
|
args.extend(["--sync-property", "deviceId = fixed-devid"])
|
|
shutil.rmtree(peer, True)
|
|
|
|
args.append("FunamBOL")
|
|
out, err, code = self.runCmdline(args)
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput())
|
|
res = scanFiles(root, "funambol")
|
|
expected = sortConfig(self.FunambolConfig())
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
@property("debug", False)
|
|
def testSetupFunambol(self):
|
|
"""TestCmdline.testSetupFunambol - configure Funambol"""
|
|
self.doSetupFunambol(False)
|
|
|
|
def doSetupSynthesis(self, shared):
|
|
root = self.configdir + "/default"
|
|
peer = ""
|
|
args = ["--configure"]
|
|
|
|
if shared:
|
|
peer = root + "/peers/synthesis"
|
|
else:
|
|
peer = root
|
|
args.extend(["--sync-property", "deviceId = fixed-devid"])
|
|
shutil.rmtree(peer, True)
|
|
|
|
args.append("synthesis")
|
|
out, err, code = self.runCmdline(args)
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput([('addressbook', ["checking usability...", "configuring datastore with sync mode 'two-way'"]),
|
|
('calendar', 'inactive'),
|
|
('memo', ["checking usability...", "configuring datastore with sync mode 'two-way'"]),
|
|
('todo', 'inactive')]))
|
|
res = scanFiles(root, "synthesis")
|
|
expected = sortConfig(self.SynthesisConfig())
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
@property("debug", False)
|
|
def testSetupSynthesis(self):
|
|
"""TestCmdline.testSetupSynthesis - configure Synthesis"""
|
|
self.doSetupSynthesis(False)
|
|
|
|
@property("debug", False)
|
|
def testTemplate(self):
|
|
"""TestCmdline.testTemplate - check --template parameter"""
|
|
out, err, code = self.runCmdline(['--template'],
|
|
sessionFlags=None,
|
|
expectSuccess = False)
|
|
self.expectUsageError(out, err,
|
|
"[ERROR] missing parameter for '--template'\n")
|
|
|
|
expected = "Available configuration templates (servers):\n" \
|
|
" template name = template description\n" \
|
|
" eGroupware = http://www.egroupware.org\n" \
|
|
" Funambol = https://onemediahub.com\n" \
|
|
" Google = event and contact sync via CalDAV/CardDAV, use for the 'target-config@google' config\n" \
|
|
" Goosync = http://www.goosync.com/\n" \
|
|
" Memotoo = http://www.memotoo.com\n" \
|
|
" Mobical = https://www.everdroid.com\n" \
|
|
" Oracle = http://www.oracle.com/technology/products/beehive/index.html\n" \
|
|
" Ovi = http://www.ovi.com\n" \
|
|
" ScheduleWorld = server no longer in operation\n" \
|
|
" SyncEvolution = http://www.syncevolution.org\n" \
|
|
" Synthesis = http://www.synthesis.ch\n" \
|
|
" WebDAV = contact and event sync using WebDAV, use for the 'target-config@<server>' config\n" \
|
|
" Yahoo = contact and event sync using WebDAV, use for the 'target-config@yahoo' config\n"
|
|
out, err, code = self.runCmdline(["--template", "? "])
|
|
self.assertEqualDiff(expected, out)
|
|
self.assertNoErrors(err)
|
|
|
|
@property("ENV", "SYNCEVOLUTION_TEMPLATE_DIR=" + xdg_root + "/templates")
|
|
@property("debug", False)
|
|
def testMatchTemplate(self):
|
|
"""TestCmdline.testMatchTemplate - test template matching"""
|
|
env = copy.deepcopy(self.storedenv)
|
|
env["XDG_CONFIG_HOME"] = "/dev/null"
|
|
self.setUpFiles('templates')
|
|
out, err, code = self.runCmdline(["--template", "?nokia 7210c"], env)
|
|
expected = "Available configuration templates (clients):\n" \
|
|
" template name = template description matching score in percent (100% = exact match)\n" \
|
|
" Nokia_7210c = Template for Nokia S40 series Phone 100%\n" \
|
|
" SyncEvolution_Client = SyncEvolution server side template 40%\n"
|
|
self.assertEqualDiff(expected, out)
|
|
self.assertNoErrors(err)
|
|
|
|
out, err, code = self.runCmdline(["--template", "?nokia"], env)
|
|
self.assertEqualDiff(expected, out)
|
|
self.assertNoErrors(err)
|
|
|
|
out, err, code = self.runCmdline(["--template", "?7210c"], env)
|
|
expected = "Available configuration templates (clients):\n" \
|
|
" template name = template description matching score in percent (100% = exact match)\n" \
|
|
" Nokia_7210c = Template for Nokia S40 series Phone 60%\n" \
|
|
" SyncEvolution_Client = SyncEvolution server side template 20%\n"
|
|
self.assertEqualDiff(expected, out)
|
|
self.assertNoErrors(err)
|
|
|
|
out, err, code = self.runCmdline(["--template", "?syncevolution client"], env)
|
|
expected = "Available configuration templates (clients):\n" \
|
|
" template name = template description matching score in percent (100% = exact match)\n" \
|
|
" SyncEvolution_Client = SyncEvolution server side template 100%\n" \
|
|
" Nokia_7210c = Template for Nokia S40 series Phone 40%\n"
|
|
self.assertEqualDiff(expected, out)
|
|
self.assertNoErrors(err)
|
|
|
|
@property("debug", False)
|
|
def testPrintServers(self):
|
|
"""TestCmdline.testPrintServers - print correct servers"""
|
|
|
|
self.doSetupScheduleWorld(False)
|
|
self.doSetupSynthesis(True)
|
|
self.doSetupFunambol(True)
|
|
|
|
out, err, code = self.runCmdline(["--print-servers"])
|
|
expected = "Configured servers:\n" \
|
|
" funambol = " + os.path.abspath(self.configdir) + "/default/peers/funambol\n" \
|
|
" scheduleworld = " + os.path.abspath(self.configdir) + "/default/peers/scheduleworld\n" \
|
|
" synthesis = " + os.path.abspath(self.configdir) + "/default/peers/synthesis\n"
|
|
self.assertEqualDiff(expected, out)
|
|
self.assertNoErrors(err)
|
|
|
|
@property("debug", False)
|
|
def testPrintConfig(self):
|
|
"""TestCmdline.testPrintConfig - print various configurations"""
|
|
self.doSetupFunambol(False)
|
|
|
|
out, err, code = self.runCmdline(["--print-config"],
|
|
expectSuccess = False)
|
|
self.expectUsageError(out, err,
|
|
"[ERROR] --print-config requires either a --template or a server name.\n")
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "foo"],
|
|
expectSuccess = False)
|
|
self.assertEqualDiff('', out)
|
|
self.assertEqualDiff("[ERROR] Server 'foo' has not been configured yet.\n",
|
|
stripOutput(err))
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "foo"],
|
|
expectSuccess = False)
|
|
self.assertEqualDiff("", out)
|
|
self.assertEqualDiff("[ERROR] No configuration template for 'foo' available.\n",
|
|
stripOutput(err))
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "scheduleworld"])
|
|
self.assertEqualDiff("", err)
|
|
# deviceId must be the one from Funambol
|
|
self.assertIn("deviceId = fixed-devid", out)
|
|
filtered = injectValues(filterConfig(out))
|
|
self.assertEqualDiff(filterConfig(internalToIni(self.ScheduleWorldConfig())),
|
|
filtered)
|
|
# there should have been comments
|
|
self.assertTrue(len(out) > len(filtered))
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "scheduleworld@nosuchcontext"])
|
|
self.assertEqualDiff("", err)
|
|
# deviceId must *not* be the one from Funambol because of the new context
|
|
self.assertNotIn("deviceId = fixed-devid", out)
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "Default"])
|
|
self.assertEqualDiff("", err)
|
|
actual = injectValues(filterConfig(out))
|
|
self.assertIn("deviceId = fixed-devid", actual)
|
|
self.assertEqualDiff(filterConfig(internalToIni(self.DefaultConfig())),
|
|
actual)
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "funambol"])
|
|
self.assertEqualDiff("", err)
|
|
self.assertEqualDiff(filterConfig(internalToIni(self.FunambolConfig())),
|
|
injectValues(filterConfig(out)))
|
|
|
|
# override context and template properties
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "scheduleworld",
|
|
"syncURL=foo",
|
|
"database=Personal",
|
|
"--source-property", "sync=disabled"])
|
|
self.assertNoErrors(err)
|
|
expected = filterConfig(internalToIni(self.ScheduleWorldConfig()))
|
|
expected = expected.replace("syncURL = http://sync.scheduleworld.com/funambol/ds",
|
|
"syncURL = foo",
|
|
1)
|
|
expected = expected.replace("# database = ",
|
|
"database = Personal")
|
|
expected = expected.replace("sync = two-way",
|
|
"sync = disabled")
|
|
actual = injectValues(filterConfig(out))
|
|
self.assertIn("deviceId = fixed-devid", actual)
|
|
self.assertEqualDiff(expected, actual)
|
|
|
|
# override context and template properties, using legacy
|
|
# property name
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "scheduleworld",
|
|
"--sync-property", "syncURL=foo",
|
|
"--source-property", "evolutionsource=Personal",
|
|
"--source-property", "sync=disabled"])
|
|
self.assertNoErrors(err)
|
|
expected = filterConfig(internalToIni(self.ScheduleWorldConfig()))
|
|
expected = expected.replace("syncURL = http://sync.scheduleworld.com/funambol/ds",
|
|
"syncURL = foo",
|
|
1)
|
|
expected = expected.replace("# database = ",
|
|
"database = Personal")
|
|
expected = expected.replace("sync = two-way",
|
|
"sync = disabled")
|
|
actual = injectValues(filterConfig(out))
|
|
self.assertIn("deviceId = fixed-devid", actual)
|
|
self.assertEqualDiff(expected,
|
|
actual)
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "--quiet",
|
|
"--template", "scheduleworld",
|
|
"funambol"])
|
|
self.assertEqualDiff("", err)
|
|
self.assertIn("deviceId = fixed-devid", out)
|
|
self.assertEqualDiff(internalToIni(self.ScheduleWorldConfig()),
|
|
injectValues(filterConfig(out)))
|
|
|
|
# change shared source properties, then check template again
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "database=Personal",
|
|
"funambol"])
|
|
self.assertEqualDiff("", err)
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "--quiet",
|
|
"--template", "scheduleworld",
|
|
"funambol"])
|
|
self.assertEqualDiff("", err)
|
|
# from modified Funambol config
|
|
expected = filterConfig(internalToIni(self.ScheduleWorldConfig())).replace("# database = ",
|
|
"database = Personal")
|
|
actual = injectValues(filterConfig(out))
|
|
self.assertIn("deviceId = fixed-devid", actual)
|
|
self.assertEqualDiff(expected, actual)
|
|
|
|
# print config => must not use settings from default context
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "scheduleworld@nosuchcontext"])
|
|
self.assertEqualDiff("", err)
|
|
expected = filterConfig(internalToIni(self.ScheduleWorldConfig()))
|
|
actual = injectValues(filterConfig(out))
|
|
# source settings *not* from modified Funambol config
|
|
self.assertNotIn("deviceId = fixed-devid", actual)
|
|
actual = self.removeRandomUUID(actual)
|
|
self.assertEqualDiff(expected, actual)
|
|
|
|
# create config => again, must not use settings from default context
|
|
out, err, code = self.runCmdline(["--configure", "--template", "scheduleworld", "other@other"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput())
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "other@other"])
|
|
self.assertEqualDiff("", err)
|
|
# source settings *not* from modified Funambol config
|
|
expected = filterConfig(internalToIni(self.ScheduleWorldConfig()))
|
|
actual = injectValues(filterConfig(out))
|
|
self.assertNotIn("deviceId = fixed-devid", actual)
|
|
actual = self.removeRandomUUID(actual)
|
|
self.assertEqualDiff(expected, actual)
|
|
|
|
def doPrintFileTemplates(self):
|
|
'''Compare only the properties which are really set'''
|
|
|
|
# note that "backend" will be taken from the @default context
|
|
# if one exists, so run this before setting up Funambol below
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "google"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff(google,
|
|
removeComments(self.removeRandomUUID(filterConfig(out))))
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "yahoo"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff(yahoo,
|
|
removeComments(self.removeRandomUUID(filterConfig(out))))
|
|
|
|
self.doSetupFunambol(False)
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "--template", "scheduleworld"])
|
|
self.assertNoErrors(err)
|
|
# deviceId must be the one from Funambol
|
|
self.assertIn("deviceId = fixed-devid", out)
|
|
filtered = injectValues(filterConfig(out))
|
|
self.assertEqualDiff(filterConfig(internalToIni(self.ScheduleWorldConfig())),
|
|
filtered)
|
|
# there should have been comments
|
|
self.assertTrue(len(out) > len(filtered))
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "funambol"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff(filterConfig(internalToIni(self.FunambolConfig())),
|
|
injectValues(filterConfig(out)))
|
|
|
|
# Use local copy of templates in build dir if they exist (no need to install);
|
|
# this assumes that test-dbus.py is run in the build "src"
|
|
# directory.
|
|
@property("ENV", os.path.exists('./templates') and "SYNCEVOLUTION_TEMPLATE_DIR=./templates" or "" )
|
|
@property("debug", False)
|
|
def testPrintFileTemplates(self):
|
|
"""TestCmdline.testPrintFileTemplates - print file templates"""
|
|
self.doPrintFileTemplates()
|
|
|
|
# Disable reading default templates, rely on finding the user
|
|
# ones (set up below via symlink).
|
|
@property("ENV", "SYNCEVOLUTION_TEMPLATE_DIR=/dev/null")
|
|
@property("debug", False)
|
|
def testPrintFileTemplatesConfig(self):
|
|
"""TestCmdline.testPrintFileTemplatesConfig - print file templates from user home directory"""
|
|
# The user's "home directory" or more precisely, his
|
|
# XDG_CONFIG_HOME, is set to xdg_root + "config". Make sure
|
|
# that there is a "syncevolution-templates" with valid
|
|
# templates in that "config" directory, because "templates"
|
|
# will not be found otherwise (SYNCEVOLUTION_TEMPLATE_DIR
|
|
# doesn't point to it).
|
|
os.makedirs(xdg_root + "/config")
|
|
# Use same "./templates" as in testPrintFileTemplates().
|
|
systemtemplates = "/usr/share/syncevolution/templates"
|
|
xdgtemplates = xdg_root + "/config/syncevolution-templates"
|
|
if os.path.exists("./templates"):
|
|
os.symlink("../../templates", xdgtemplates)
|
|
elif os.path.exists(systemtemplates):
|
|
os.symlink(systemtemplates, xdgtemplates)
|
|
self.doPrintFileTemplates()
|
|
|
|
@property("debug", False)
|
|
def testAddSource(self):
|
|
'''TestCmdline.testAddSource - add a source'''
|
|
self.doSetupScheduleWorld(False)
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "uri = dummy",
|
|
"scheduleworld",
|
|
"xyz"])
|
|
res = scanFiles(self.configdir + "/default")
|
|
expected = sortConfig(self.ScheduleWorldConfig() + """
|
|
peers/scheduleworld/sources/xyz/.internal.ini:# adminData =
|
|
peers/scheduleworld/sources/xyz/.internal.ini:# synthesisID = 0
|
|
peers/scheduleworld/sources/xyz/config.ini:# sync = disabled
|
|
peers/scheduleworld/sources/xyz/config.ini:uri = dummy
|
|
peers/scheduleworld/sources/xyz/config.ini:# syncFormat =
|
|
peers/scheduleworld/sources/xyz/config.ini:# forceSyncFormat = 0
|
|
sources/xyz/config.ini:# backend = select backend
|
|
sources/xyz/config.ini:# database =
|
|
sources/xyz/config.ini:# databaseFormat =
|
|
sources/xyz/config.ini:# databaseUser =
|
|
sources/xyz/config.ini:# databasePassword = """)
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
@property("debug", False)
|
|
def testEmptyDatabaseProp(self):
|
|
'''TestCmdline.testEmptyDatabaseProp - add a source with explicit empty database property'''
|
|
self.doSetupScheduleWorld(False)
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"database=",
|
|
"scheduleworld",
|
|
"xyz"])
|
|
res = scanFiles(self.configdir + "/default")
|
|
expected = sortConfig(self.ScheduleWorldConfig() + """
|
|
peers/scheduleworld/sources/xyz/.internal.ini:# adminData =
|
|
peers/scheduleworld/sources/xyz/.internal.ini:# synthesisID = 0
|
|
peers/scheduleworld/sources/xyz/config.ini:# sync = disabled
|
|
peers/scheduleworld/sources/xyz/config.ini:# uri =
|
|
peers/scheduleworld/sources/xyz/config.ini:# syncFormat =
|
|
peers/scheduleworld/sources/xyz/config.ini:# forceSyncFormat = 0
|
|
sources/xyz/config.ini:# backend = select backend
|
|
sources/xyz/config.ini:database =
|
|
sources/xyz/config.ini:# databaseFormat =
|
|
sources/xyz/config.ini:# databaseUser =
|
|
sources/xyz/config.ini:# databasePassword = """)
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
@property("debug", False)
|
|
def testSync(self):
|
|
"""TestCmdline.testSync - check sync with various options"""
|
|
out, err, code = self.runCmdline(["--sync"],
|
|
sessionFlags=None,
|
|
expectSuccess = False)
|
|
self.expectUsageError(out, err,
|
|
"[ERROR] missing parameter for '--sync'\n")
|
|
|
|
out, err, code = self.runCmdline(["--sync", "foo"],
|
|
sessionFlags=None,
|
|
expectSuccess = False)
|
|
self.assertEqualDiff('', out)
|
|
self.assertEqualDiff("[ERROR] '--sync foo': not one of the valid values (two-way, slow, refresh-from-local, refresh-from-remote = refresh, one-way-from-local, one-way-from-remote = one-way, refresh-from-client = refresh-client, refresh-from-server = refresh-server, one-way-from-client = one-way-client, one-way-from-server = one-way-server, local-cache-slow, local-cache-incremental = local-cache, disabled = none)\n",
|
|
stripOutput(err))
|
|
|
|
out, err, code = self.runCmdline(["--sync", " ?"],
|
|
sessionFlags=None)
|
|
self.assertEqualDiff("""--sync
|
|
Requests a certain synchronization mode when initiating a sync:
|
|
|
|
two-way
|
|
only send/receive changes since last sync
|
|
slow
|
|
exchange all items
|
|
refresh-from-remote
|
|
discard all local items and replace with
|
|
the items on the peer
|
|
refresh-from-local
|
|
discard all items on the peer and replace
|
|
with the local items
|
|
one-way-from-remote
|
|
transmit changes from peer
|
|
one-way-from-local
|
|
transmit local changes
|
|
local-cache-slow (server only)
|
|
mirror remote data locally, transferring all data
|
|
local-cache-incremental (server only)
|
|
mirror remote data locally, transferring only changes;
|
|
falls back to local-cache-slow automatically if necessary
|
|
disabled (or none)
|
|
synchronization disabled
|
|
|
|
refresh/one-way-from-server/client are also supported. Their use is
|
|
discouraged because the direction of the data transfer depends
|
|
on the role of the local side (can be server or client), which is
|
|
not always obvious.
|
|
|
|
When accepting a sync session in a SyncML server (HTTP server), only
|
|
datastores with sync != disabled are made available to the client,
|
|
which chooses the final sync mode based on its own configuration.
|
|
When accepting a sync session in a SyncML client (local sync with
|
|
the server contacting SyncEvolution on a device), the sync mode
|
|
specified in the client is typically overriden by the server.
|
|
""",
|
|
out)
|
|
self.assertEqualDiff("", err)
|
|
|
|
out, err, code = self.runCmdline(["--sync", "refresh-from-server"],
|
|
sessionFlags=None,
|
|
expectSuccess = False)
|
|
self.expectUsageError(out, err,
|
|
"[ERROR] No configuration name specified.\n")
|
|
|
|
out, err, code = self.runCmdline(["--source-property", "sync=refresh"],
|
|
sessionFlags=None,
|
|
expectSuccess = False)
|
|
self.expectUsageError(out, err,
|
|
"[ERROR] No configuration name specified.\n")
|
|
|
|
out, err, code = self.runCmdline(["--source-property", "xyz=1"],
|
|
sessionFlags=None,
|
|
expectSuccess = False)
|
|
self.assertEqualDiff('', out)
|
|
self.assertEqualDiff("[ERROR] '--source-property xyz=1': no such property\n",
|
|
stripOutput(err))
|
|
|
|
out, err, code = self.runCmdline(["xyz=1"],
|
|
sessionFlags=None,
|
|
expectSuccess = False)
|
|
self.expectUsageError(out, err,
|
|
"[ERROR] unrecognized property in 'xyz=1'\n")
|
|
|
|
out, err, code = self.runCmdline(["=1"],
|
|
sessionFlags=None,
|
|
expectSuccess = False)
|
|
self.expectUsageError(out, err,
|
|
"[ERROR] a property name must be given in '=1'\n")
|
|
|
|
# scan output from "backend=?" to determine whether CalDAV/CardDAV are enabled
|
|
def isWebDAVEnabled(self):
|
|
'''scan output from "backend=?" to determine whether CalDAV/CardDAV are enabled'''
|
|
out, err, code = self.runCmdline(["backend=?"],
|
|
sessionFlags=None,
|
|
expectSuccess = True)
|
|
inactive = re.search(r'Currently inactive:.* CalDAV', out, re.DOTALL)
|
|
active = re.search(r' CalDAV.*Currently inactive:', out, re.DOTALL)
|
|
# Verify regex and indirectly the output.
|
|
self.assertTrue(inactive or active)
|
|
return active
|
|
|
|
@property("debug", False)
|
|
def testWebDAV(self):
|
|
"""TestCmdline.testWebDAV - configure and print WebDAV configs"""
|
|
|
|
# configure Yahoo under a different name, with explicit
|
|
# template selection
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--template", "yahoo",
|
|
"target-config@my-yahoo"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput([('addressbook', 'inactive'),
|
|
('calendar', "configuring datastore with sync mode 'two-way'")]))
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "target-config@my-yahoo"])
|
|
self.assertNoErrors(err)
|
|
davenabled = self.isWebDAVEnabled()
|
|
if davenabled:
|
|
self.assertEqualDiff(yahoo,
|
|
removeComments(self.removeRandomUUID(filterConfig(out))))
|
|
else:
|
|
self.assertEqualDiff(yahoo.replace("sync = two-way", "sync = disabled"),
|
|
removeComments(self.removeRandomUUID(filterConfig(out))))
|
|
|
|
# configure Google Calendar/Contacts with template derived from config name
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"target-config@google"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput([('addressbook', "configuring datastore with sync mode 'two-way'"),
|
|
('calendar', "configuring datastore with sync mode 'two-way'")]))
|
|
|
|
out, err, code = self.runCmdline(["--print-config", "target-config@google"])
|
|
self.assertNoErrors(err)
|
|
if davenabled:
|
|
self.assertEqualDiff(google,
|
|
removeComments(self.removeRandomUUID(filterConfig(out))))
|
|
else:
|
|
self.assertEqualDiff(google.replace("sync = two-way", "sync = disabled"),
|
|
removeComments(self.removeRandomUUID(filterConfig(out))))
|
|
|
|
# test "template not found" error cases
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--template", "yahooxyz",
|
|
"target-config@my-yahoo-xyz"],
|
|
expectSuccess = False)
|
|
error = """[ERROR] No configuration template for 'yahooxyz' available.
|
|
[INFO]
|
|
[INFO] Available configuration templates (clients and servers):
|
|
"""
|
|
self.assertEqualDiff('', out)
|
|
self.assertTrue(err.startswith(error))
|
|
self.assertTrue(err.endswith("\n"))
|
|
self.assertFalse(err.endswith("\n\n"))
|
|
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"target-config@foobar"],
|
|
expectSuccess = False)
|
|
error = """[ERROR] No configuration template for 'foobar' available.
|
|
[INFO] Use '--template none' and/or specify relevant properties on the command line to create a configuration without a template. Need values for: syncURL
|
|
[INFO]
|
|
[INFO] Available configuration templates (clients and servers):
|
|
"""
|
|
|
|
self.assertEqualDiff('', out)
|
|
self.assertTrue(err.startswith(error))
|
|
self.assertTrue(err.endswith("\n"))
|
|
self.assertFalse(err.endswith("\n\n"))
|
|
|
|
def printConfig(self, server):
|
|
out, err, code = self.runCmdline(["--print-config", server])
|
|
self.assertNoErrors(err)
|
|
return out
|
|
|
|
def doConfigure(self, config, prefix):
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "sync = disabled",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput())
|
|
expected = filterConfig(internalToIni(config)).replace("sync = two-way",
|
|
"sync = disabled")
|
|
self.assertEqualDiff(expected,
|
|
filterConfig(self.printConfig("scheduleworld")))
|
|
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "sync = one-way-from-server",
|
|
"scheduleworld",
|
|
"addressbook"])
|
|
self.assertSilent(out, err)
|
|
expected = config.replace("sync = two-way",
|
|
"sync = disabled")
|
|
expected = expected.replace(prefix + "sync = disabled",
|
|
prefix + "sync = one-way-from-server",
|
|
1)
|
|
expected = filterConfig(internalToIni(expected))
|
|
self.assertEqualDiff(expected,
|
|
filterConfig(self.printConfig("scheduleworld")))
|
|
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--sync", "two-way",
|
|
"-z", "database=source",
|
|
# note priority of suffix: most specific wins
|
|
"--sync-property", "maxlogdirs@scheduleworld@default=20",
|
|
"--sync-property", "maxlogdirs@default=10",
|
|
"--sync-property", "maxlogdirs=5",
|
|
"-y", "LOGDIR@default=logdir",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
expected = expected.replace("sync = one-way-from-server",
|
|
"sync = two-way")
|
|
expected = expected.replace("sync = disabled",
|
|
"sync = two-way")
|
|
expected = expected.replace("# database = ",
|
|
"database = source")
|
|
expected = expected.replace("database = xyz",
|
|
"database = source")
|
|
expected = expected.replace("# maxlogdirs = 10",
|
|
"maxlogdirs = 20")
|
|
expected = expected.replace("# logdir = ",
|
|
"logdir = logdir")
|
|
self.assertEqualDiff(expected,
|
|
filterConfig(self.printConfig("scheduleworld")))
|
|
return expected
|
|
|
|
@property("debug", False)
|
|
def testConfigure(self):
|
|
"""TestCmdline.testConfigure - run configures"""
|
|
self.doSetupScheduleWorld(False)
|
|
|
|
expected = self.doConfigure(self.ScheduleWorldConfig(), "sources/addressbook/config.ini:")
|
|
|
|
# using "type" for peer is mapped to updating "backend",
|
|
# "databaseFormat", "syncFormat", "forceSyncFormat"
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "addressbook/type=file:text/vcard:3.0",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
expected = expected.replace("backend = addressbook",
|
|
"backend = file",
|
|
1)
|
|
expected = expected.replace("# databaseFormat = ",
|
|
"databaseFormat = text/vcard",
|
|
1)
|
|
self.assertEqualDiff(expected,
|
|
filterConfig(self.printConfig("scheduleworld")))
|
|
shared = filterConfig(self.printConfig("@default"))
|
|
self.assertIn("backend = file", shared)
|
|
self.assertIn("databaseFormat = text/vcard", shared)
|
|
|
|
# force sync format
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "addressbook/type=file:text/vcard:3.0!",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
expected = expected.replace('# forceSyncFormat = 0',
|
|
'forceSyncFormat = 1',
|
|
1)
|
|
self.assertEqualDiff(expected,
|
|
filterConfig(self.printConfig("scheduleworld")))
|
|
|
|
# unset format
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "addressbook/type=file",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
expected = expected.replace('databaseFormat = text/vcard',
|
|
'# databaseFormat = ',
|
|
1)
|
|
expected = expected.replace('syncFormat = text/vcard',
|
|
'# syncFormat = ',
|
|
1)
|
|
expected = expected.replace('forceSyncFormat = 1',
|
|
'# forceSyncFormat = 0',
|
|
1)
|
|
self.assertEqualDiff(expected,
|
|
filterConfig(self.printConfig("scheduleworld")))
|
|
|
|
# updating type for context must not affect peer
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "type=file:text/x-vcard:2.1",
|
|
"@default", "addressbook"])
|
|
self.assertSilent(out, err)
|
|
expected = expected.replace("# databaseFormat = ",
|
|
"databaseFormat = text/x-vcard",
|
|
1)
|
|
self.assertEqualDiff(expected,
|
|
filterConfig(self.printConfig("scheduleworld")))
|
|
shared = filterConfig(self.printConfig("@default"))
|
|
self.assertIn("backend = file", shared)
|
|
self.assertIn("databaseFormat = text/x-vcard", shared)
|
|
|
|
syncproperties = """syncURL (no default, unshared, required)
|
|
|
|
username (no default, unshared)
|
|
|
|
password (no default, unshared)
|
|
|
|
logdir (no default, shared)
|
|
|
|
loglevel (0, unshared)
|
|
|
|
notifyLevel (3, unshared)
|
|
|
|
printChanges (TRUE, unshared)
|
|
|
|
dumpData (TRUE, unshared)
|
|
|
|
maxlogdirs (10, shared)
|
|
|
|
autoSync (0, unshared)
|
|
|
|
autoSyncInterval (30M, unshared)
|
|
|
|
autoSyncDelay (5M, unshared)
|
|
|
|
preventSlowSync (TRUE, unshared)
|
|
|
|
useProxy (FALSE, unshared)
|
|
|
|
proxyHost (no default, unshared)
|
|
|
|
proxyUsername (no default, unshared)
|
|
|
|
proxyPassword (no default, unshared)
|
|
|
|
clientAuthType (md5, unshared)
|
|
|
|
RetryDuration (5M, unshared)
|
|
|
|
RetryInterval (2M, unshared)
|
|
|
|
remoteIdentifier (no default, unshared)
|
|
|
|
PeerIsClient (FALSE, unshared)
|
|
|
|
SyncMLVersion (no default, unshared)
|
|
|
|
PeerName (no default, unshared)
|
|
|
|
deviceId (no default, shared)
|
|
|
|
remoteDeviceId (no default, unshared)
|
|
|
|
enableWBXML (TRUE, unshared)
|
|
|
|
enableRefreshSync (FALSE, unshared)
|
|
|
|
maxMsgSize (150000, unshared), maxObjSize (4000000, unshared)
|
|
|
|
SSLServerCertificates ({0}, unshared)
|
|
|
|
SSLVerifyServer (TRUE, unshared)
|
|
|
|
SSLVerifyHost (TRUE, unshared)
|
|
|
|
WebURL (no default, unshared)
|
|
|
|
IconURI (no default, unshared)
|
|
|
|
ConsumerReady (FALSE, unshared)
|
|
|
|
peerType (no default, unshared)
|
|
|
|
defaultPeer (no default, global)
|
|
|
|
keyring (yes, global)
|
|
""".format(self.getSSLServerCertificates())
|
|
|
|
sourceproperties = """sync (disabled, unshared, required)
|
|
|
|
uri (no default, unshared)
|
|
|
|
backend (select backend, shared)
|
|
|
|
syncFormat (no default, unshared)
|
|
|
|
forceSyncFormat (FALSE, unshared)
|
|
|
|
database = evolutionsource (no default, shared)
|
|
|
|
databaseFormat (no default, shared)
|
|
|
|
databaseUser = evolutionuser (no default, shared), databasePassword = evolutionpassword (no default, shared)
|
|
"""
|
|
|
|
# The WORKAROUND lines remove trailing newline from expected
|
|
# output. This should be fixed in Cmdline, I guess. For now I
|
|
# adapted the test to actual output to check if there are
|
|
# other errors.
|
|
|
|
out, err, code = self.runCmdline(["--sync-property", "?"],
|
|
sessionFlags=None)
|
|
self.assertNoErrors(err)
|
|
# WORKAROUND-----------------------vvvvv
|
|
self.assertEqualDiff(syncproperties[:-1],
|
|
filterIndented(out))
|
|
|
|
out, err, code = self.runCmdline(["--source-property", "?"],
|
|
sessionFlags=None)
|
|
self.assertNoErrors(err)
|
|
# WORKAROUND-------------------------vvvvv
|
|
self.assertEqualDiff(sourceproperties[:-1],
|
|
filterIndented(out))
|
|
|
|
out, err, code = self.runCmdline(["--source-property", "?",
|
|
"--sync-property", "?"],
|
|
sessionFlags=None)
|
|
self.assertNoErrors(err)
|
|
# WORKAROUND------------------------------------------vvvvv
|
|
self.assertEqualDiff(sourceproperties + syncproperties[:-1],
|
|
filterIndented(out))
|
|
|
|
out, err, code = self.runCmdline(["--sync-property", "?",
|
|
"--source-property", "?"],
|
|
sessionFlags=None)
|
|
self.assertNoErrors(err)
|
|
# WORKAROUND------------------------------------------vvvvv
|
|
self.assertEqualDiff(syncproperties + sourceproperties[:-1],
|
|
filterIndented(out))
|
|
|
|
out, err, code = self.runCmdline(["--source-property", "sync=?"],
|
|
sessionFlags=None)
|
|
self.assertNoErrors(err)
|
|
# WORKAROUND---------------------------------------vvvvv
|
|
self.assertEqualDiff("'--source-property sync=?'\n"[:-1],
|
|
filterIndented(out))
|
|
|
|
out, err, code = self.runCmdline(["sync=?"],
|
|
sessionFlags=None)
|
|
self.assertNoErrors(err)
|
|
# WORKAROUND---------------------vvvvv
|
|
self.assertEqualDiff("'sync=?'\n"[:-1],
|
|
filterIndented(out))
|
|
|
|
out, err, code = self.runCmdline(["syncURL=?"],
|
|
sessionFlags=None)
|
|
self.assertNoErrors(err)
|
|
# WORKAROUND------------------------vvvvv
|
|
self.assertEqualDiff("'syncURL=?'\n"[:-1],
|
|
filterIndented(out))
|
|
|
|
# test semantic of config creation (instead of updating) with and
|
|
# without templates. See BMC # 14805
|
|
@property("debug", False)
|
|
def testConfigureTemplates(self):
|
|
"""TestCmdline.testConfigureTemplates - make configuration with user provided informations"""
|
|
shutil.rmtree(self.configdir, True)
|
|
|
|
# catch possible typos like "sheduleworld"
|
|
out, err, code = self.runCmdline(["--configure", "foo"],
|
|
expectSuccess = False)
|
|
error = """[ERROR] No configuration template for 'foo@default' available.
|
|
[INFO] Use '--template none' and/or specify relevant properties on the command line to create a configuration without a template. Need values for: syncURL
|
|
[INFO]
|
|
[INFO] Available configuration templates (clients and servers):
|
|
"""
|
|
err = stripOutput(err)
|
|
self.assertTrue(err.startswith(error))
|
|
self.assertTrue(err.endswith("\n"))
|
|
self.assertFalse(err.endswith("\n\n"))
|
|
self.assertEqualDiff('', out)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# catch possible typos like "sheduleworld" when enough
|
|
# properties are specified to continue without a template
|
|
out, err, code = self.runCmdline(["--configure", "syncURL=http://foo.com", "--template", "foo", "bar"],
|
|
expectSuccess = False)
|
|
error = """[ERROR] No configuration template for 'foo' available.
|
|
[INFO] All relevant properties seem to be set, omit the --template parameter to proceed.
|
|
[INFO]
|
|
[INFO] Available configuration templates (clients and servers):
|
|
"""
|
|
err = stripOutput(err)
|
|
self.assertTrue(err.startswith(error))
|
|
self.assertTrue(err.endswith("\n"))
|
|
self.assertFalse(err.endswith("\n\n"))
|
|
self.assertEqualDiff('', out)
|
|
|
|
fooconfig = """syncevolution/.internal.ini:rootMinVersion = {0}
|
|
syncevolution/.internal.ini:rootCurVersion = {1}
|
|
syncevolution/default/.internal.ini:contextMinVersion = {2}
|
|
syncevolution/default/.internal.ini:contextCurVersion = {3}
|
|
syncevolution/default/config.ini:deviceId = fixed-devid
|
|
syncevolution/default/peers/foo/.internal.ini:peerMinVersion = {4}
|
|
syncevolution/default/peers/foo/.internal.ini:peerCurVersion = {5}
|
|
""".format(self.getRootMinVersion(),
|
|
self.getRootCurVersion(),
|
|
self.getContextMinVersion(),
|
|
self.getContextCurVersion(),
|
|
self.getPeerMinVersion(),
|
|
self.getPeerCurVersion())
|
|
syncurl = "syncevolution/default/peers/foo/config.ini:syncURL = local://@bar\n"
|
|
configsource = """syncevolution/default/peers/foo/sources/eds_event/config.ini:sync = two-way
|
|
syncevolution/default/sources/eds_event/config.ini:backend = calendar
|
|
"""
|
|
xdg_config = xdg_root + "/config"
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow users to proceed if they wish: should result in no
|
|
# sources configured
|
|
out, err, code = self.runCmdline(["--configure", "--template", "none", "foo"])
|
|
self.assertSilent(out, err)
|
|
res = filterFiles(self.removeRandomUUID(scanFiles(xdg_config)))
|
|
self.assertEqualDiff(fooconfig, res)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow user to proceed if they wish: should result in no
|
|
# sources configured even if general source properties are
|
|
# specified
|
|
out, err, code = self.runCmdline(["--configure", "--template", "none", "backend=calendar", "foo"])
|
|
self.assertSilent(out, err)
|
|
res = filterFiles(self.removeRandomUUID(scanFiles(xdg_config)))
|
|
self.assertEqualDiff(fooconfig, res)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow user to proceed if they wish: should result in no
|
|
# sources configured, even if specific source properties are
|
|
# specified
|
|
out, err, code = self.runCmdline(["--configure", "--template", "none", "eds_event/backend=calendar", "foo"])
|
|
self.assertSilent(out, err)
|
|
res = filterFiles(self.removeRandomUUID(scanFiles(xdg_config)))
|
|
self.assertEqualDiff(fooconfig, res)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow user to proceed if they wish and possible: here
|
|
# eds_event is not usable
|
|
out, err, code = self.runCmdline(["--configure", "--template", "none", "foo", "eds_event"],
|
|
expectSuccess = False)
|
|
self.assertEqualDiff('', out)
|
|
err = stripOutput(err)
|
|
self.assertEqualDiff('[ERROR] error code from SyncEvolution fatal error (local, status 10500): eds_event: no backend available\n', err)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow user to proceed if they wish and possible: here
|
|
# eds_event is not configurable
|
|
out, err, code = self.runCmdline(["--configure", "syncURL=local://@bar", "foo", "eds_event"],
|
|
expectSuccess = False)
|
|
self.assertEqualDiff('', out)
|
|
err = stripOutput(err)
|
|
self.assertEqualDiff('[ERROR] error code from SyncEvolution fatal error (local, status 10500): no such datastore(s): eds_event\n', err)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow user to proceed if they wish and possible: here
|
|
# eds_event is not configurable (wrong context)
|
|
out, err, code = self.runCmdline(["--configure", "syncURL=local://@bar", "eds_event/backend@xyz=calendar", "foo", "eds_event"],
|
|
expectSuccess = False)
|
|
self.assertEqualDiff('', out)
|
|
err = stripOutput(err)
|
|
self.assertEqualDiff('[ERROR] error code from SyncEvolution fatal error (local, status 10500): no such datastore(s): eds_event\n', err)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow user to proceed if they wish: configure exactly the
|
|
# specified sources
|
|
out, err, code = self.runCmdline(["--configure", "--template", "none", "backend=calendar", "foo", "eds_event"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput('eds_event'))
|
|
res = filterFiles(self.removeRandomUUID(scanFiles(xdg_config)))
|
|
self.assertEqualDiff(fooconfig + configsource, res)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow user to proceed if they provide enough information:
|
|
# should result in no sources configured
|
|
out, err, code = self.runCmdline(["--configure", "syncURL=local://@bar", "foo"])
|
|
self.assertSilent(out, err)
|
|
res = filterFiles(self.removeRandomUUID(scanFiles(xdg_config)))
|
|
self.assertEqualDiff(fooconfig + syncurl, res)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow user to proceed if they provide enough information:
|
|
# source created because listed and usable
|
|
out, err, code = self.runCmdline(["--configure", "syncURL=local://@bar", "backend=calendar", "foo", "eds_event"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput('eds_event'))
|
|
res = filterFiles(self.removeRandomUUID(scanFiles(xdg_config)))
|
|
self.assertEqualDiff(fooconfig + syncurl + configsource, res)
|
|
|
|
shutil.rmtree(self.configdir, True)
|
|
# allow user to proceed if they provide enough information:
|
|
# source created because listed and usable
|
|
out, err, code = self.runCmdline(["--configure", "syncURL=local://@bar", "eds_event/backend@default=calendar", "foo", "eds_event"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput('eds_event'))
|
|
res = filterFiles(self.removeRandomUUID(scanFiles(xdg_config)))
|
|
self.assertEqualDiff(fooconfig + syncurl + configsource, res)
|
|
|
|
@property("debug", False)
|
|
def testConfigureSource(self):
|
|
'''TestCmdline.testConfigureSource - configure some sources'''
|
|
# create from scratch with only addressbook configured
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "database = file://tmp/test",
|
|
"--source-property", "type = file:text/x-vcard",
|
|
"@foobar",
|
|
"addressbook"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput([('addressbook', ('checking usability...', 'configuring datastore'))]))
|
|
root = self.configdir + "/foobar"
|
|
res = self.removeRandomUUID(scanFiles(root))
|
|
expected = '''.internal.ini:contextMinVersion = {0}
|
|
.internal.ini:contextCurVersion = {1}
|
|
config.ini:# logdir =
|
|
config.ini:# maxlogdirs = 10
|
|
config.ini:deviceId = fixed-devid
|
|
sources/addressbook/config.ini:backend = file
|
|
sources/addressbook/config.ini:database = file://tmp/test
|
|
sources/addressbook/config.ini:databaseFormat = text/x-vcard
|
|
sources/addressbook/config.ini:# databaseUser =
|
|
sources/addressbook/config.ini:# databasePassword =
|
|
'''.format(self.getContextMinVersion(),
|
|
self.getContextCurVersion())
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
# add calendar
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "database@foobar = file://tmp/test2",
|
|
"--source-property", "backend = file",
|
|
"--source-property", "databaseFormat = text/calendar",
|
|
"@foobar",
|
|
"calendar"])
|
|
self.assertSilent(out, err)
|
|
|
|
res = self.removeRandomUUID(scanFiles(root))
|
|
expected += '''sources/calendar/config.ini:backend = file
|
|
sources/calendar/config.ini:database = file://tmp/test2
|
|
sources/calendar/config.ini:databaseFormat = text/calendar
|
|
sources/calendar/config.ini:# databaseUser =
|
|
sources/calendar/config.ini:# databasePassword =
|
|
'''
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
# add ScheduleWorld peer: must reuse existing backend settings
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"scheduleworld@foobar"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput())
|
|
|
|
res = self.removeRandomUUID(scanFiles(root))
|
|
expected = self.ScheduleWorldConfig()
|
|
expected = expected.replace("addressbook/config.ini:backend = addressbook",
|
|
"addressbook/config.ini:backend = file")
|
|
expected = expected.replace("addressbook/config.ini:# database = ",
|
|
"addressbook/config.ini:database = file://tmp/test")
|
|
expected = expected.replace("addressbook/config.ini:# databaseFormat = ",
|
|
"addressbook/config.ini:databaseFormat = text/x-vcard")
|
|
expected = expected.replace("calendar/config.ini:backend = calendar",
|
|
"calendar/config.ini:backend = file")
|
|
expected = expected.replace("calendar/config.ini:# database = ",
|
|
"calendar/config.ini:database = file://tmp/test2")
|
|
expected = expected.replace("calendar/config.ini:# databaseFormat = ",
|
|
"calendar/config.ini:databaseFormat = text/calendar")
|
|
expected = sortConfig(expected)
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
# disable all sources except for addressbook
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--source-property", "addressbook/sync=two-way",
|
|
"--source-property", "sync=none",
|
|
"scheduleworld@foobar"])
|
|
self.assertSilent(out, err)
|
|
|
|
res = self.removeRandomUUID(scanFiles(root))
|
|
expected = expected.replace("sync = two-way",
|
|
"sync = disabled")
|
|
expected = expected.replace("sync = disabled",
|
|
"sync = two-way",
|
|
1)
|
|
self.assertEqualDiff(expected, res)
|
|
|
|
# override type in template while creating from scratch
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--template", "SyncEvolution",
|
|
"--source-property", "addressbook/type=file:text/vcard:3.0",
|
|
"--source-property", "addressbook/database=file:///tmp/test",
|
|
"--source-property", "calendar/type=file:text/calendar:2.0",
|
|
"--source-property", "calendar/database=file:///tmp/test",
|
|
"syncevo@syncevo"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput())
|
|
|
|
syncevoroot = self.configdir + "/syncevo"
|
|
res = scanFiles(syncevoroot + "/sources/addressbook")
|
|
self.assertIn("backend = file\n", res)
|
|
self.assertIn("databaseFormat = text/vcard\n", res)
|
|
|
|
res = scanFiles(syncevoroot + "/sources/calendar")
|
|
self.assertIn("backend = file\n", res)
|
|
self.assertIn("databaseFormat = text/calendar\n", res)
|
|
|
|
@property("debug", False)
|
|
def testPrintDatabases(self):
|
|
'''TestCmdline.testPrintDatabases - print some databases'''
|
|
# full output
|
|
out, err, code = self.runCmdline(["--print-databases"])
|
|
# ignore errors about Akonadi not running
|
|
err = re.sub(r'''\[ERROR\] KDE.*\n\[ERROR\] (listing|accessing) databases failed\n\[ERROR\] Akonadi is not running. It can be started with 'akonadictl start'\.\n''', "", err)
|
|
self.assertNoErrors(err)
|
|
# exact output varies, do not test
|
|
|
|
haveEDS = False
|
|
out, err, code = self.runCmdline(["--print-databases", "backend=evolution-contacts"])
|
|
if "not one of the valid values" in err:
|
|
# not enabled, only this error message expected
|
|
self.assertEqualDiff('', out)
|
|
else:
|
|
# enabled, no error, one entry
|
|
haveEDS = True
|
|
self.assertNoErrors(err)
|
|
self.assertTrue(out.startswith("evolution-contacts:\n"))
|
|
entries = 0
|
|
lines = out.splitlines()
|
|
for line in lines:
|
|
if line and not line.startswith(" "):
|
|
entries += 1
|
|
self.assertEqual(1, entries)
|
|
if haveEDS:
|
|
# limit output to one specific backend, chosen via config
|
|
out, err, code = self.runCmdline(["--configure", "backend=evolution-contacts", "@foo-config", "bar-source"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput([('bar-source', ('checking usability...', 'configuring datastore'))]))
|
|
out, err, code = self.runCmdline(["--print-databases", "@foo-config", "bar-source"])
|
|
self.assertNoErrors(err)
|
|
self.assertTrue(out.startswith("@foo-config/bar-source:\n"))
|
|
entries = 0
|
|
lines = out.splitlines()
|
|
for line in lines:
|
|
if line and not line.startswith(" "):
|
|
entries += 1
|
|
self.assertEqual(1, entries)
|
|
|
|
@property("debug", False)
|
|
def testMigrate(self):
|
|
'''TestCmdline.testMigrate - migrate from old configuration'''
|
|
oldroot = xdg_root + "/.sync4j/evolution/scheduleworld"
|
|
newroot = self.configdir + "/default"
|
|
oldconfig = self.OldScheduleWorldConfig()
|
|
|
|
# migrate old config
|
|
createFiles(oldroot, oldconfig)
|
|
createdoldconfig = scanFiles(oldroot)
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(newroot)
|
|
expected = sortConfig(self.ScheduleWorldConfig())
|
|
# migrating SyncEvolution < 1.2 configs sets ConsumerReady, to
|
|
# keep config visible in the updated sync-ui
|
|
expected = expected.replace("# ConsumerReady = 0",
|
|
"ConsumerReady = 1")
|
|
expected = expected.replace("# database = ",
|
|
"database = xyz",
|
|
1)
|
|
expected = expected.replace("# databaseUser = ",
|
|
"databaseUser = foo",
|
|
1)
|
|
# syncevo-dbus-server always uses keyring and doesn't
|
|
# return plain-text password.
|
|
expected = expected.replace("# databasePassword = ",
|
|
"databasePassword = -",
|
|
1)
|
|
# migrating "type" sets forceSyncFormat (if non-standard) and
|
|
# databaseFormat (if format was part of type, as for
|
|
# addressbook
|
|
expected = expected.replace("# databaseFormat = ",
|
|
"databaseFormat = text/vcard",
|
|
1)
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(oldroot + ".old")
|
|
self.assertEqualDiff(createdoldconfig, renamedconfig)
|
|
|
|
# rewrite existing config with obsolete properties => these
|
|
# properties should get removed
|
|
#
|
|
# There is one limitation: shared nodes are not
|
|
# rewritten. This is acceptable.
|
|
createFiles(newroot + "/peers/scheduleworld",
|
|
'''config.ini:# obsolete comment
|
|
config.ini:obsoleteprop = foo
|
|
''',
|
|
True)
|
|
createdconfig = scanFiles(newroot, "scheduleworld")
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(newroot, "scheduleworld")
|
|
expected = sortConfig(self.ScheduleWorldConfig())
|
|
expected = expected.replace("# ConsumerReady = 0", "ConsumerReady = 1")
|
|
expected = expected.replace("# database = ", "database = xyz", 1)
|
|
expected = expected.replace("# databaseUser = ", "databaseUser = foo", 1)
|
|
# uses keyring
|
|
expected = expected.replace("# databasePassword = ", "databasePassword = -", 1)
|
|
expected = expected.replace("# databaseFormat = ", "databaseFormat = text/vcard", 1)
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(newroot, "scheduleworld.old.1")
|
|
createdconfig = createdconfig.replace("ConsumerReady = 1", "ConsumerReady = 0", 1)
|
|
createdconfig = createdconfig.replace("/scheduleworld/", "/scheduleworld.old.1/")
|
|
self.assertEqualDiff(createdconfig, renamedconfig)
|
|
|
|
# migrate old config with changes and .synthesis directory, a
|
|
# second time
|
|
createFiles(oldroot, oldconfig)
|
|
createFiles(oldroot,
|
|
'''.synthesis/dummy-file.bfi:dummy = foobar
|
|
spds/sources/addressbook/changes/config.txt:foo = bar
|
|
spds/sources/addressbook/changes/config.txt:foo2 = bar2
|
|
''',
|
|
True)
|
|
createdconfig = scanFiles(oldroot)
|
|
shutil.rmtree(newroot, True)
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(newroot)
|
|
expected = sortConfig(self.ScheduleWorldConfig())
|
|
expected = expected.replace("# ConsumerReady = 0",
|
|
"ConsumerReady = 1")
|
|
expected = expected.replace("# database = ",
|
|
"database = xyz",
|
|
1)
|
|
expected = expected.replace("# databaseUser = ",
|
|
"databaseUser = foo",
|
|
1)
|
|
# uses keyring
|
|
expected = expected.replace("# databasePassword = ",
|
|
"databasePassword = -",
|
|
1)
|
|
expected = expected.replace("# databaseFormat = ",
|
|
"databaseFormat = text/vcard",
|
|
1)
|
|
expected = expected.replace("peers/scheduleworld/sources/addressbook/config.ini",
|
|
'''peers/scheduleworld/sources/addressbook/.other.ini:foo = bar
|
|
peers/scheduleworld/sources/addressbook/.other.ini:foo2 = bar2
|
|
peers/scheduleworld/sources/addressbook/config.ini''',
|
|
1)
|
|
expected = expected.replace("peers/scheduleworld/config.ini",
|
|
'''peers/scheduleworld/.synthesis/dummy-file.bfi:dummy = foobar
|
|
peers/scheduleworld/config.ini''',
|
|
1)
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(oldroot + ".old.1")
|
|
createdconfig = createdconfig.replace("ConsumerReady = 1",
|
|
"ConsumerReady = 0",
|
|
1)
|
|
self.assertEqualDiff(createdconfig, renamedconfig)
|
|
|
|
otherroot = self.configdir + "/other"
|
|
shutil.rmtree(otherroot, True)
|
|
|
|
# migrate old config into non-default context
|
|
createFiles(oldroot, oldconfig)
|
|
createdconfig = scanFiles(oldroot)
|
|
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld@other"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(otherroot)
|
|
expected = sortConfig(self.ScheduleWorldConfig())
|
|
expected = expected.replace("# ConsumerReady = 0",
|
|
"ConsumerReady = 1")
|
|
expected = expected.replace("# database = ",
|
|
"database = xyz",
|
|
1)
|
|
expected = expected.replace("# databaseUser = ",
|
|
"databaseUser = foo",
|
|
1)
|
|
# uses keyring
|
|
expected = expected.replace("# databasePassword = ",
|
|
"databasePassword = -",
|
|
1)
|
|
expected = expected.replace("# databaseFormat = ",
|
|
"databaseFormat = text/vcard",
|
|
1)
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(oldroot + ".old")
|
|
self.assertEqualDiff(createdconfig, renamedconfig)
|
|
|
|
# migrate the migrated config again inside the "other"
|
|
# context, with no "default" context which might interfere
|
|
# with the tests
|
|
|
|
# ConsumerReady was set as part of previous migration, must be
|
|
# removed during migration to hide the migrated config from
|
|
# average users.
|
|
shutil.rmtree(newroot, True)
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld@other"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(otherroot, "scheduleworld")
|
|
expected = sortConfig(self.ScheduleWorldConfig())
|
|
expected = expected.replace("# ConsumerReady = 0",
|
|
"ConsumerReady = 1")
|
|
expected = expected.replace("# database = ",
|
|
"database = xyz",
|
|
1)
|
|
expected = expected.replace("# databaseUser = ",
|
|
"databaseUser = foo",
|
|
1)
|
|
# uses keyring
|
|
expected = expected.replace("# databasePassword = ",
|
|
"databasePassword = -",
|
|
1)
|
|
expected = expected.replace("# databaseFormat = ",
|
|
"databaseFormat = text/vcard",
|
|
1)
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(otherroot, "scheduleworld.old.3")
|
|
expected = expected.replace("/scheduleworld/",
|
|
"/scheduleworld.old.3/")
|
|
expected = expected.replace("ConsumerReady = 1",
|
|
"ConsumerReady = 0")
|
|
self.assertEqualDiff(expected, renamedconfig)
|
|
|
|
# migrate once more, this time without the explicit context in
|
|
# the config name => must not change the context, need second
|
|
# .old dir
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
migratedconfig = scanFiles(otherroot, "scheduleworld")
|
|
expected = expected.replace("/scheduleworld.old.3/",
|
|
"/scheduleworld/")
|
|
expected = expected.replace("ConsumerReady = 0",
|
|
"ConsumerReady = 1")
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(otherroot, "scheduleworld.old.4")
|
|
expected = expected.replace("/scheduleworld/",
|
|
"/scheduleworld.old.4/")
|
|
expected = expected.replace("ConsumerReady = 1",
|
|
"ConsumerReady = 0")
|
|
self.assertEqualDiff(expected, renamedconfig)
|
|
|
|
# remove ConsumerReady: must remain unset when migrating
|
|
# hidden Syncevolution >= 1.2 configs
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--sync-property", "ConsumerReady=0",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
|
|
# migrate once more => keep ConsumerReady unset
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(otherroot, "scheduleworld")
|
|
expected = expected.replace("/scheduleworld.old.4/",
|
|
"/scheduleworld/")
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(otherroot, "scheduleworld.old.5")
|
|
expected = expected.replace("/scheduleworld/",
|
|
"/scheduleworld.old.5/")
|
|
self.assertEqualDiff(expected, renamedconfig)
|
|
|
|
@property("debug", False)
|
|
def testMigrateSyncFormat(self):
|
|
'''TestCmdline.testMigrateSyncFormat - migrate from old configuration with forced format'''
|
|
oldroot = xdg_root + "/.sync4j/evolution/scheduleworld"
|
|
newroot = self.configdir + "/default"
|
|
oldconfig = self.OldScheduleWorldConfig()
|
|
oldconfig = oldconfig.replace('type = addressbook:text/vcard',
|
|
'type = addressbook:text/vcard!',
|
|
1)
|
|
|
|
# migrate old config
|
|
createFiles(oldroot, oldconfig)
|
|
createdoldconfig = scanFiles(oldroot)
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(newroot)
|
|
expected = sortConfig(self.ScheduleWorldConfig())
|
|
# migrating SyncEvolution < 1.2 configs sets ConsumerReady, to
|
|
# keep config visible in the updated sync-ui
|
|
expected = expected.replace("# ConsumerReady = 0",
|
|
"ConsumerReady = 1")
|
|
expected = expected.replace("# database = ",
|
|
"database = xyz",
|
|
1)
|
|
expected = expected.replace("# databaseUser = ",
|
|
"databaseUser = foo",
|
|
1)
|
|
# syncevo-dbus-server always uses keyring and doesn't
|
|
# return plain-text password.
|
|
expected = expected.replace("# databasePassword = ",
|
|
"databasePassword = -",
|
|
1)
|
|
# migrating "type" sets forceSyncFormat (if non-standard) and
|
|
# databaseFormat (if format was part of type, as for
|
|
# addressbook
|
|
expected = expected.replace("# databaseFormat = ",
|
|
"databaseFormat = text/vcard",
|
|
1)
|
|
expected = expected.replace("# forceSyncFormat = 0",
|
|
"forceSyncFormat = 1",
|
|
1)
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(oldroot + ".old")
|
|
self.assertEqualDiff(createdoldconfig, renamedconfig)
|
|
|
|
@property("debug", False)
|
|
def testMigrateContext(self):
|
|
'''TestCmdline.testMigrateContext - migrate context containing a peer'''
|
|
# Must also migrate peer. Covers special case of inconsistent
|
|
# "type".
|
|
|
|
root = self.configdir + "/default"
|
|
oldconfig = '''config.ini:logDir = none
|
|
peers/scheduleworld/config.ini:syncURL = http://sync.scheduleworld.com/funambol/ds
|
|
peers/scheduleworld/config.ini:# username =
|
|
peers/scheduleworld/config.ini:# password =
|
|
|
|
peers/scheduleworld/sources/addressbook/config.ini:sync = two-way
|
|
peers/scheduleworld/sources/addressbook/config.ini:uri = card3
|
|
peers/scheduleworld/sources/addressbook/config.ini:type = addressbook:text/vcard
|
|
sources/addressbook/config.ini:type = calendar
|
|
|
|
peers/funambol/config.ini:syncURL = http://sync.funambol.com/funambol/ds
|
|
peers/funambol/config.ini:# username =
|
|
peers/funambol/config.ini:# password =
|
|
|
|
peers/funambol/sources/calendar/config.ini:sync = refresh-from-server
|
|
peers/funambol/sources/calendar/config.ini:uri = cal
|
|
peers/funambol/sources/calendar/config.ini:type = calendar
|
|
peers/funambol/sources/addressbook/config.ini:# sync = disabled
|
|
peers/funambol/sources/addressbook/config.ini:type = file
|
|
sources/calendar/config.ini:type = memos
|
|
|
|
peers/memotoo/config.ini:syncURL = http://sync.memotoo.com/memotoo/ds
|
|
peers/memotoo/config.ini:# username =
|
|
peers/memotoo/config.ini:# password =
|
|
|
|
peers/memotoo/sources/memo/config.ini:sync = refresh-from-client
|
|
peers/memotoo/sources/memo/config.ini:uri = cal
|
|
peers/memotoo/sources/memo/config.ini:type = memo:text/plain
|
|
sources/memo/config.ini:type = todo
|
|
'''
|
|
|
|
createFiles(root, oldconfig)
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"memo/backend=file", # override memo "backend" during migration
|
|
"@default"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(root)
|
|
|
|
expectedlines = ["peers/scheduleworld/",
|
|
"sources/addressbook/config.ini:backend = addressbook",
|
|
"sources/addressbook/config.ini:databaseFormat = text/vcard",
|
|
"peers/scheduleworld/sources/addressbook/config.ini:syncFormat = text/vcard",
|
|
"peers/scheduleworld/sources/addressbook/config.ini:sync = two-way",
|
|
"peers/scheduleworld/sources/calendar/config.ini:# sync = disabled",
|
|
"peers/scheduleworld/sources/memo/config.ini:# sync = disabled",
|
|
"sources/calendar/config.ini:backend = calendar",
|
|
"sources/calendar/config.ini:# databaseFormat = ",
|
|
"peers/funambol/sources/calendar/config.ini:# syncFormat = ",
|
|
"peers/funambol/sources/addressbook/config.ini:# sync = disabled",
|
|
"peers/funambol/sources/calendar/config.ini:sync = refresh-from-server",
|
|
"peers/funambol/sources/memo/config.ini:# sync = disabled",
|
|
"sources/memo/config.ini:backend = file",
|
|
"sources/memo/config.ini:databaseFormat = text/plain",
|
|
"peers/memotoo/sources/memo/config.ini:syncFormat = text/plain",
|
|
"peers/memotoo/sources/addressbook/config.ini:# sync = disabled",
|
|
"peers/memotoo/sources/calendar/config.ini:# sync = disabled",
|
|
"peers/memotoo/sources/memo/config.ini:sync = refresh-from-client"]
|
|
|
|
for expectedline in expectedlines:
|
|
self.assertIn(expectedline, migratedconfig)
|
|
|
|
@property("debug", False)
|
|
def testMigrateAutoSync(self):
|
|
'''TestCmdline.testMigrateAutoSync - migrate old configuration files'''
|
|
oldroot = xdg_root + "/.sync4j/evolution/scheduleworld"
|
|
newroot = self.configdir + "/default"
|
|
oldconfig = "spds/syncml/config.txt:autoSync = 1\n" + self.OldScheduleWorldConfig()
|
|
|
|
# migrate old config
|
|
createFiles(oldroot, oldconfig)
|
|
createdconfig = scanFiles(oldroot)
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(newroot)
|
|
expected = self.ScheduleWorldConfig()
|
|
expected = expected.replace("# autoSync = 0",
|
|
"autoSync = 1",
|
|
1)
|
|
expected = sortConfig(expected)
|
|
|
|
# migrating Syncevolution < 1.2 configs sets ConsumerReady, to
|
|
# keep config visible in the updated sync-ui
|
|
expected = expected.replace("# ConsumerReady = 0",
|
|
"ConsumerReady = 1")
|
|
expected = expected.replace("# database = ",
|
|
"database = xyz",
|
|
1)
|
|
expected = expected.replace("# databaseUser = ",
|
|
"databaseUser = foo",
|
|
1)
|
|
# uses keyring
|
|
expected = expected.replace("# databasePassword = ",
|
|
"databasePassword = -",
|
|
1)
|
|
# migrating "type" sets forceSyncFormat (if non-standard) and
|
|
# databaseFormat (if format was part of type, as for
|
|
# addressbook)
|
|
expected = expected.replace("# databaseFormat = ",
|
|
"databaseFormat = text/vcard", 1)
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(oldroot + ".old")
|
|
# autoSync must have been unset
|
|
createdconfig = createdconfig.replace(":autoSync = 1",
|
|
":autoSync = 0",
|
|
1)
|
|
self.assertEqualDiff(createdconfig, renamedconfig)
|
|
|
|
# rewrite existing config with autoSync set
|
|
createdconfig = scanFiles(newroot, "scheduleworld")
|
|
|
|
out, err, code = self.runCmdline(["--migrate",
|
|
"scheduleworld"])
|
|
self.assertSilent(out, err)
|
|
|
|
migratedconfig = scanFiles(newroot, "scheduleworld")
|
|
expected = self.ScheduleWorldConfig()
|
|
expected = expected.replace("# autoSync = 0",
|
|
"autoSync = 1",
|
|
1)
|
|
expected = sortConfig(expected)
|
|
expected = expected.replace("# ConsumerReady = 0",
|
|
"ConsumerReady = 1")
|
|
expected = expected.replace("# database = ",
|
|
"database = xyz",
|
|
1)
|
|
expected = expected.replace("# databaseUser = ",
|
|
"databaseUser = foo",
|
|
1)
|
|
# uses keyring
|
|
expected = expected.replace("# databasePassword = ",
|
|
"databasePassword = -",
|
|
1)
|
|
expected = expected.replace("# databaseFormat = ",
|
|
"databaseFormat = text/vcard",
|
|
1)
|
|
self.assertEqualDiff(expected, migratedconfig)
|
|
renamedconfig = scanFiles(newroot, "scheduleworld.old.1")
|
|
# autoSync must have been unset
|
|
createdconfig = createdconfig.replace(":autoSync = 1",
|
|
":autoSync = 0",
|
|
1)
|
|
# the scheduleworld config was consumer ready, the migrated
|
|
# one isn't
|
|
createdconfig = createdconfig.replace("ConsumerReady = 1",
|
|
"ConsumerReady = 0")
|
|
createdconfig = createdconfig.replace("/scheduleworld/",
|
|
"/scheduleworld.old.1/")
|
|
self.assertEqualDiff(createdconfig, renamedconfig)
|
|
|
|
@property("debug", False)
|
|
def testItemOperations(self):
|
|
'''TestCmdline.testItemOperations - add and print items'''
|
|
# "foo" not configured
|
|
out, err, code = self.runCmdline(["--print-items",
|
|
"foo",
|
|
"bar"],
|
|
expectSuccess = False)
|
|
# Information about supported modules is optional, depends on compilation of
|
|
# SyncEvolution.
|
|
self.assertRegexpMatches(err, r'''\[ERROR\] error code from SyncEvolution error parsing config file \(local, status 20010\): bar: backend not supported (by any of the backend modules \((\S+, )+\S+\) )?or not correctly configured \(backend=select backend databaseFormat= syncFormat=\)\n\[ERROR\] configuration 'foo' does not exist\n\[ERROR\] datastore 'bar' does not exist\n\[ERROR\] backend property not set\n''')
|
|
self.assertEqualDiff('', out)
|
|
|
|
# "foo" not configured, no source named
|
|
out, err, code = self.runCmdline(["--print-items",
|
|
"foo"],
|
|
expectSuccess = False)
|
|
self.assertRegexpMatches(err, r'''\[ERROR\] error code from SyncEvolution error parsing config file \(local, status 20010\): backend not supported (by any of the backend modules \((\S+, )+\S+\) )?or not correctly configured \(backend=select backend databaseFormat= syncFormat=\)\n\[ERROR\] configuration 'foo' does not exist\n\[ERROR\] no datastore selected\n\[ERROR\] backend property not set\n''')
|
|
self.assertEqualDiff('', out)
|
|
|
|
# nothing known about source
|
|
out, err, code = self.runCmdline(["--print-items"],
|
|
expectSuccess = False)
|
|
self.assertRegexpMatches(err, r'''\[ERROR\] error code from SyncEvolution error parsing config file \(local, status 20010\): backend not supported (by any of the backend modules \((\S+, )+\S+\) )?or not correctly configured \(backend=select backend databaseFormat= syncFormat=\)\n\[ERROR\] no datastore selected\n\[ERROR\] backend property not set\n''')
|
|
self.assertEqualDiff('', out)
|
|
|
|
# now create "foo"
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"--template", "default",
|
|
"foo"])
|
|
self.assertSilent(out, err, ignore=self.sourceCheckOutput())
|
|
|
|
# "foo" now configured, still no source
|
|
out, err, code = self.runCmdline(["--print-items",
|
|
"foo"],
|
|
expectSuccess = False)
|
|
self.assertRegexpMatches(err, r'''\[ERROR\] error code from SyncEvolution error parsing config file \(local, status 20010\): backend not supported (by any of the backend modules \((\S+, )+\S+\) )?or not correctly configured \(backend=select backend databaseFormat= syncFormat=\)\n\[ERROR\] no datastore selected\n\[ERROR\] backend property not set\n''')
|
|
self.assertEqualDiff('', out)
|
|
|
|
# "foo" configured, but "bar" is not
|
|
out, err, code = self.runCmdline(["--print-items",
|
|
"foo",
|
|
"bar"],
|
|
expectSuccess = False)
|
|
self.assertRegexpMatches(err, r'''\[ERROR\] error code from SyncEvolution error parsing config file \(local, status 20010\): bar: backend not supported (by any of the backend modules \((\S+, )+\S+\) )?or not correctly configured \(backend=select backend databaseFormat= syncFormat=\)\n\[ERROR\] datastore 'bar' does not exist\n\[ERROR\] backend property not set\n''')
|
|
self.assertEqualDiff('', out)
|
|
|
|
# add "bar" source, using file backend
|
|
out, err, code = self.runCmdline(["--configure",
|
|
"backend=file",
|
|
"database=file://" + xdg_root + "/addressbook",
|
|
"databaseFormat=text/vcard",
|
|
"foo",
|
|
"bar"])
|
|
self.assertSilent(out, err)
|
|
|
|
# no items yet
|
|
out, err, code = self.runCmdline(["--print-items",
|
|
"foo",
|
|
"bar"])
|
|
self.assertSilent(out, err)
|
|
|
|
john = """BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:John Doe
|
|
N:Doe;John;;;
|
|
END:VCARD
|
|
"""
|
|
joan = """BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:Joan Doe
|
|
N:Doe;Joan;;;
|
|
END:VCARD
|
|
"""
|
|
|
|
# create one file
|
|
file1 = "1:" + john
|
|
file2 = "2:" + joan
|
|
file1 = file1.replace("\n", "\n1:")
|
|
file1 = file1[:-2]
|
|
file2 = file2.replace("\n", "\n2:")
|
|
file2 = file2[:-2]
|
|
createFiles(xdg_root + "/addressbook", file1 + file2)
|
|
|
|
out, err, code = self.runCmdline(["--print-items",
|
|
"foo",
|
|
"bar"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff("1\n2\n", out)
|
|
|
|
# alternatively just specify enough parameters without the
|
|
# "foo" "bar" config part
|
|
out, err, code = self.runCmdline(["--print-items",
|
|
"backend=file",
|
|
"database=file://" + xdg_root + "/addressbook",
|
|
"databaseFormat=text/vcard"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff("1\n2\n", out)
|
|
|
|
# export all
|
|
out, err, code = self.runCmdline(["--export", "-",
|
|
"backend=file",
|
|
"database=file://" + xdg_root + "/addressbook",
|
|
"databaseFormat=text/vcard"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff(john + "\n" + joan, out)
|
|
|
|
# export all via config
|
|
out, err, code = self.runCmdline(["--export", "-",
|
|
"foo", "bar"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff(john + "\n" + joan, out)
|
|
|
|
# export all into file
|
|
exportfile = xdg_root + "/export.vcf"
|
|
out, err, code = self.runCmdline(["--export", exportfile,
|
|
"foo", "bar"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff(john + "\n" + joan, open(exportfile).read())
|
|
|
|
# export one
|
|
out, err, code = self.runCmdline(["--export", "-",
|
|
"backend=file",
|
|
"database=file://" + xdg_root + "/addressbook",
|
|
"databaseFormat=text/vcard",
|
|
"--luids", "1"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff(john, out)
|
|
|
|
# export one via config
|
|
out, err, code = self.runCmdline(["--export", "-",
|
|
"foo", "bar", "1"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff(john, out)
|
|
|
|
# export one into file
|
|
exportfile = xdg_root + "/export.vcf"
|
|
out, err, code = self.runCmdline(["--export", exportfile,
|
|
"foo", "bar", "1"])
|
|
self.assertNoErrors(err)
|
|
self.assertEqualDiff(john, open(exportfile).read())
|
|
|
|
# Copied from C++ test:
|
|
# TODO: check configuration of just the source as @foo bar
|
|
# without peer
|
|
|
|
# check error message for missing config name
|
|
out, err, code = self.runCmdline([],
|
|
expectSuccess = False)
|
|
self.expectUsageError(out, err,
|
|
"[ERROR] No configuration name specified.\n")
|
|
|
|
# check error message for missing config name, version II
|
|
out, err, code = self.runCmdline(["--run"],
|
|
expectSuccess = False)
|
|
self.expectUsageError(out, err,
|
|
"[ERROR] No configuration name specified.\n")
|
|
|
|
def stripSyncTime(self, out):
|
|
'''remove varying time from sync session output'''
|
|
p = re.compile(r'^\| +start .*?, duration \d:\d\dmin +\|$',
|
|
re.MULTILINE)
|
|
return p.sub('| start xxx, duration a:bcmin |', out)
|
|
|
|
@property("debug", False)
|
|
@timeout(usingValgrind() and 600 or 200)
|
|
def testSyncOutputErrors(self):
|
|
"""TestCmdline.testSyncOutputErrors - check error messages for sync operations"""
|
|
self.setUpLocalSyncConfigs()
|
|
self.session.Detach()
|
|
|
|
# error message for --dry-run
|
|
out, err, code = self.runCmdline(["--dry-run", "server"],
|
|
expectSuccess=False)
|
|
self.expectUsageError(out, err, '[ERROR] --dry-run not supported for running a synchronization\n')
|
|
|
|
# ambiguous command line
|
|
out, err, code = self.runCmdline(["sync=two-way", "server"],
|
|
expectSuccess=False)
|
|
self.expectUsageError(out, err, "[ERROR] Properties specified, but neither '--configure' nor '--run' - what did you want?\n")
|
|
|
|
# ambiguous command line, empty source property
|
|
out, err, code = self.runCmdline(["database=", "server"],
|
|
expectSuccess=False)
|
|
self.expectUsageError(out, err, "[ERROR] Properties specified, but neither '--configure' nor '--run' - what did you want?\n")
|
|
|
|
# ambiguous command line, empty sync property
|
|
out, err, code = self.runCmdline(["logdir=", "server"],
|
|
expectSuccess=False)
|
|
self.expectUsageError(out, err, "[ERROR] Properties specified, but neither '--configure' nor '--run' - what did you want?\n")
|
|
|
|
# empty global property allowed
|
|
out, err, code = self.runCmdline(["keyring=", "server"],
|
|
sessionFlags=[],
|
|
expectSuccess=True)
|
|
|
|
|
|
@property("debug", False)
|
|
@property("ENV", "SYNCEVOLUTION_SYNC_DELAY=60 LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
@timeout(200)
|
|
def testSyncFailure1(self):
|
|
"""TestCmdline.testSyncFailure1 - check that cmdline notices when sync fails prematurely before it even starts"""
|
|
self.setUpLocalSyncConfigs()
|
|
self.session.Detach()
|
|
|
|
# We must kill the process before the sync starts, to achieve
|
|
# the expected output and sync result. The
|
|
# SYNCEVOLUTION_LOCAL_CHILD_DELAY2 should give us that chance.
|
|
self.killed = False
|
|
def output(path, level, text, procname):
|
|
if self.running and not self.killed and procname == '' and text == 'ready to sync':
|
|
# kill syncevo-local-sync
|
|
for pid, (name, cmdline) in self.getChildren().iteritems():
|
|
if 'syncevo-dbus-helper' in cmdline:
|
|
logging.printf('killing syncevo-dbus-helper with pid %d', pid)
|
|
os.kill(pid, signal.SIGKILL)
|
|
loop.quit()
|
|
self.killed = True
|
|
break
|
|
|
|
receiver = bus.add_signal_receiver(output,
|
|
'LogOutput',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
try:
|
|
s = self.startCmdline(["--sync", "slow", "server"], preserveOutputOrder=True)
|
|
loop.run()
|
|
finally:
|
|
receiver.remove()
|
|
self.assertTrue(self.killed)
|
|
|
|
out, err, code = self.finishCmdline(s, expectSuccess=False, sessionFlags=[])
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(1, code)
|
|
out = self.stripSyncTime(out)
|
|
# The error message is not particularly informative, but the error should
|
|
# not occur, so let it be... Also, the "connection is closed" error only
|
|
# occurs occasionally.
|
|
self.assertNotEqual(out, '')
|
|
self.assertTrue('''[ERROR] The connection is closed
|
|
''' in out or
|
|
'''[ERROR syncevo-dbus-server] child process quit because of signal 9
|
|
''' in out)
|
|
|
|
@property("debug", False)
|
|
@property("ENV", "SYNCEVOLUTION_LOCAL_CHILD_DELAY2=60 LC_ALL=en_US.UTF-8 LANGUAGE=en_US")
|
|
@timeout(200)
|
|
def testSyncFailure2(self):
|
|
"""TestCmdline.testSyncFailure2 - check that cmdline notices when sync fails prematurely in the middle"""
|
|
self.setUpLocalSyncConfigs()
|
|
self.session.Detach()
|
|
|
|
# We must kill the process before the sync starts, to achieve
|
|
# the expected output and sync result. The
|
|
# SYNCEVOLUTION_LOCAL_CHILD_DELAY2 should give us that chance.
|
|
self.killed = False
|
|
def output(path, level, text, procname):
|
|
if self.running and not self.killed and procname == 'remote@client' and text == 'target side of local sync ready':
|
|
# kill syncevo-local-sync
|
|
for pid, (name, cmdline) in self.getChildren().iteritems():
|
|
if 'syncevo-local-sync' in cmdline:
|
|
logging.printf('killing syncevo-local-sync with pid %d', pid)
|
|
os.kill(pid, signal.SIGKILL)
|
|
loop.quit()
|
|
self.killed = True
|
|
break
|
|
|
|
receiver = bus.add_signal_receiver(output,
|
|
'LogOutput',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
try:
|
|
s = self.startCmdline(["--sync", "slow", "server"], preserveOutputOrder=True)
|
|
loop.run()
|
|
finally:
|
|
receiver.remove()
|
|
self.assertTrue(self.killed)
|
|
|
|
out, err, code = self.finishCmdline(s, expectSuccess=False, sessionFlags=[])
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(1, code)
|
|
out = self.stripSyncTime(out)
|
|
out = re.sub(r'giving up after \d+ retries and \d+:\d+min',
|
|
'giving up after x retries and y:zzmin',
|
|
out)
|
|
out = re.sub(r'Synchronization failed, see .*temp-test-dbus/cache/syncevolution/server.*/syncevolution-log.html for details.',
|
|
'Synchronization failed, see syncevolution-log.html for details.',
|
|
out)
|
|
# Two possible outcomes:
|
|
# 1. death of child is noticed first.
|
|
# 2. loss of D-Bus connection is noticed first.
|
|
# Also, the "connection is closed" error only
|
|
# occurs occasionally.
|
|
if out.startswith('''[INFO remote@client] target side of local sync ready
|
|
[ERROR] child process quit because of signal 9'''):
|
|
out = out.replace('''[ERROR] sending message to child failed: The connection is closed
|
|
''', '')
|
|
self.assertEqualDiff(out, '''[INFO remote@client] target side of local sync ready
|
|
[ERROR] child process quit because of signal 9
|
|
[ERROR] local transport failed: child process quit because of signal 9
|
|
[INFO] Transport giving up after x retries and y:zzmin
|
|
[ERROR] transport problem: transport failed, retry period exceeded
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization failed, see syncevolution-log.html for details.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| external transport failure (local, status 20043) |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
First ERROR encountered: child process quit because of signal 9
|
|
|
|
''')
|
|
else:
|
|
self.assertEqualDiff(out, '''[INFO remote@client] target side of local sync ready
|
|
[ERROR] sending message to child failed: The connection is closed
|
|
[INFO] Transport giving up after x retries and y:zzmin
|
|
[ERROR] transport problem: transport failed, retry period exceeded
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization failed, see syncevolution-log.html for details.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| external transport failure (local, status 20043) |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
First ERROR encountered: sending message to child failed: The connection is closed
|
|
|
|
''')
|
|
|
|
@property("debug", False)
|
|
@property("ENV", "SYNCEVOLUTION_SYNC_DELAY=60")
|
|
@timeout(200)
|
|
def testSyncFailure3(self):
|
|
"""TestCmdline.testSyncFailure3 - check that cmdline notices when server dies"""
|
|
self.setUpLocalSyncConfigs()
|
|
self.session.Detach()
|
|
|
|
# We must kill the process before the sync starts, to achieve
|
|
# the expected output and sync result. The
|
|
# SYNCEVOLUTION_LOCAL_CHILD_DELAY2 should give us that chance.
|
|
self.killed = False
|
|
pid = self.serverPid()
|
|
def output(path, level, text, procname):
|
|
if self.running and not self.killed and text == 'ready to sync':
|
|
# kill syncevo-dbus-server (and not the wrapper, because that would
|
|
# also kill the process group)
|
|
logging.printf('killing syncevo-dbus-server with pid %d', pid)
|
|
os.kill(pid, signal.SIGKILL)
|
|
self.killed = True
|
|
loop.quit()
|
|
|
|
receiver = bus.add_signal_receiver(output,
|
|
'LogOutput',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
try:
|
|
s = self.startCmdline(["--sync", "slow", "server"], preserveOutputOrder=True)
|
|
loop.run()
|
|
finally:
|
|
receiver.remove()
|
|
self.assertTrue(self.killed)
|
|
|
|
# Remove syncevo-dbus-server zombie process(es).
|
|
DBusUtil.pserver.wait()
|
|
DBusUtil.pserver = None
|
|
|
|
out, err, code = self.finishCmdline(s, expectSuccess=False, sessionFlags=None)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(1, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff(out, '''[ERROR] Background sync daemon has gone.
|
|
''')
|
|
|
|
@property("debug", False)
|
|
@timeout(usingValgrind() and 600 or 200)
|
|
def testSyncOutput(self):
|
|
"""TestCmdline.testSyncOutput - run syncs between local dirs and check output"""
|
|
self.setUpLocalSyncConfigs()
|
|
self.session.Detach()
|
|
os.makedirs(xdg_root + "/server")
|
|
item = xdg_root + "/server/0"
|
|
output = open(item, "w")
|
|
output.write('''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:John Doe
|
|
N:Doe;John
|
|
END:VCARD''')
|
|
output.close()
|
|
numSyncs = 0
|
|
|
|
out, err, code = self.runCmdline(["--sync", "slow", "server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting first time sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
[INFO] @default/addressbook: starting first time sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO] @default/addressbook: sent 1
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO remote@client] @client/addressbook: adding "John Doe"
|
|
[INFO remote@client] @client/addressbook: received 1/1
|
|
[INFO] @default/addressbook: first time sync done successfully
|
|
[INFO remote@client] @client/addressbook: first time sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| slow, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
before sync | after sync
|
|
removed during sync <
|
|
> added during sync
|
|
-------------------------------------------------------------------------------
|
|
> BEGIN:VCARD
|
|
> N:Doe;John
|
|
> FN:John Doe
|
|
> VERSION:3.0
|
|
> END:VCARD
|
|
-------------------------------------------------------------------------------
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
|
|
| slow, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
|
|
''', out)
|
|
|
|
# Only 'addressbook' ever active. When done (= progress 100%),
|
|
# a lot of information seems to be missing (= -1) or dubious
|
|
# (1 out of 0 items sent?!). This information comes straight
|
|
# from libsynthesis; use it as it is for now.
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(slow, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(sending, -1, -1, 1, 0, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
numSyncs = numSyncs + 1
|
|
self.checkSync(numReports=numSyncs)
|
|
|
|
# check result (should be unchanged)
|
|
input = open(item, "r")
|
|
self.assertIn("FN:John Doe", input.read())
|
|
|
|
# no changes
|
|
out, err, code = self.runCmdline(["server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: starting normal sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO] @default/addressbook: normal sync done successfully
|
|
[INFO remote@client] @client/addressbook: normal sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(two-way, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(, -1, -1, -1, -1, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
numSyncs = numSyncs + 1
|
|
self.checkSync(numReports=numSyncs)
|
|
|
|
# Now once more in the same mode with dumping data disabled.
|
|
# This should trigger the "quit sync early, avoid data write"
|
|
# optimization.
|
|
def listxdg():
|
|
return listall([xdg_root],
|
|
exclude=[xdg_root + '/cache/syncevolution($|/.*)'],
|
|
includedata=True)
|
|
before = listxdg()
|
|
out, err, code = self.runCmdline(["--run", "printChanges=0", "dumpData=0", "server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting normal sync, two-way (peer is server)
|
|
[INFO] @default/addressbook: starting normal sync, two-way (peer is client)
|
|
[INFO] @default/addressbook: started
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO] @default/addressbook: normal sync done successfully
|
|
[INFO remote@client] @client/addressbook: normal sync done successfully
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(two-way, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(, -1, -1, -1, -1, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
numSyncs = numSyncs + 1
|
|
self.checkSync(numReports=numSyncs)
|
|
after = listxdg()
|
|
self.assertEqual(before, after)
|
|
|
|
# Now once more in the same mode with data comparison.
|
|
before = listxdg()
|
|
out, err, code = self.runCmdline(["server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
[INFO] @default/addressbook: starting normal sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO] @default/addressbook: normal sync done successfully
|
|
[INFO remote@client] @client/addressbook: normal sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(two-way, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(, -1, -1, -1, -1, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
numSyncs = numSyncs + 1
|
|
self.checkSync(numReports=numSyncs)
|
|
after = listxdg()
|
|
self.assertEqual(before, after)
|
|
|
|
# update contact
|
|
output = open(item, "w")
|
|
output.write('''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:Joan Doe
|
|
N:Doe;Joan
|
|
END:VCARD''')
|
|
output.close()
|
|
out, err, code = self.runCmdline(["server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: starting normal sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
after last sync | current data
|
|
removed since last sync <
|
|
> added since last sync
|
|
-------------------------------------------------------------------------------
|
|
BEGIN:VCARD BEGIN:VCARD
|
|
N:Doe;John | N:Doe;Joan
|
|
FN:John Doe | FN:Joan Doe
|
|
VERSION:3.0 VERSION:3.0
|
|
END:VCARD END:VCARD
|
|
-------------------------------------------------------------------------------
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO] @default/addressbook: sent 1
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO remote@client] @client/addressbook: updating "Joan Doe"
|
|
[INFO remote@client] @client/addressbook: received 1/1
|
|
[INFO] @default/addressbook: normal sync done successfully
|
|
[INFO remote@client] @client/addressbook: normal sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
before sync | after sync
|
|
removed during sync <
|
|
> added during sync
|
|
-------------------------------------------------------------------------------
|
|
BEGIN:VCARD BEGIN:VCARD
|
|
N:Doe;John | N:Doe;Joan
|
|
FN:John Doe | FN:Joan Doe
|
|
VERSION:3.0 VERSION:3.0
|
|
END:VCARD END:VCARD
|
|
-------------------------------------------------------------------------------
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(two-way, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(sending, -1, -1, 1, 0, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
numSyncs = numSyncs + 1
|
|
self.checkSync(numReports=numSyncs)
|
|
|
|
# now remove contact
|
|
os.unlink(item)
|
|
out, err, code = self.runCmdline(["server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: starting normal sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
after last sync | current data
|
|
removed since last sync <
|
|
> added since last sync
|
|
-------------------------------------------------------------------------------
|
|
BEGIN:VCARD <
|
|
N:Doe;Joan <
|
|
FN:Joan Doe <
|
|
VERSION:3.0 <
|
|
END:VCARD <
|
|
-------------------------------------------------------------------------------
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO] @default/addressbook: sent 1
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO remote@client] @client/addressbook: deleting <0>
|
|
[INFO remote@client] @client/addressbook: received 1/1
|
|
[INFO] @default/addressbook: normal sync done successfully
|
|
[INFO remote@client] @client/addressbook: normal sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
before sync | after sync
|
|
removed during sync <
|
|
> added during sync
|
|
-------------------------------------------------------------------------------
|
|
BEGIN:VCARD <
|
|
N:Doe;Joan <
|
|
FN:Joan Doe <
|
|
VERSION:3.0 <
|
|
END:VCARD <
|
|
-------------------------------------------------------------------------------
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(two-way, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(sending, -1, -1, 1, 0, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
numSyncs = numSyncs + 1
|
|
self.checkSync(numReports=numSyncs)
|
|
|
|
@property("debug", False)
|
|
@timeout(usingValgrind() and 1000 or 200)
|
|
def testSyncOutput2(self):
|
|
"""TestCmdline.testSyncOutput2 - run syncs between local dirs and check output, with two sources"""
|
|
self.setUpLocalSyncConfigs(enableCalendar=True)
|
|
self.session.Detach()
|
|
os.makedirs(xdg_root + "/server")
|
|
item = xdg_root + "/server/0"
|
|
output = open(item, "w")
|
|
output.write('''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:John Doe
|
|
N:Doe;John
|
|
END:VCARD''')
|
|
output.close()
|
|
|
|
out, err, code = self.runCmdline(["--sync", "slow", "server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting first time sync, two-way (peer is server)
|
|
[INFO remote@client] @client/calendar: starting first time sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
[INFO remote@client] creating complete data backup of datastore calendar before sync (enabled with dumpData and needed for printChanges)
|
|
*** @client/calendar ***
|
|
Comparison was impossible.
|
|
|
|
[INFO] @default/addressbook: starting first time sync, two-way (peer is client)
|
|
[INFO] @default/calendar: starting first time sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO] creating complete data backup of datastore calendar before sync (enabled with dumpData and needed for printChanges)
|
|
*** @default/calendar ***
|
|
Comparison was impossible.
|
|
|
|
[INFO] @default/calendar: started
|
|
[INFO] @default/addressbook: sent 1
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO remote@client] @client/addressbook: adding "John Doe"
|
|
[INFO remote@client] @client/addressbook: received 1/1
|
|
[INFO remote@client] @client/calendar: started
|
|
[INFO] @default/addressbook: first time sync done successfully
|
|
[INFO] @default/calendar: first time sync done successfully
|
|
[INFO remote@client] @client/addressbook: first time sync done successfully
|
|
[INFO remote@client] @client/calendar: first time sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| slow, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| calendar | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| slow, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
before sync | after sync
|
|
removed during sync <
|
|
> added during sync
|
|
-------------------------------------------------------------------------------
|
|
> BEGIN:VCARD
|
|
> N:Doe;John
|
|
> FN:John Doe
|
|
> VERSION:3.0
|
|
> END:VCARD
|
|
-------------------------------------------------------------------------------
|
|
*** @client/calendar ***
|
|
no changes
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
|
|
| slow, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| calendar | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| slow, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
*** @default/calendar ***
|
|
no changes
|
|
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(slow, running, 0\), calendar: \(slow, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(sending, -1, -1, 1, 0, -1, -1\), calendar: \(, -1, -1, -1, -1, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
self.checkSync(numReports=1)
|
|
|
|
# check result (should be unchanged)
|
|
input = open(item, "r")
|
|
self.assertIn("FN:John Doe", input.read())
|
|
|
|
# no changes
|
|
out, err, code = self.runCmdline(["server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] @client/calendar: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO remote@client] creating complete data backup of datastore calendar before sync (enabled with dumpData and needed for printChanges)
|
|
*** @client/calendar ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: starting normal sync, two-way (peer is client)
|
|
[INFO] @default/calendar: starting normal sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO] creating complete data backup of datastore calendar before sync (enabled with dumpData and needed for printChanges)
|
|
*** @default/calendar ***
|
|
no changes
|
|
|
|
[INFO] @default/calendar: started
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO remote@client] @client/calendar: started
|
|
[INFO] @default/addressbook: normal sync done successfully
|
|
[INFO] @default/calendar: normal sync done successfully
|
|
[INFO remote@client] @client/addressbook: normal sync done successfully
|
|
[INFO remote@client] @client/calendar: normal sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| calendar | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
*** @client/calendar ***
|
|
no changes
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| calendar | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
*** @default/calendar ***
|
|
no changes
|
|
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(two-way, running, 0\), calendar: \(two-way, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(, -1, -1, -1, -1, -1, -1\), calendar: \(, -1, -1, -1, -1, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
self.checkSync(numReports=2)
|
|
|
|
# update contact
|
|
output = open(item, "w")
|
|
output.write('''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:Joan Doe
|
|
N:Doe;Joan
|
|
END:VCARD''')
|
|
output.close()
|
|
out, err, code = self.runCmdline(["server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] @client/calendar: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO remote@client] creating complete data backup of datastore calendar before sync (enabled with dumpData and needed for printChanges)
|
|
*** @client/calendar ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: starting normal sync, two-way (peer is client)
|
|
[INFO] @default/calendar: starting normal sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
after last sync | current data
|
|
removed since last sync <
|
|
> added since last sync
|
|
-------------------------------------------------------------------------------
|
|
BEGIN:VCARD BEGIN:VCARD
|
|
N:Doe;John | N:Doe;Joan
|
|
FN:John Doe | FN:Joan Doe
|
|
VERSION:3.0 VERSION:3.0
|
|
END:VCARD END:VCARD
|
|
-------------------------------------------------------------------------------
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO] creating complete data backup of datastore calendar before sync (enabled with dumpData and needed for printChanges)
|
|
*** @default/calendar ***
|
|
no changes
|
|
|
|
[INFO] @default/calendar: started
|
|
[INFO] @default/addressbook: sent 1
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO remote@client] @client/addressbook: updating "Joan Doe"
|
|
[INFO remote@client] @client/addressbook: received 1/1
|
|
[INFO remote@client] @client/calendar: started
|
|
[INFO] @default/addressbook: normal sync done successfully
|
|
[INFO] @default/calendar: normal sync done successfully
|
|
[INFO remote@client] @client/addressbook: normal sync done successfully
|
|
[INFO remote@client] @client/calendar: normal sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| calendar | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
before sync | after sync
|
|
removed during sync <
|
|
> added during sync
|
|
-------------------------------------------------------------------------------
|
|
BEGIN:VCARD BEGIN:VCARD
|
|
N:Doe;John | N:Doe;Joan
|
|
FN:John Doe | FN:Joan Doe
|
|
VERSION:3.0 VERSION:3.0
|
|
END:VCARD END:VCARD
|
|
-------------------------------------------------------------------------------
|
|
*** @client/calendar ***
|
|
no changes
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 1 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| calendar | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
*** @default/calendar ***
|
|
no changes
|
|
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(two-way, running, 0\), calendar: \(two-way, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(sending, -1, -1, 1, 0, -1, -1\), calendar: \(, -1, -1, -1, -1, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
self.checkSync(numReports=3)
|
|
|
|
# now remove contact
|
|
os.unlink(item)
|
|
out, err, code = self.runCmdline(["server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] @client/calendar: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO remote@client] creating complete data backup of datastore calendar before sync (enabled with dumpData and needed for printChanges)
|
|
*** @client/calendar ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: starting normal sync, two-way (peer is client)
|
|
[INFO] @default/calendar: starting normal sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
after last sync | current data
|
|
removed since last sync <
|
|
> added since last sync
|
|
-------------------------------------------------------------------------------
|
|
BEGIN:VCARD <
|
|
N:Doe;Joan <
|
|
FN:Joan Doe <
|
|
VERSION:3.0 <
|
|
END:VCARD <
|
|
-------------------------------------------------------------------------------
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO] creating complete data backup of datastore calendar before sync (enabled with dumpData and needed for printChanges)
|
|
*** @default/calendar ***
|
|
no changes
|
|
|
|
[INFO] @default/calendar: started
|
|
[INFO] @default/addressbook: sent 1
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO remote@client] @client/addressbook: deleting <0>
|
|
[INFO remote@client] @client/addressbook: received 1/1
|
|
[INFO remote@client] @client/calendar: started
|
|
[INFO] @default/addressbook: normal sync done successfully
|
|
[INFO] @default/calendar: normal sync done successfully
|
|
[INFO remote@client] @client/addressbook: normal sync done successfully
|
|
[INFO remote@client] @client/calendar: normal sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| calendar | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
before sync | after sync
|
|
removed during sync <
|
|
> added during sync
|
|
-------------------------------------------------------------------------------
|
|
BEGIN:VCARD <
|
|
N:Doe;Joan <
|
|
FN:Joan Doe <
|
|
VERSION:3.0 <
|
|
END:VCARD <
|
|
-------------------------------------------------------------------------------
|
|
*** @client/calendar ***
|
|
no changes
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| calendar | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
*** @default/calendar ***
|
|
no changes
|
|
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(two-way, running, 0\), calendar: \(two-way, running, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(sending, -1, -1, 1, 0, -1, -1\), calendar: \(, -1, -1, -1, -1, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
self.checkSync(numReports=4)
|
|
|
|
# only 'addressbook' active
|
|
out, err, code = self.runCmdline(["server", "addressbook"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO] @default/calendar: inactive
|
|
[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/calendar: inactive
|
|
[INFO remote@client] @client/addressbook: starting normal sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: starting normal sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO] @default/addressbook: normal sync done successfully
|
|
[INFO remote@client] @client/addressbook: normal sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
no changes
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
|
| two-way, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 0 before sync, 0 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
no changes
|
|
|
|
''', out)
|
|
self.collectEvents()
|
|
self.assertRegexpMatches(self.prettyPrintEvents(),
|
|
r'''status: idle, .*
|
|
(.*\n)+status: running;waiting, 0, \{addressbook: \(two-way, running, 0\), calendar: \(none, idle, 0\)\}
|
|
(.*\n)*progress: 100, \{addressbook: \(, -1, -1, -1, -1, -1, -1\), calendar: \(, -1, -1, -1, -1, -1, -1\)\}
|
|
(.*\n)*status: done, .*''')
|
|
self.checkSync(numReports=5)
|
|
|
|
@property("debug", False)
|
|
@timeout(usingValgrind() and 600 or 200)
|
|
def testSyncOutputBadData(self):
|
|
"""TestCmdline.testSyncOutputBadData - run syncs between local dirs and check output using data which is not quite valid"""
|
|
self.setUpLocalSyncConfigs()
|
|
self.session.Detach()
|
|
os.makedirs(xdg_root + "/server")
|
|
item = xdg_root + "/server/0"
|
|
output = open(item, "w")
|
|
output.write('''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:John\377 Doe
|
|
N:Doe;John\377
|
|
END:VCARD''')
|
|
output.close()
|
|
os.makedirs(xdg_root + "/client")
|
|
item = xdg_root + "/client/0"
|
|
output = open(item, "w")
|
|
output.write('''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:Joan Doe\377
|
|
N:Doe\377;Joan
|
|
END:VCARD''')
|
|
output.close()
|
|
item = xdg_root + "/client/1"
|
|
output = open(item, "w")
|
|
output.write('''BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:J\377\377 D'Arc\377
|
|
N:D'Arc\377;J\377\377
|
|
END:VCARD''')
|
|
output.close()
|
|
|
|
out, err, code = self.runCmdline(["--sync", "slow", "server"],
|
|
sessionFlags=[],
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
out = self.stripSyncTime(out)
|
|
self.assertEqualDiff('''[INFO remote@client] target side of local sync ready
|
|
[INFO remote@client] @client/addressbook: starting first time sync, two-way (peer is server)
|
|
[INFO remote@client] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@client data changes to be applied during synchronization:
|
|
*** @client/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
[INFO remote@client] @client/addressbook: sent 2/2
|
|
[INFO] @default/addressbook: starting first time sync, two-way (peer is client)
|
|
[INFO] creating complete data backup of datastore addressbook before sync (enabled with dumpData and needed for printChanges)
|
|
@default data changes to be applied during synchronization:
|
|
*** @default/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
[INFO] @default/addressbook: started
|
|
[INFO] @default/addressbook: adding "Joan Doe?"
|
|
[INFO] @default/addressbook: adding "J?? D'Arc?"
|
|
[INFO] @default/addressbook: received 2
|
|
[INFO] @default/addressbook: sent 1
|
|
[INFO remote@client] @client/addressbook: started
|
|
[INFO remote@client] @client/addressbook: adding "John? Doe"
|
|
[INFO remote@client] @client/addressbook: received 1/1
|
|
[INFO] @default/addressbook: first time sync done successfully
|
|
[INFO remote@client] @client/addressbook: first time sync done successfully
|
|
[INFO remote@client] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization (remote@client):
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @client | @default | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 1 | 0 | 0 | 0 | 2 | 0 | 0 | 0 | 0 |
|
|
| slow, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 2 before sync, 3 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @client during synchronization:
|
|
*** @client/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
[INFO] creating complete data backup after sync (enabled with dumpData and needed for printChanges)
|
|
|
|
Synchronization successful.
|
|
|
|
Changes applied during synchronization:
|
|
+---------------|-----------------------|-----------------------|-CON-+
|
|
| | @default | @client | FLI |
|
|
| Source | NEW | MOD | DEL | ERR | NEW | MOD | DEL | ERR | CTS |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| addressbook | 2 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
|
|
| slow, 0 KB sent by client, 0 KB received |
|
|
| item(s) in database backup: 1 before sync, 3 after it |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
| start xxx, duration a:bcmin |
|
|
| synchronization completed successfully |
|
|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
|
|
Data modified @default during synchronization:
|
|
*** @default/addressbook ***
|
|
Comparison was impossible.
|
|
|
|
''', out)
|
|
|
|
|
|
class TestHTTP(CmdlineUtil, unittest.TestCase):
|
|
"""Test syncevo-http-server."""
|
|
|
|
def setUp(self):
|
|
self.setUpServer()
|
|
self.setUpListeners(None)
|
|
# All tests run with their own XDG root hierarchy.
|
|
# Here are the config files.
|
|
self.configdir = xdg_root + "/config/syncevolution"
|
|
|
|
# Check whether we have multithreading support.
|
|
out, err, code = self.runCmdline(["--sync-property", "SyncMLVersion=?", "--daemon=no"],
|
|
sessionFlags=None,
|
|
preserveOutputOrder=False)
|
|
self.assertEqual(err, '')
|
|
self.assertIn('REQUESTMAXTIME=', out)
|
|
self.assertEqual(0, code)
|
|
self.haveMultithreadedSyncEvo = 'is thread-safe' in out
|
|
self.httpserver = None
|
|
|
|
def tearDown(self):
|
|
if self.httpserver:
|
|
self.httpserver.kill()
|
|
self.httpserver.wait()
|
|
|
|
def run(self, result):
|
|
# Runtime varies a lot when using valgrind, because
|
|
# of the need to check an additional process. Allow
|
|
# a lot more time when running under valgrind.
|
|
self.runTest(result, own_xdg=True, own_home=True,
|
|
defTimeout=usingValgrind() and 600 or 20)
|
|
|
|
def runHTTPServer(self):
|
|
'''Pick port dynamically by running syncevo-http-server with port 0 and parsing its output.'''
|
|
logname = self.testname + '.syncevo-http-server.log'
|
|
self.additional_logs.append(logname)
|
|
self.httpserver = subprocess.Popen(['syncevo-http-server', '-d', 'http://127.0.0.1:0'],
|
|
stdout=open(logname, 'w'),
|
|
stderr=subprocess.STDOUT)
|
|
self.port = 0
|
|
while self.port == 0:
|
|
res = self.httpserver.poll()
|
|
if self.httpserver.poll() != None:
|
|
self.fail('syncevo-http-server failed to start, return code %d' % res)
|
|
regex = re.compile(r'listening on port (\d+)')
|
|
for line in open(logname, 'r'):
|
|
m = regex.search(line)
|
|
if m:
|
|
self.port = int(m.group(1))
|
|
break
|
|
time.sleep(0.1)
|
|
return self.port
|
|
|
|
def setUpConfigs(self,
|
|
port,
|
|
slowServer = False,
|
|
dumpData = True,
|
|
requestMaxTime = None,
|
|
retryDuration = None,
|
|
retryInterval = None,
|
|
username = "johndoe",
|
|
password = "foo",
|
|
clientID = "fake-device"):
|
|
self.setUpSession("", flags = [ "all-configs" ])
|
|
config = {"" : { "loglevel": "4",
|
|
"syncURL": "http://127.0.0.1:%d" % port,
|
|
"deviceID": clientID,
|
|
"useProxy": "0",
|
|
"username": username,
|
|
"password": password,
|
|
"RetryDuration": "1m", # abort syncing after 1 minute of no server response
|
|
},
|
|
"source/addressbook-client" : { "sync": "two-way",
|
|
"backend": "file",
|
|
"databaseFormat": "text/vcard",
|
|
"uri": slowServer and "addressbook-slow-server" or "addressbook-server",
|
|
"database": "file://" + xdg_root + "/client",
|
|
},
|
|
}
|
|
if retryDuration != None:
|
|
config[""]["RetryDuration"] = retryDuration
|
|
if retryInterval != None:
|
|
config[""]["RetryInterval"] = retryInterval
|
|
self.session.SetNamedConfig("client", False, False, config)
|
|
config = {"" : { "loglevel": "4",
|
|
"peerIsClient": "1",
|
|
"deviceId": clientID, # Has to be repeated because it is shared.
|
|
"remoteDeviceId": clientID,
|
|
"username": username,
|
|
"password": password,
|
|
"printChanges": "0",
|
|
"dumpData": dumpData and "1" or "0",
|
|
},
|
|
"source/addressbook-server": { "sync": "two-way",
|
|
"uri": "addressbook",
|
|
"backend": "file",
|
|
"databaseFormat": "text/vcard",
|
|
"database": "file://" + xdg_root + "/server" },
|
|
"source/addressbook-slow-server": { "sync": "two-way",
|
|
"uri": "addressbook",
|
|
"backend": "file",
|
|
"databaseFormat": "text/vcard",
|
|
"database": "file://" + xdg_root + "/server" },
|
|
}
|
|
if requestMaxTime != None:
|
|
config[""]["SyncMLVersion"] = "requestmaxtime=%d" % requestMaxTime
|
|
self.session.SetNamedConfig("server", False, False, config)
|
|
self.session.Detach()
|
|
|
|
@timeout(200)
|
|
def testHTTP(self):
|
|
"""TestHTTP.testHTTP - run simple HTTP-based sync"""
|
|
port = self.runHTTPServer()
|
|
self.setUpConfigs(port)
|
|
out, err, code = self.runCmdline(["--sync", "slow", "--daemon=no", "client"],
|
|
sessionFlags=None,
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
|
|
@timeout(200)
|
|
@property("ENV", "SYNCEVOLUTION_FILE_SOURCE_DELAY_OPEN_addressbook-slow-server=60")
|
|
def testTimeoutOpen(self):
|
|
"""TestHTTP.testTimeoutOpen - slow down server in open, let client abort"""
|
|
port = self.runHTTPServer()
|
|
self.setUpConfigs(port, slowServer=True, retryDuration="10s", retryInterval="5m")
|
|
out, err, code = self.runCmdline(["--sync", "slow", "--daemon=no", "client"],
|
|
sessionFlags=None,
|
|
expectSuccess=False,
|
|
preserveOutputOrder=False)
|
|
self.assertNotIn('resend previous message', err)
|
|
self.assertIn('transport problem: timeout, retry period exceeded', err)
|
|
self.assertEqual(1, code)
|
|
|
|
@timeout(200)
|
|
@property("ENV", "SYNCEVOLUTION_FILE_SOURCE_DELAY_LISTALL_addressbook-slow-server=60")
|
|
def testTimeoutListAll(self):
|
|
"""TestHTTP.testTimeoutListAll - slow down server in listAll, let client abort"""
|
|
port = self.runHTTPServer()
|
|
self.setUpConfigs(port, slowServer=True, retryDuration="10s", retryInterval="5m")
|
|
out, err, code = self.runCmdline(["--sync", "slow", "--daemon=no", "client"],
|
|
sessionFlags=None,
|
|
expectSuccess=False,
|
|
preserveOutputOrder=False)
|
|
self.assertNotIn('resend previous message', err)
|
|
self.assertIn('transport problem: timeout, retry period exceeded', err)
|
|
self.assertEqual(1, code)
|
|
|
|
@timeout(200)
|
|
@property("ENV", "SYNCEVOLUTION_FILE_SOURCE_DELAY_OPEN_addressbook-slow-server=140")
|
|
def testTimeoutSlow(self):
|
|
"""TestHTTP.testTimeoutSlow - slow down server in open with disabled keep-alive messages, let client abort"""
|
|
port = self.runHTTPServer()
|
|
self.setUpConfigs(port, slowServer=True, retryDuration="130s", retryInterval="5m",
|
|
requestMaxTime=0)
|
|
out, err, code = self.runCmdline(["--sync", "slow", "--daemon=no", "client"],
|
|
sessionFlags=None,
|
|
expectSuccess=False,
|
|
preserveOutputOrder=False)
|
|
self.assertNotIn('resend previous message', err)
|
|
self.assertIn('transport problem: timeout, retry period exceeded', err)
|
|
self.assertEqual(1, code)
|
|
|
|
@timeout(200)
|
|
@property("ENV", "SYNCEVOLUTION_FILE_SOURCE_DELAY_OPEN_addressbook-slow-server=140")
|
|
def testThreadedOpen(self):
|
|
"""TestHTTP.testThreadedOpen - slow down server in open, let server send keep-alive messages at default rate"""
|
|
if not self.haveMultithreadedSyncEvo:
|
|
return
|
|
port = self.runHTTPServer()
|
|
self.setUpConfigs(port, slowServer=True, retryDuration="130", retryInterval="5m")
|
|
out, err, code = self.runCmdline(["--sync", "slow", "--daemon=no", "client"],
|
|
sessionFlags=None,
|
|
expectSuccess=True,
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
|
|
@timeout(200)
|
|
@property("ENV", "SYNCEVOLUTION_FILE_SOURCE_DELAY_LISTALL_addressbook-slow-server=140")
|
|
def testThreadedListAll(self):
|
|
"""TestHTTP.testThreadedListAll - slow down server in listAll, let server send keep-alive messages at default rate"""
|
|
if not self.haveMultithreadedSyncEvo:
|
|
return
|
|
port = self.runHTTPServer()
|
|
self.setUpConfigs(port, slowServer=True, retryDuration="130s", retryInterval="5m",
|
|
dumpData=False) # listAll() gets called too often otherwise, which slows down the test too much.
|
|
out, err, code = self.runCmdline(["--sync", "slow", "--daemon=no", "client"],
|
|
sessionFlags=None,
|
|
expectSuccess=True,
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
|
|
@timeout(200)
|
|
@property("ENV", "SYNCEVOLUTION_FILE_SOURCE_DELAY_OPEN_addressbook-slow-server=%d" % (usingValgrind() and 80 or 40))
|
|
def testThreadedOpenQuick(self):
|
|
"""TestHTTP.testThreadedOpenQuick - slow down server in open, let server send keep-alive messages at faster rate"""
|
|
port = self.runHTTPServer()
|
|
self.setUpConfigs(port, slowServer=True, retryDuration=(usingValgrind() and "60s" or "30s"), retryInterval="5m",
|
|
requestMaxTime=10)
|
|
out, err, code = self.runCmdline(["--sync", "slow", "--daemon=no", "client"],
|
|
sessionFlags=None,
|
|
expectSuccess=True,
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
|
|
@timeout(usingValgrind() and 400 or 200)
|
|
@property("ENV", "SYNCEVOLUTION_FILE_SOURCE_DELAY_LISTALL_addressbook-slow-server=%d" % (usingValgrind() and 80 or 40))
|
|
def testThreadedListAllQuick(self):
|
|
"""TestHTTP.testThreadedListAllQuick - slow down server in listAll, let server send keep-alive messages at faster rate"""
|
|
port = self.runHTTPServer()
|
|
self.setUpConfigs(port, slowServer=True, retryDuration=(usingValgrind() and "60s" or "30s"), retryInterval="5m",
|
|
requestMaxTime=10)
|
|
out, err, code = self.runCmdline(["--sync", "slow", "--daemon=no", "client"],
|
|
sessionFlags=None,
|
|
expectSuccess=True,
|
|
preserveOutputOrder=True)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(0, code)
|
|
|
|
@timeout(200)
|
|
@property("ENV", "SYNCEVOLUTION_FILE_SOURCE_DELAY_OPEN_addressbook-slow-server=400")
|
|
def testAbortThread(self):
|
|
"""TestHTTP.testAbortThread - slow down server in open, abort sync while waiting for background thread"""
|
|
port = self.runHTTPServer()
|
|
self.setUpConfigs(port, slowServer=True, requestMaxTime=200)
|
|
self.session = None
|
|
self.isWaiting = False
|
|
self.messages = []
|
|
def output(path, level, message, process):
|
|
self.messages.append(message)
|
|
if 'wait for background thread: ' in message:
|
|
self.isWaiting = True
|
|
signal = bus.add_signal_receiver(output,
|
|
'LogOutput',
|
|
'org.syncevolution.Server',
|
|
self.server.bus_name,
|
|
None,
|
|
byte_arrays=True,
|
|
utf8_strings=True)
|
|
|
|
s = self.startCmdline(["--sync", "slow", "--daemon=no", "client"],
|
|
preserveOutputOrder=True)
|
|
|
|
# Wait for session...
|
|
while not self.session:
|
|
self.loopIteration('waiting for session', may_block=True)
|
|
# ... and the confirmation that the background thread is running.
|
|
while not self.isWaiting:
|
|
self.loopIteration('background thread', may_block=True)
|
|
|
|
# We need to abort syncevo-dbus-server. Aborting the client via SIGTERM
|
|
# would not abort the server.
|
|
self.session.Abort()
|
|
|
|
# The HTTP client should see a network error.
|
|
out, err, code = self.finishCmdline(s, expectSuccess=False, sessionFlags=None)
|
|
self.assertEqual(err, None)
|
|
self.assertEqual(1, code)
|
|
self.assertIn('external transport failure', out)
|
|
|
|
# We expect that the FileSyncSource::open() detects the abort
|
|
# request quickly and then let's the background thread be
|
|
# reaped normally by the master thread.
|
|
# This may happen without ever having to enter our threading
|
|
# code, so we can't check for its 'background thread completed'
|
|
# message. Instead check that the file source continued normally.
|
|
self.assertIn('addressbook-slow-server: continue opening file source', self.messages)
|
|
|
|
# Finally, also check server session status.
|
|
status, error, sources = self.session.GetStatus()
|
|
self.assertEqual(('done', 20017), (status, error))
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|