[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