[CalendarServer-changes] [9309] CalendarServer/trunk/contrib/performance/loadtest
source_changes at macosforge.org
source_changes at macosforge.org
Tue May 29 19:32:43 PDT 2012
Revision: 9309
http://trac.macosforge.org/projects/calendarserver/changeset/9309
Author: cdaboo at apple.com
Date: 2012-05-29 19:32:43 -0700 (Tue, 29 May 2012)
Log Message:
-----------
Clients now save and restore state between runs. Fixed some reporting issues and handling of an invalid
sync token.
Modified Paths:
--------------
CalendarServer/trunk/contrib/performance/loadtest/config.dist.plist
CalendarServer/trunk/contrib/performance/loadtest/config.plist
CalendarServer/trunk/contrib/performance/loadtest/ical.py
CalendarServer/trunk/contrib/performance/loadtest/population.py
CalendarServer/trunk/contrib/performance/loadtest/sim.py
CalendarServer/trunk/contrib/performance/loadtest/test_ical.py
Modified: CalendarServer/trunk/contrib/performance/loadtest/config.dist.plist
===================================================================
--- CalendarServer/trunk/contrib/performance/loadtest/config.dist.plist 2012-05-29 17:09:36 UTC (rev 9308)
+++ CalendarServer/trunk/contrib/performance/loadtest/config.dist.plist 2012-05-30 02:32:43 UTC (rev 9309)
@@ -50,6 +50,15 @@
<integer>8080</integer>
</dict>
+ <!-- Define whether client data should be saved and re-used. -->
+ <key>clientDataSerialization</key>
+ <dict>
+ <key>Enabled</key>
+ <true/>
+ <key>Path</key>
+ <string>/tmp/sim</string>
+ </dict>
+
<!-- Define the credentials of the clients which will be used to load test
the server. These credentials must already be valid on the server. -->
<key>accounts</key>
Modified: CalendarServer/trunk/contrib/performance/loadtest/config.plist
===================================================================
--- CalendarServer/trunk/contrib/performance/loadtest/config.plist 2012-05-29 17:09:36 UTC (rev 9308)
+++ CalendarServer/trunk/contrib/performance/loadtest/config.plist 2012-05-30 02:32:43 UTC (rev 9309)
@@ -37,6 +37,15 @@
<integer>8080</integer>
</dict>
+ <!-- Define whether client data should be saved and re-used. -->
+ <key>clientDataSerialization</key>
+ <dict>
+ <key>Enabled</key>
+ <true/>
+ <key>Path</key>
+ <string>/tmp/sim</string>
+ </dict>
+
<!-- Define the credentials of the clients which will be used to load test
the server. These credentials must already be valid on the server. -->
<key>accounts</key>
Modified: CalendarServer/trunk/contrib/performance/loadtest/ical.py
===================================================================
--- CalendarServer/trunk/contrib/performance/loadtest/ical.py 2012-05-29 17:09:36 UTC (rev 9308)
+++ CalendarServer/trunk/contrib/performance/loadtest/ical.py 2012-05-30 02:32:43 UTC (rev 9309)
@@ -41,16 +41,19 @@
from twisted.python.log import addObserver, err, msg
from twisted.python.util import FancyEqMixin
from twisted.web.client import Agent
-from twisted.web.http import OK, MULTI_STATUS, CREATED, NO_CONTENT, PRECONDITION_FAILED, MOVED_PERMANENTLY
+from twisted.web.http import OK, MULTI_STATUS, CREATED, NO_CONTENT, PRECONDITION_FAILED, MOVED_PERMANENTLY,\
+ FORBIDDEN
from twisted.web.http_headers import Headers
from twistedcaldav.ical import Component, Property
-from urlparse import urlparse, urlunparse, urlsplit
+from urlparse import urlparse, urlunparse, urlsplit, urljoin
from uuid import uuid4
from xml.etree import ElementTree
import random
+import os
+import json
ElementTree.QName.__repr__ = lambda self: '<QName %r>' % (self.text,)
@@ -94,7 +97,9 @@
self.pushkey = pushkey
-
+def u2str(data):
+ return data.encode("utf-8") if type(data) is unicode else data
+
class Event(object):
def __init__(self, url, etag, vevent=None):
self.url = url
@@ -112,7 +117,31 @@
return None
+ def serialize(self):
+ """
+ Create a dict of the data so we can serialize as JSON.
+ """
+
+ result = {}
+ for attr in ("url", "etag", "scheduleTag"):
+ result[attr] = getattr(self, attr)
+ result["icalendar"] = str(self.vevent)
+ return result
+ @staticmethod
+ def deserialize(data):
+ """
+ Convert dict (deserialized from JSON) into an L{Event}.
+ """
+
+ event = Event(None, None)
+ for attr in ("url", "etag", "scheduleTag"):
+ setattr(event, attr, u2str(data[attr]))
+ event.vevent = Component.fromString(data["icalendar"])
+ return event
+
+
+
class Calendar(object):
def __init__(self, resourceType, componentTypes, name, url, changeToken):
self.resourceType = resourceType
@@ -123,7 +152,41 @@
self.events = {}
+ def serialize(self):
+ """
+ Create a dict of the data so we can serialize as JSON.
+ """
+
+ result = {}
+ for attr in ("resourceType", "name", "url", "changeToken"):
+ result[attr] = getattr(self, attr)
+ result["componentTypes"] = list(sorted(self.componentTypes))
+ result["events"] = sorted(self.events.keys())
+ return result
+
+ @staticmethod
+ def deserialize(data, events):
+ """
+ Convert dict (deserialized from JSON) into an L{Event}.
+ """
+
+ calendar = Calendar(None, None, None, None, None)
+ for attr in ("resourceType", "name", "url", "changeToken"):
+ setattr(calendar, attr, u2str(data[attr]))
+ calendar.componentTypes = set(map(u2str, data["componentTypes"]))
+
+ for event in data["events"]:
+ url = urljoin(calendar.url, event)
+ if url in events:
+ calendar.events[event] = events[url]
+ else:
+ # Ughh - an event is missing - force changeToken to empty to trigger full resync
+ calendar.changeToken = ""
+ return calendar
+
+
+
class BaseClient(object):
"""
Base interface for all simulated clients.
@@ -222,6 +285,8 @@
Implementation of common OS X/iOS client behavior.
"""
+ _client_type = "Generic"
+
USER_AGENT = None # Override this for specific clients
# The default interval, used if none is specified in external
@@ -266,8 +331,20 @@
email = None
- def __init__(self, reactor, root, principalPathTemplate, record, auth, calendarHomePollInterval=None, supportPush=True,
- supportAmpPush=True, ampPushHost="localhost", ampPushPort=62311):
+ def __init__(
+ self,
+ reactor,
+ root,
+ principalPathTemplate,
+ serializePath,
+ record,
+ auth,
+ calendarHomePollInterval=None,
+ supportPush=True,
+ supportAmpPush=True,
+ ampPushHost="localhost",
+ ampPushPort=62311,
+ ):
self._client_id = str(uuid4())
@@ -287,12 +364,17 @@
self.ampPushHost = ampPushHost
self.ampPushPort = ampPushPort
+ self.serializePath = serializePath
+
self.supportSync = self._SYNC_REPORT
# Keep track of the calendars on this account, keys are
# Calendar URIs, values are Calendar instances.
self._calendars = {}
+ # The principalURL found during discovery
+ self.principalURL = None
+
# Keep track of the events on this account, keys are event
# URIs (which are unambiguous across different calendars
# because they start with the uri of the calendar they are
@@ -458,8 +540,10 @@
StringProducer(body),
method_label=method_label,
)
+
body = yield readBody(response)
- result = self._parseMultiStatus(body, otherTokens)
+ result = self._parseMultiStatus(body, otherTokens) if response.code == MULTI_STATUS else None
+
returnValue(result)
@@ -645,15 +729,36 @@
the existing calendar once sync is done.
"""
+ # Grab old hrefs prior to the REPORT so we sync with the old state. We need this because
+ # the sim can fire a PUT between the REPORT and when process the removals.
+ old_hrefs = set([calendar.url + child for child in calendar.events.keys()])
+
# Get changes from sync REPORT (including the other nodes at the top-level
# which will have the new sync token.
- result, others = yield self._report(
+ fullSync = not calendar.changeToken
+ result = yield self._report(
calendar.url,
self._POLL_CALENDAR_SYNC_REPORT % {'sync-token': calendar.changeToken},
depth='1',
+ allowedStatus=(MULTI_STATUS, FORBIDDEN,),
otherTokens = True,
method_label="REPORT{sync}" if calendar.changeToken else "REPORT{sync-init}",
)
+ if result is None:
+ if not fullSync:
+ fullSync = True
+ result = yield self._report(
+ calendar.url,
+ self._POLL_CALENDAR_SYNC_REPORT % {'sync-token': ''},
+ depth='1',
+ otherTokens = True,
+ method_label="REPORT{sync}" if calendar.changeToken else "REPORT{sync-init}",
+ )
+ else:
+ raise IncorrectResponseCode((MULTI_STATUS,), None)
+
+ result, others = result
+
changed = []
for responseHref in result:
@@ -679,6 +784,13 @@
yield self._updateChangedEvents(calendar, changed)
+ # Handle removals only when doing an initial sync
+ if fullSync:
+ # Detect removed items and purge them
+ remove_hrefs = old_hrefs - set(changed)
+ for href in remove_hrefs:
+ self._removeEvent(href)
+
# Now update calendar to the new token taken from the report
for node in others:
if node.tag == davxml.sync_token:
@@ -763,9 +875,9 @@
hrefs = "".join([self._POLL_CALENDAR_MULTIGET_REPORT_HREF % {'href': event} for event in events])
label_suffix = "small"
- if len(hrefs) > 5:
+ if len(events) > 5:
label_suffix = "medium"
- if len(hrefs) > 15:
+ if len(events) > 15:
label_suffix = "large"
return self._report(
@@ -1035,8 +1147,63 @@
"""
Called before connections are closed, giving a chance to clean up
"""
+
+ self.serialize()
return self._unsubscribePubSub()
+
+ def serialize(self):
+ """
+ Write current state to disk.
+ """
+
+ if self.serializePath is None or not os.path.isdir(self.serializePath):
+ return
+
+ key = "%s-%s.json" % (self.record.uid, self._client_type.replace(" ", "_"))
+ path = os.path.join(self.serializePath, key)
+
+ # Create dict for all the data we need to store
+ data = {
+ "principalURL": self.principalURL,
+ "calendars": [calendar.serialize() for calendar in sorted(self._calendars.values(), key=lambda x:x.name)],
+ "events": [event.serialize() for event in sorted(self._events.values(), key=lambda x:x.url)],
+ }
+
+ # Write JSON data
+ json.dump(data, open(path, "w"), indent=2)
+
+
+ def deserialize(self):
+ """
+ Read state from disk.
+ """
+
+ if self.serializePath is None or not os.path.isdir(self.serializePath):
+ return
+
+ self._calendars = {}
+ self._events = {}
+
+ # Parse JSON data for calendars
+ key = "%s-%s.json" % (self.record.uid, self._client_type.replace(" ", "_"))
+ path = os.path.join(self.serializePath, key)
+ try:
+ data = json.load(open(path))
+ except IOError:
+ return
+
+ self.principalURL = data["principalURL"]
+
+ # Extract all the events first, then do the calendars (which reference the events)
+ for event in data["events"]:
+ event = Event.deserialize(event)
+ self._events[event.url] = event
+ for calendar in data["calendars"]:
+ calendar = Calendar.deserialize(calendar, self._events)
+ self._calendars[calendar.url] = calendar
+
+
def _makeSelfAttendee(self):
attendee = Property(
name=u'ATTENDEE',
@@ -1396,10 +1563,14 @@
@inlineCallbacks
def startup(self):
- # PROPFIND principal path to retrieve actual principal-URL
- response = yield self._principalPropfindInitial(self.record.uid)
- hrefs = response.getHrefProperties()
- self.principalURL = hrefs[davxml.principal_URL].toString()
+ # Try to read data from disk - if it succeeds self.principalURL will be set
+ self.deserialize()
+
+ if self.principalURL is None:
+ # PROPFIND principal path to retrieve actual principal-URL
+ response = yield self._principalPropfindInitial(self.record.uid)
+ hrefs = response.getHrefProperties()
+ self.principalURL = hrefs[davxml.principal_URL].toString()
# Using the actual principal URL, retrieve principal information
principal = yield self._principalPropfind()
@@ -1497,18 +1668,22 @@
@inlineCallbacks
def startup(self):
- # PROPFIND well-known with redirect
- response = yield self._startupPropfindWellKnown()
- hrefs = response.getHrefProperties()
- if davxml.current_user_principal in hrefs:
- self.principalURL = hrefs[davxml.current_user_principal].toString()
- elif davxml.principal_URL in hrefs:
- self.principalURL = hrefs[davxml.principal_URL].toString()
- else:
- # PROPFIND principal path to retrieve actual principal-URL
- response = yield self._principalPropfindInitial(self.record.uid)
+ # Try to read data from disk - if it succeeds self.principalURL will be set
+ self.deserialize()
+
+ if self.principalURL is None:
+ # PROPFIND well-known with redirect
+ response = yield self._startupPropfindWellKnown()
hrefs = response.getHrefProperties()
- self.principalURL = hrefs[davxml.principal_URL].toString()
+ if davxml.current_user_principal in hrefs:
+ self.principalURL = hrefs[davxml.current_user_principal].toString()
+ elif davxml.principal_URL in hrefs:
+ self.principalURL = hrefs[davxml.principal_URL].toString()
+ else:
+ # PROPFIND principal path to retrieve actual principal-URL
+ response = yield self._principalPropfindInitial(self.record.uid)
+ hrefs = response.getHrefProperties()
+ self.principalURL = hrefs[davxml.principal_URL].toString()
# Using the actual principal URL, retrieve principal information
principal = yield self._principalPropfind()
@@ -1701,18 +1876,22 @@
@inlineCallbacks
def startup(self):
- # PROPFIND well-known with redirect
- response = yield self._startupPropfindWellKnown()
- hrefs = response.getHrefProperties()
- if davxml.current_user_principal in hrefs:
- self.principalURL = hrefs[davxml.current_user_principal].toString()
- elif davxml.principal_URL in hrefs:
- self.principalURL = hrefs[davxml.principal_URL].toString()
- else:
- # PROPFIND principal path to retrieve actual principal-URL
- response = yield self._principalPropfindInitial(self.record.uid)
+ # Try to read data from disk - if it succeeds self.principalURL will be set
+ self.deserialize()
+
+ if self.principalURL is None:
+ # PROPFIND well-known with redirect
+ response = yield self._startupPropfindWellKnown()
hrefs = response.getHrefProperties()
- self.principalURL = hrefs[davxml.principal_URL].toString()
+ if davxml.current_user_principal in hrefs:
+ self.principalURL = hrefs[davxml.current_user_principal].toString()
+ elif davxml.principal_URL in hrefs:
+ self.principalURL = hrefs[davxml.principal_URL].toString()
+ else:
+ # PROPFIND principal path to retrieve actual principal-URL
+ response = yield self._principalPropfindInitial(self.record.uid)
+ hrefs = response.getHrefProperties()
+ self.principalURL = hrefs[davxml.principal_URL].toString()
# Using the actual principal URL, retrieve principal information
principal = yield self._principalPropfind()
Modified: CalendarServer/trunk/contrib/performance/loadtest/population.py
===================================================================
--- CalendarServer/trunk/contrib/performance/loadtest/population.py 2012-05-29 17:09:36 UTC (rev 9308)
+++ CalendarServer/trunk/contrib/performance/loadtest/population.py 2012-05-30 02:32:43 UTC (rev 9309)
@@ -77,12 +77,12 @@
self.profileTypes = profileTypes
- def new(self, reactor, serverAddress, principalPathTemplate, userRecord, authInfo):
+ def new(self, reactor, serverAddress, principalPathTemplate, serializationPath, userRecord, authInfo):
"""
Create a new instance of this client type.
"""
return self.clientType(
- reactor, serverAddress, principalPathTemplate, userRecord, authInfo, **self.clientParams)
+ reactor, serverAddress, principalPathTemplate, serializationPath, userRecord, authInfo, **self.clientParams)
@@ -156,12 +156,13 @@
class CalendarClientSimulator(object):
def __init__(self, records, populator, parameters, reactor, server,
- principalPathTemplate, workerIndex=0, workerCount=1):
+ principalPathTemplate, serializationPath, workerIndex=0, workerCount=1):
self._records = records
self.populator = populator
self.reactor = reactor
self.server = server
self.principalPathTemplate = principalPathTemplate
+ self.serializationPath = serializationPath
self._pop = self.populator.populate(parameters)
self._user = 0
self._stopped = False
@@ -226,7 +227,12 @@
reactor = loggedReactor(self.reactor)
client = clientType.new(
- reactor, self.server, self.principalPathTemplate, self.getUserRecord(number), auth
+ reactor,
+ self.server,
+ self.principalPathTemplate,
+ self.serializationPath,
+ self.getUserRecord(number),
+ auth,
)
self.clients.append(client)
d = client.run()
Modified: CalendarServer/trunk/contrib/performance/loadtest/sim.py
===================================================================
--- CalendarServer/trunk/contrib/performance/loadtest/sim.py 2012-05-29 17:09:36 UTC (rev 9308)
+++ CalendarServer/trunk/contrib/performance/loadtest/sim.py 2012-05-30 02:32:43 UTC (rev 9309)
@@ -193,7 +193,7 @@
user information about the accounts on the server being put
under load.
"""
- def __init__(self, server, principalPathTemplate, webadminPort, arrival, parameters, observers=None,
+ def __init__(self, server, principalPathTemplate, webadminPort, serializationPath, arrival, parameters, observers=None,
records=None, reactor=None, runtime=None, workers=None,
configTemplate=None, workerID=None, workerCount=1):
if reactor is None:
@@ -201,6 +201,7 @@
self.server = server
self.principalPathTemplate = principalPathTemplate
self.webadminPort = webadminPort
+ self.serializationPath = serializationPath
self.arrival = arrival
self.parameters = parameters
self.observers = observers
@@ -244,6 +245,7 @@
server = 'http://127.0.0.1:8008/'
principalPathTemplate = "/principals/users/%s/"
webadminPort = None
+ serializationPath = None
if 'server' in config:
server = config['server']
@@ -255,6 +257,10 @@
if config['webadmin']['enabled']:
webadminPort = config['webadmin']['HTTPPort']
+ if 'clientDataSerialization' in config:
+ if config['clientDataSerialization']['Enabled']:
+ serializationPath = config['clientDataSerialization']['Path']
+
if 'arrival' in config:
arrival = Arrival(
namedAny(config['arrival']['factory']),
@@ -284,6 +290,7 @@
server = ''
principalPathTemplate = ''
webadminPort = None
+ serializationPath = None
arrival = None
parameters = None
workerID = 0
@@ -304,7 +311,8 @@
records.extend(namedAny(loader)(**params))
output.write("Loaded {0} accounts.\n".format(len(records)))
- return cls(server, principalPathTemplate, webadminPort, arrival, parameters, observers=observers,
+ return cls(server, principalPathTemplate, webadminPort, serializationPath,
+ arrival, parameters, observers=observers,
records=records, runtime=runtime, reactor=reactor,
workers=workers, configTemplate=configTemplate,
workerID=workerID, workerCount=workerCount)
@@ -344,8 +352,15 @@
def createSimulator(self):
populator = Populator(Random())
return CalendarClientSimulator(
- self.records, populator, self.parameters, self.reactor, self.server,
- self.principalPathTemplate, self.workerID, self.workerCount
+ self.records,
+ populator,
+ self.parameters,
+ self.reactor,
+ self.server,
+ self.principalPathTemplate,
+ self.serializationPath,
+ self.workerID,
+ self.workerCount,
)
Modified: CalendarServer/trunk/contrib/performance/loadtest/test_ical.py
===================================================================
--- CalendarServer/trunk/contrib/performance/loadtest/test_ical.py 2012-05-29 17:09:36 UTC (rev 9308)
+++ CalendarServer/trunk/contrib/performance/loadtest/test_ical.py 2012-05-30 02:32:43 UTC (rev 9309)
@@ -15,27 +15,31 @@
#
##
-from twisted.python.failure import Failure
-from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
-from twisted.trial.unittest import TestCase
-from twisted.web.http import OK, NO_CONTENT, CREATED, MULTI_STATUS
-from twisted.web.http_headers import Headers
-from twisted.web.client import ResponseDone
-from twisted.internet.protocol import ProtocolToConsumerAdapter
-
+from caldavclientlibrary.protocol.caldav.definitions import caldavxml
+from caldavclientlibrary.protocol.caldav.definitions import csxml
from caldavclientlibrary.protocol.url import URL
from caldavclientlibrary.protocol.webdav.definitions import davxml
-from caldavclientlibrary.protocol.caldav.definitions import caldavxml
-from caldavclientlibrary.protocol.caldav.definitions import csxml
+from contrib.performance.httpclient import MemoryConsumer, StringProducer
from contrib.performance.loadtest.ical import XMPPPush, Event, Calendar, OS_X_10_6
from contrib.performance.loadtest.sim import _DirectoryRecord
-from contrib.performance.httpclient import MemoryConsumer, StringProducer
-from twistedcaldav.ical import Component
+
from pycalendar.datetime import PyCalendarDateTime
from pycalendar.timezone import PyCalendarTimezone
+
+from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
+from twisted.internet.protocol import ProtocolToConsumerAdapter
+from twisted.python.failure import Failure
+from twisted.trial.unittest import TestCase
+from twisted.web.client import ResponseDone
+from twisted.web.http import OK, NO_CONTENT, CREATED, MULTI_STATUS
+from twisted.web.http_headers import Headers
+
+from twistedcaldav.ical import Component
from twistedcaldav.timezones import TimezoneCache
+import os
+
EVENT_UID = 'D94F247D-7433-43AF-B84B-ADD684D023B0'
EVENT = """\
@@ -1155,7 +1159,12 @@
self.record = _DirectoryRecord(
u"user91", u"user91", u"User 91", u"user91 at example.org")
self.client = OS_X_10_6(
- None, "http://127.0.0.1/", "/principals/users/%s/", self.record, None
+ None,
+ "http://127.0.0.1/",
+ "/principals/users/%s/",
+ None,
+ self.record,
+ None,
)
@@ -1460,6 +1469,172 @@
return d
+ def test_serialization(self):
+ """
+ L{OS_X_10_6.serialize} properly generates a JSON document.
+ """
+ events = (
+ Event(u'/home/calendar/1.ics', u'123.123', "BEGIN:VALENDAR\r\nEND:VCALENDAR\r\n"),
+ Event(u'/home/inbox/i1.ics', u'123.123', "BEGIN:VALENDAR\r\nMETHOD:REQUEST\r\nEND:VCALENDAR\r\n"),
+ )
+ self.client._events.update(dict([[event.url, event] for event in events]))
+
+ calendars = (
+ Calendar(str(caldavxml.calendar), set(('VEVENT',)), u'calendar', u'/home/calendar/', "123"),
+ Calendar(str(caldavxml.calendar), set(('VTODO',)), u'tasks', u'/home/tasks/', "456"),
+ Calendar(str(caldavxml.schedule_inbox), set(('VEVENT', "VTODO",)), u'calendar', u'/home/inbox/', "789"),
+ )
+ self.client._calendars.update(dict([[calendar.url, calendar] for calendar in calendars]))
+ self.client._calendars["/home/calendar/"].events["1.ics"] = events[0]
+ self.client._calendars["/home/inbox/"].events["i1.ics"] = events[1]
+
+ tmp = self.mktemp()
+ os.mkdir(tmp)
+ self.client.serializePath = tmp
+ tmpPath = os.path.join(tmp, "user91-OS_X_10.6.json")
+ self.assertFalse(os.path.exists(tmpPath))
+
+ self.client.serialize()
+ self.assertTrue(os.path.exists(tmpPath))
+ self.assertEqual(open(tmpPath).read(), """{
+ "calendars": [
+ {
+ "changeToken": "123",
+ "name": "calendar",
+ "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
+ "componentTypes": [
+ "VEVENT"
+ ],
+ "url": "/home/calendar/",
+ "events": [
+ "1.ics"
+ ]
+ },
+ {
+ "changeToken": "789",
+ "name": "calendar",
+ "resourceType": "{urn:ietf:params:xml:ns:caldav}schedule-inbox",
+ "componentTypes": [
+ "VEVENT",
+ "VTODO"
+ ],
+ "url": "/home/inbox/",
+ "events": [
+ "i1.ics"
+ ]
+ },
+ {
+ "changeToken": "456",
+ "name": "tasks",
+ "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
+ "componentTypes": [
+ "VTODO"
+ ],
+ "url": "/home/tasks/",
+ "events": []
+ }
+ ],
+ "principalURL": null,
+ "events": [
+ {
+ "url": "/home/calendar/1.ics",
+ "scheduleTag": null,
+ "etag": "123.123",
+ "icalendar": "BEGIN:VALENDAR\\r\\nEND:VCALENDAR\\r\\n"
+ },
+ {
+ "url": "/home/inbox/i1.ics",
+ "scheduleTag": null,
+ "etag": "123.123",
+ "icalendar": "BEGIN:VALENDAR\\r\\nMETHOD:REQUEST\\r\\nEND:VCALENDAR\\r\\n"
+ }
+ ]
+}""")
+
+
+ def test_deserialization(self):
+ """
+ L{OS_X_10_6.deserailize} properly parses a JSON document.
+ """
+
+ tmp = self.mktemp()
+ os.mkdir(tmp)
+ self.client.serializePath = tmp
+ tmpPath = os.path.join(tmp, "user91-OS_X_10.6.json")
+ open(tmpPath, "w").write("""{
+ "calendars": [
+ {
+ "changeToken": "321",
+ "name": "calendar",
+ "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
+ "componentTypes": [
+ "VEVENT"
+ ],
+ "url": "/home/calendar/",
+ "events": [
+ "2.ics"
+ ]
+ },
+ {
+ "changeToken": "987",
+ "name": "calendar",
+ "resourceType": "{urn:ietf:params:xml:ns:caldav}schedule-inbox",
+ "componentTypes": [
+ "VEVENT",
+ "VTODO"
+ ],
+ "url": "/home/inbox/",
+ "events": [
+ "i2.ics"
+ ]
+ },
+ {
+ "changeToken": "654",
+ "name": "tasks",
+ "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
+ "componentTypes": [
+ "VTODO"
+ ],
+ "url": "/home/tasks/",
+ "events": []
+ }
+ ],
+ "principalURL": null,
+ "events": [
+ {
+ "url": "/home/calendar/2.ics",
+ "scheduleTag": null,
+ "etag": "321.321",
+ "icalendar": "BEGIN:VCALENDAR\\r\\nVERSION:2.0\\r\\nPRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN\\r\\nBEGIN:VEVENT\\r\\nUID:put-1 at example.com\\r\\nDTSTART:20110427\\r\\nDURATION:P1DT\\r\\nDTSTAMP:20051222T205953Z\\r\\nSUMMARY:event 1\\r\\nEND:VEVENT\\r\\nEND:VCALENDAR\\r\\n"
+ },
+ {
+ "url": "/home/inbox/i2.ics",
+ "scheduleTag": null,
+ "etag": "987.987",
+ "icalendar": "BEGIN:VCALENDAR\\r\\nVERSION:2.0\\r\\nMETHOD:REQUEST\\r\\nPRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN\\r\\nBEGIN:VEVENT\\r\\nUID:put-1 at example.com\\r\\nDTSTART:20110427\\r\\nDURATION:P1DT\\r\\nDTSTAMP:20051222T205953Z\\r\\nSUMMARY:event 1\\r\\nEND:VEVENT\\r\\nEND:VCALENDAR\\r\\n"
+ }
+ ]
+}""")
+
+ self.client.deserialize()
+
+ self.assertEqual(len(self.client._calendars), 3)
+ self.assertTrue("/home/calendar/" in self.client._calendars)
+ self.assertEqual(self.client._calendars["/home/calendar/"].changeToken, "321")
+ self.assertEqual(self.client._calendars["/home/calendar/"].name, "calendar")
+ self.assertEqual(self.client._calendars["/home/calendar/"].resourceType, "{urn:ietf:params:xml:ns:caldav}calendar")
+ self.assertEqual(self.client._calendars["/home/calendar/"].componentTypes, set(("VEVENT",)))
+ self.assertTrue("/home/tasks/" in self.client._calendars)
+ self.assertTrue("/home/inbox/" in self.client._calendars)
+ self.assertEqual(self.client._calendars["/home/inbox/"].componentTypes, set(("VEVENT", "VTODO",)))
+ self.assertEqual(len(self.client._events), 2)
+ self.assertTrue("/home/calendar/2.ics" in self.client._events)
+ self.assertEqual(self.client._events["/home/calendar/2.ics"].scheduleTag, None)
+ self.assertEqual(self.client._events["/home/calendar/2.ics"].etag, "321.321")
+ self.assertEqual(self.client._events["/home/calendar/2.ics"].getUID(), "put-1 at example.com")
+ self.assertTrue("/home/inbox/i2.ics" in self.client._events)
+
+
class UpdateCalendarTests(OS_X_10_6Mixin, TestCase):
"""
Tests for L{OS_X_10_6._updateCalendar}.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120529/4e0964ec/attachment-0001.html>
More information about the calendarserver-changes
mailing list