Revision: 9309 http://trac.macosforge.org/projects/calendarserver/changeset/9309 Author: cdaboo@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@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@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@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@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}.
participants (1)
-
source_changes@macosforge.org