[CalendarServer-changes] [2415] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue May 13 17:13:33 PDT 2008


Revision: 2415
          http://trac.macosforge.org/projects/calendarserver/changeset/2415
Author:   wsanchez at apple.com
Date:     2008-05-13 17:13:33 -0700 (Tue, 13 May 2008)

Log Message:
-----------
Merge http://svn.calendarserver.org/repository/calendarserver/CalendarServer/branches/propfind-cache-3

Modified Paths:
--------------
    CalendarServer/trunk/test
    CalendarServer/trunk/twistedcaldav/config.py
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/root.py
    CalendarServer/trunk/twistedcaldav/static.py
    CalendarServer/trunk/twistedcaldav/test/test_root.py
    CalendarServer/trunk/twistedcaldav/test/util.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/cache.py
    CalendarServer/trunk/twistedcaldav/test/test_cache.py
    CalendarServer/trunk/twistedcaldav/test/test_resource.py
    CalendarServer/trunk/twistedcaldav/test/test_static.py

Modified: CalendarServer/trunk/test
===================================================================
--- CalendarServer/trunk/test	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/test	2008-05-14 00:13:33 UTC (rev 2415)
@@ -24,6 +24,7 @@
     random="--random=$(date "+%s")";
  no_colour="";
 until_fail="";
+  coverage="";
 
 usage ()
 {
@@ -37,18 +38,20 @@
   echo "        -n  Do not use color";
   echo "        -o  Do not run tests in random order.";
   echo "        -u  Run until the tests fail.";
+  echo "        -c  Generate coverage reports.";
 
   if [ "${1-}" == "-" ]; then return 0; fi;
   exit 64;
 }
 
-while getopts "nhou" option; do
+while getopts "nhouc" option; do
   case "${option}" in
     '?') usage; ;;
     'h') usage -; exit 0; ;;
     'o')     random=""; ;;
     'n')  no_colour="--reporter=bwverbose"; ;;
     'u') until_fail="--until-failure"; ;;
+    'c')   coverage="--coverage"; ;;
   esac;
 done;
 shift $((${OPTIND} - 1));
@@ -64,4 +67,4 @@
     test_modules="twistedcaldav twisted";
 fi;
 
-cd "${wd}" && "${twisted}/bin/trial" ${random} ${until_fail} ${no_colour} ${test_modules};
+cd "${wd}" && "${twisted}/bin/trial" ${random} ${until_fail} ${no_colour} ${coverage} ${test_modules};

Copied: CalendarServer/trunk/twistedcaldav/cache.py (from rev 2414, CalendarServer/branches/propfind-cache-3/twistedcaldav/cache.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/cache.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/cache.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -0,0 +1,294 @@
+##
+# Copyright (c) 2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import uuid
+import time
+import os
+
+from zope.interface import implements
+
+from twisted.python.filepath import FilePath
+from twisted.python import log
+
+from twisted.web2.iweb import IResource
+from twisted.web2.dav import davxml
+from twisted.web2.dav.util import allDataFromStream
+from twisted.web2.http import HTTPError, Response
+from twisted.web2.stream import MemoryStream
+
+from twisted.web2.dav.xattrprops import xattrPropertyStore
+
+
+from twistedcaldav.log import LoggingMixIn
+
+
+class CacheTokensProperty(davxml.WebDAVTextElement):
+    namespace = davxml.twisted_private_namespace
+    name = "cacheTokens"
+
+
+
+class CacheChangeNotifier(LoggingMixIn):
+    def __init__(self, propertyStore):
+        self._propertyStore = propertyStore
+        self._token = None
+
+
+    def _newCacheToken(self):
+        return uuid.uuid4()
+
+
+    def changed(self):
+        self.log_debug("Changing Cache Token for %r" % (
+                self._propertyStore))
+        property = CacheTokensProperty.fromString(self._newCacheToken())
+        self._propertyStore.set(property)
+
+
+
+
+class ResponseCache(LoggingMixIn):
+    """
+    An object that caches responses to given requests.
+
+    @ivar CACHE_TIMEOUT: The number of seconds that a cache entry is valid,
+        (default 3600 seconds or 1 hour).
+
+    @ivar _docroot: An L{FilePath} that points to the document root.
+    @ivar _responses: A C{dict} with (request-method, request-uri,
+         principal-uri) keys and (principal-token, uri-token, cache-time,
+         response) values.
+    """
+
+    CACHE_SIZE = 1000
+    propertyStoreFactory = xattrPropertyStore
+
+    def __init__(self, docroot, cacheSize=None):
+        self._docroot = docroot
+        self._responses = {}
+
+        if cacheSize is not None:
+            self.CACHE_SIZE = cacheSize
+
+        self._accessTimes = None
+
+
+    def _tokenForURI(self, uri):
+        """
+        Get a property store for the given C{uri}.
+
+        @param uri: The URI we'd like the token for.
+        @return: A C{str} representing the token for the URI.
+        """
+
+        class __FauxStaticResource(object):
+            def __init__(self, fp):
+                self.fp = fp
+
+
+        fp = self._docroot
+        for childPath in uri.split('/')[:4]:
+            fp = fp.child(childPath)
+
+        props = self.propertyStoreFactory(__FauxStaticResource(fp))
+
+        try:
+            tokenElement = props.get(CacheTokensProperty.qname())
+            return tokenElement.children[0].data
+
+        except HTTPError, err:
+            pass
+
+
+    def _principalURI(self, principal):
+        return str(principal.children[0])
+
+
+    def _time(self):
+        """
+        Return the current time in seconds since the epoch
+        """
+        return time.time()
+
+
+    def getResponseForRequest(self, request):
+        """
+        Retrieve a cached response to the given C{request} otherwise return
+        C{None}
+
+        @param request: An L{IRequest} provider that will be used to locate
+            a cached L{IResponse}.
+
+        @return: An L{IResponse} or C{None} if the response has not been cached.
+        """
+        def _returnRequest(requestBody):
+
+            if requestBody is not None:
+                request.stream = MemoryStream(requestBody)
+                request.stream.doStartReading = None
+
+            principalURI = self._principalURI(request.authnUser)
+
+            key = (request.method,
+                   request.uri,
+                   principalURI,
+                   request.headers.getHeader('depth'),
+                   hash(requestBody))
+
+            self.log_debug("Checking cache for: %r" % (key,))
+
+            request.cacheKey = key
+
+            if key not in self._responses:
+                self.log_debug("Not in cache: %r" % (key,))
+                self.log_debug("  Cache Keys: %r" % (
+                        self._responses.keys(),))
+                return None
+
+            principalToken, uriToken, accessTime, response = self._responses[key]
+
+            if self._tokenForURI(principalURI) != principalToken:
+                self.log_debug("Principal token changed: %r" % (
+                        key,))
+                return None
+
+            elif self._tokenForURI(request.uri) != uriToken:
+                self.log_debug("URI token changed: %r" % (
+                        key,))
+                return None
+
+            response[1].removeHeader('date')
+
+            responseObj = Response(response[0],
+                                   headers=response[1],
+                                   stream=MemoryStream(response[2]))
+
+            self._responses[key] = (principalToken,
+                                    uriToken,
+                                    self._time(),
+                                    response)
+
+            self.log_debug("Found in cache: %r = %r" % (key,
+                                                        responseObj))
+
+            return responseObj
+
+        d = allDataFromStream(request.stream)
+        d.addCallback(_returnRequest)
+        return d
+
+
+    def cacheResponseForRequest(self, request, response):
+        """
+        Cache the given C{response} for the given C{request}.
+
+        @param request: An L{IRequest} provider that will be keyed to the
+            given C{response}.
+
+        @param response: An L{IResponse} provider that will be returned on
+            subsequent checks for the given L{IRequest}
+
+        @return: A deferred that fires when the response has been added
+            to the cache.
+        """
+        def _getRequestBody(responseBody):
+            d1 = allDataFromStream(request.stream)
+            d1.addCallback(lambda requestBody: (requestBody, responseBody))
+            return d1
+
+        def _cacheResponse((requestBody, responseBody)):
+            if requestBody is not None:
+                request.stream = MemoryStream(requestBody)
+                request.stream.doStartReading = None
+
+            principalURI = self._principalURI(request.authnUser)
+
+            if hasattr(request, 'cacheKey'):
+                key = request.cacheKey
+            else:
+                key = (request.method,
+                       request.uri,
+                       principalURI,
+                       request.headers.getHeader('depth'),
+                       hash(requestBody))
+
+            self.log_debug("Adding to cache: %r = %r" % (key,
+                                                         response))
+
+            if len(self._responses) >= self.CACHE_SIZE:
+                leastRecentlyUsedTime = None
+                leastRecentlyUsedKey = None
+
+                for cacheKey, cacheEntry in self._responses.iteritems():
+                    if leastRecentlyUsedTime is None:
+                        leastRecentlyUsedTime = cacheEntry[2]
+                        leastRecentlyUsedKey = cacheKey
+                        continue
+
+                    if leastRecentlyUsedTime < cacheEntry[2]:
+                        leastRecentlyUsedTime = cacheEntry[2]
+                        leastRecentlyUsedKey = cacheKey
+
+                del self._responses[leastRecentlyUsedKey]
+
+
+            self._responses[key] = (self._tokenForURI(principalURI),
+                                    self._tokenForURI(request.uri),
+                                    self._time(),
+                                    (response.code,
+                                     response.headers,
+                                     responseBody))
+
+            self.log_debug("Cache Stats: # keys = %r" % (len(self._responses),))
+
+            response.stream = MemoryStream(responseBody)
+            return response
+
+
+        d = allDataFromStream(response.stream)
+        d.addCallback(_getRequestBody)
+        d.addCallback(_cacheResponse)
+        return d
+
+
+
+class _CachedResponseResource(object):
+    implements(IResource)
+
+    def __init__(self, response):
+        self._response = response
+
+    def renderHTTP(self, request):
+        return self._response
+
+    def locateChild(self, request, segments):
+        return self, []
+
+
+class PropfindCacheMixin(object):
+    def http_PROPFIND(self, request):
+        def _cacheResponse(responseCache, response):
+            return responseCache.cacheResponseForRequest(request, response)
+
+        def _getResponseCache(response):
+            d1 = request.locateResource("/")
+            d1.addCallback(lambda resource: resource.responseCache)
+            d1.addCallback(_cacheResponse, response)
+            return d1
+
+        d = super(PropfindCacheMixin, self).http_PROPFIND(request)
+        d.addCallback(_getResponseCache)
+        return d

Modified: CalendarServer/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/config.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -190,6 +190,9 @@
 
     # Set the maximum number of outstanding requests to this server.
     "MaxRequests": 600,
+
+    # Configure the number of seconds that Propfinds should be cached for.
+    "ResponseCacheSize": 1000
 }
 
 class Config (object):

Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -54,7 +54,7 @@
                 davxml.Protected(),
             ),
         )
-        
+
         # Add admins
         aces += tuple([davxml.ACE(
                     davxml.Principal(davxml.HRef(principal)),
@@ -118,10 +118,10 @@
     def _index(self):
         """
         Return the SQL database for this group principal.
-        
+
         @return: the L{CalendarUserProxyDatabase} for the principal collection.
         """
-        
+
         # The db is located in the principal collection root
         if not hasattr(self.pcollection, "calendar_user_proxy_db"):
             setattr(self.pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(self.pcollection.fp.path))
@@ -166,7 +166,7 @@
 
         # Break out the list into a set of URIs.
         members = [str(h) for h in new_members.children]
-        
+
         # Map the URIs to principals.
         principals = []
         for uri in members:
@@ -178,11 +178,13 @@
                     "Attempt to use a non-existent principal %s as a group member of %s." % (uri, self.principalURL(),)
                 ))
             principals.append(principal)
-        
+            principal.cacheNotifier.changed()
+
         # Map the principals to UIDs.
         uids = [p.principalUID() for p in principals]
 
         self._index().setGroupMembers(self.uid, uids)
+        self.parent.cacheNotifier.changed()
         return succeed(True)
 
     ##
@@ -298,19 +300,19 @@
 
     def hasEditableMembership(self):
         return self.parent.hasEditableProxyMembership()
-        
+
 class CalendarUserProxyDatabase(AbstractSQLDatabase):
     """
     A database to maintain calendar user proxy group memberships.
 
     SCHEMA:
-    
+
     Group Database:
-    
+
     ROW: GROUPNAME, MEMBER
-    
+
     """
-    
+
     dbType = "CALENDARUSERPROXY"
     dbFilename = db_prefix + "calendaruserproxy"
     dbFormatVersion = "4"
@@ -322,11 +324,11 @@
     def setGroupMembers(self, principalUID, members):
         """
         Add a group membership record.
-    
+
         @param principalUID: the UID of the group principal to add.
         @param members: a list UIDs of principals that are members of this group.
         """
-        
+
         # Remove what is there, then add it back.
         self._delete_from_db(principalUID)
         self._add_to_db(principalUID, members)
@@ -335,12 +337,12 @@
     def removeGroup(self, principalUID):
         """
         Remove a group membership record.
-    
+
         @param principalUID: the UID of the group principal to remove.
         """
         self._delete_from_db(principalUID)
         self._db_commit()
-    
+
     def getMembers(self, principalUID):
         """
         Return the list of group member UIDs for the specified principal.
@@ -349,7 +351,7 @@
         for row in self._db_execute("select MEMBER from GROUPS where GROUPNAME = :1", principalUID):
             members.add(row[0])
         return members
-    
+
     def getMemberships(self, principalUID):
         """
         Return the list of group principal UIDs the specified principal is a member of.
@@ -373,7 +375,7 @@
                 values (:1, :2)
                 """, principalUID, member
             )
-       
+
     def _delete_from_db(self, principalUID):
         """
         Deletes the specified entry from the database.
@@ -381,19 +383,19 @@
         @param principalUID: the UID of the group principal to remove.
         """
         self._db_execute("delete from GROUPS where GROUPNAME = :1", principalUID)
-    
+
     def _db_version(self):
         """
         @return: the schema version assigned to this index.
         """
         return CalendarUserProxyDatabase.dbFormatVersion
-        
+
     def _db_type(self):
         """
         @return: the collection type assigned to this index.
         """
         return CalendarUserProxyDatabase.dbType
-        
+
     def _db_init_data_tables(self, q):
         """
         Initialise the underlying database tables.

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -40,6 +40,8 @@
 from twisted.web2.dav.util import joinURL
 
 from twistedcaldav.config import config
+from twistedcaldav.cache import CacheChangeNotifier, PropfindCacheMixin
+
 from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
 from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyPrincipalResource
 from twistedcaldav.directory.directory import DirectoryService
@@ -174,7 +176,7 @@
                 principalResource = typeResource.getChild(segments[2])
                 if principalResource:
                     return principalResource
-            
+
         return None
 
     def principalForCalendarUserAddress(self, address):
@@ -343,10 +345,12 @@
     def principalCollections(self):
         return self.parent.principalCollections()
 
-class DirectoryPrincipalResource (AutoProvisioningFileMixIn, PermissionsMixIn, DAVPrincipalResource, DAVFile):
+class DirectoryPrincipalResource (PropfindCacheMixin, AutoProvisioningFileMixIn, PermissionsMixIn, DAVPrincipalResource, DAVFile):
     """
     Directory principal resource.
     """
+    cacheNotifierFactory = CacheChangeNotifier
+
     def __init__(self, path, parent, record):
         """
         @param path: them path to the file which will back this resource.
@@ -355,6 +359,8 @@
         """
         super(DirectoryPrincipalResource, self).__init__(path)
 
+        self.cacheNotifier = self.cacheNotifierFactory(self.deadProperties())
+
         if self.isCollection():
             slash = "/"
         else:
@@ -380,7 +386,7 @@
         return "(%s) %s" % (self.record.recordType, self.record.shortName)
 
     def provisionFile(self):
-        
+
         result = super(DirectoryPrincipalResource, self).provisionFile()
         if result:
             self.writeDeadProperty(RecordTypeProperty(self.record.recordType))
@@ -440,13 +446,13 @@
     def _calendar_user_proxy_index(self):
         """
         Return the SQL database for calendar user proxies.
-        
+
         @return: the L{CalendarUserProxyDatabase} for the principal collection.
         """
-        
+
         # Get the principal collection we are contained in
         pcollection = self.parent.parent
-        
+
         # The db is located in the principal collection root
         if not hasattr(pcollection, "calendar_user_proxy_db"):
             setattr(pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(pcollection.fp.path))
@@ -511,7 +517,7 @@
 
     def principalUID(self):
         return self.record.guid
-        
+
     ##
     # Static
     ##
@@ -529,6 +535,9 @@
     def listChildren(self):
         return ()
 
+
+
+
 class DirectoryCalendarPrincipalResource (DirectoryPrincipalResource, CalendarPrincipalResource):
     """
     Directory calendar principal resource.
@@ -588,7 +597,7 @@
 
     def autoSchedule(self):
         return self.record.autoSchedule
-    
+
     def proxies(self):
         return self._getRelatives("proxies")
 

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -40,6 +40,8 @@
 from twistedcaldav.directory.principal import DirectoryPrincipalResource
 from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
 
+from twistedcaldav.cache import CacheChangeNotifier
+
 import twistedcaldav.test.util
 
 directoryServices = (
@@ -54,7 +56,7 @@
     """
     def setUp(self):
         super(ProvisionedPrincipals, self).setUp()
-        
+
         # Set up a principals hierarchy for each service we're testing with
         self.principalRootResources = {}
         for directory in directoryServices:
@@ -112,7 +114,7 @@
 
                 shortNames = set(typeResource.listChildren())
                 self.assertEquals(shortNames, set(r.shortName for r in directory.listRecords(recordType)))
-                
+
                 for shortName in shortNames:
                     #print "     -> %s" % (shortName,)
                     recordResource = typeResource.getChild(shortName)
@@ -226,6 +228,15 @@
     # DirectoryPrincipalResource
     ##
 
+    def test_cacheNotifier(self):
+        """
+        Each DirectoryPrincipalResource should have a cacheNotifier attribute
+        that is an instance of CacheChangeNotifier
+        """
+        for provisioningResource, recordType, recordResource, record in self._allRecords():
+            self.failUnless(isinstance(recordResource.cacheNotifier,
+                                       CacheChangeNotifier))
+
     def test_displayName(self):
         """
         DirectoryPrincipalResource.displayName()
@@ -303,34 +314,34 @@
             provisioningResource = CalendarHomeProvisioningFile(path, directory, url)
 
             calendarRootResources[directory.__class__.__name__] = provisioningResource
-        
+
         # Calendar home provisioners should result in calendar homes.
         for provisioningResource, recordType, recordResource, record in self._allRecords():
             if record.enabledForCalendaring:
                 homeURLs = tuple(recordResource.calendarHomeURLs())
                 self.failUnless(homeURLs)
-    
+
                 calendarRootURL = calendarRootResources[record.service.__class__.__name__].url()
-    
+
                 inboxURL = recordResource.scheduleInboxURL()
                 outboxURL = recordResource.scheduleOutboxURL()
-    
+
                 self.failUnless(inboxURL)
                 self.failUnless(outboxURL)
-    
+
                 for homeURL in homeURLs:
                     self.failUnless(homeURL.startswith(calendarRootURL))
-    
+
                     if inboxURL and inboxURL.startswith(homeURL):
                         self.failUnless(len(inboxURL) > len(homeURL))
                         self.failUnless(inboxURL.endswith("/"))
                         inboxURL = None
-    
+
                     if outboxURL and outboxURL.startswith(homeURL):
                         self.failUnless(len(outboxURL) > len(homeURL))
                         self.failUnless(outboxURL.endswith("/"))
                         outboxURL = None
-    
+
                 self.failIf(inboxURL)
                 self.failIf(outboxURL)
 

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -17,11 +17,13 @@
 import os
 
 from twisted.web2.dav.fileop import rmdir
+from twisted.web2.dav import davxml
 
 from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.directory.test.test_xmlfile import xmlFile
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
+from twistedcaldav.directory.principal import DirectoryPrincipalResource
 
 import twistedcaldav.test.util
 
@@ -33,7 +35,7 @@
     """
     def setUp(self):
         super(ProxyPrincipals, self).setUp()
-        
+
         # Set up a principals hierarchy for each service we're testing with
         self.principalRootResources = {}
         name = directoryService.__class__.__name__
@@ -114,7 +116,7 @@
         """
         DirectoryPrincipalResource.groupMembers()
         """
-        
+
         # Setup the fake entry in the DB
         proxy = self._getRecordByShortName(DirectoryService.recordType_users, "cdaboo")
         proxy_group = proxy.getChild("calendar-proxy-write")
@@ -131,7 +133,7 @@
         """
         DirectoryPrincipalResource.groupMembers()
         """
-        
+
         # Setup the fake entry in the DB
         fake_uid = "12345"
         proxy = self._getRecordByShortName(DirectoryService.recordType_users, "cdaboo")
@@ -157,3 +159,66 @@
         """
         provisioningResource = self.principalRootResources[directoryService.__class__.__name__]
         return provisioningResource.principalForShortName(type, name)
+
+
+    def test_setGroupMemberSet(self):
+        class StubMemberDB(object):
+            def __init__(self):
+                self.members = None
+
+            def setGroupMembers(self, uid, members):
+                self.members = members
+
+
+        user = self._getRecordByShortName(directoryService.recordType_users,
+                                           "cdaboo")
+
+        proxy_group = user.getChild("calendar-proxy-write")
+
+        memberdb = StubMemberDB()
+
+        proxy_group._index = (lambda: memberdb)
+
+        new_members = davxml.GroupMemberSet(
+            davxml.HRef.fromString(
+                "/XMLDirectoryService/__uids__/8B4288F6-CC82-491D-8EF9-642EF4F3E7D0/"),
+            davxml.HRef.fromString(
+                "/XMLDirectoryService/__uids__/5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1/"))
+
+        proxy_group.setGroupMemberSet(new_members, None)
+
+        self.assertEquals(
+            set([str(p) for p in memberdb.members]),
+            set(["5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1",
+                 "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0"]))
+
+
+    def test_setGroupMemberSetNotifiesPrincipalCaches(self):
+        class StubCacheNotifier(object):
+            changedCount = 0
+            def changed(self):
+                self.changedCount += 1
+
+        user = self._getRecordByShortName(directoryService.recordType_users,
+                                          "cdaboo")
+
+        proxy_group = user.getChild("calendar-proxy-write")
+
+        notifier = StubCacheNotifier()
+
+        oldCacheNotifier = DirectoryPrincipalResource.cacheNotifierFactory
+
+        try:
+            DirectoryPrincipalResource.cacheNotifierFactory = (lambda _1, _2: notifier)
+
+            self.assertEquals(notifier.changedCount, 0)
+
+            proxy_group.setGroupMemberSet(
+                davxml.GroupMemberSet(
+                    davxml.HRef.fromString(
+                        "/XMLDirectoryService/__uids__/5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1/")),
+                None)
+
+            self.assertEquals(notifier.changedCount, 1)
+        finally:
+            DirectoryPrincipalResource.cacheNotifierFactory = oldCacheNotifier

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -156,7 +156,7 @@
                 d = self.owner(request)
                 d.addCallback(lambda x: davxml.Owner(x))
                 return d
-            
+
         elif namespace == caldav_namespace:
             if name == "supported-calendar-component-set":
                 # CalDAV-access-09, section 5.2.3
@@ -195,7 +195,7 @@
                         responsecode.NOT_IMPLEMENTED,
                         "Component %s is not supported by this server" % (component.toxml(),)
                     ))
-                    
+
         # Strictly speaking CalDAV:timezone is a live property in the sense that the
         # server enforces what can be stored, however it need not actually
         # exist so we cannot list it in liveProperties on this resource, since its
@@ -214,6 +214,15 @@
 
         return super(CalDAVResource, self).writeProperty(property, request)
 
+    def writeDeadProperty(self, property):
+        val = super(CalDAVResource, self).writeDeadProperty(property)
+
+        if hasattr(self, 'cacheNotifier'):
+            self.cacheNotifier.changed()
+
+        return val
+
+
     ##
     # ACL
     ##
@@ -313,7 +322,7 @@
         owner = d.getResult()
         result = (davxml.Principal(owner) == self.currentPrincipal(request))
         yield result
- 
+
     ##
     # CalDAV
     ##
@@ -351,13 +360,13 @@
 
         def checkPrivilegesError(failure):
             failure.trap(AccessDeniedError)
-            
+
             reactor.callLater(0, getChild)
 
         def checkPrivileges(child):
             if privileges is None:
                 return child
-   
+
             ca = child.checkPrivileges(request, privileges)
             ca.addCallback(lambda ign: child)
             return ca
@@ -366,7 +375,7 @@
             if child.isCalendarCollection():
                 callback(child, childpath)
             elif child.isCollection():
-                if depth == "infinity": 
+                if depth == "infinity":
                     fc = child.findCalendarCollections(depth, request, callback, privileges)
                     fc.addCallback(lambda x: reactor.callLater(0, getChild))
                     return fc
@@ -478,10 +487,10 @@
         calendar collection have the same privileges unless explicitly overridden. The same applies
         to drop box collections as we want all resources (attachments) to have the same privileges as
         the drop box collection.
-        
+
         @param newaces: C{list} of L{ACE} for ACL being set.
         """
-        
+
         # Do this only for regular calendar collections and Inbox/Outbox
         if self.isPseudoCalendarCollection():
             edited_aces = []
@@ -494,7 +503,7 @@
                     edited_aces.append(ace)
         else:
             edited_aces = newaces
-        
+
         # Do inherited with possibly modified set of aces
         super(CalDAVResource, self).writeNewACEs(edited_aces)
 
@@ -660,14 +669,14 @@
             def getFreeBusy(has):
                 if not has:
                     return ()
-    
+
                 def parseFreeBusy(freeBusySet):
                     return tuple(str(href) for href in freeBusySet.children)
-        
+
                 d = inbox.readProperty((caldav_namespace, "calendar-free-busy-set"), request)
                 d.addCallback(parseFreeBusy)
                 return d
-    
+
             d = inbox.hasProperty((caldav_namespace, "calendar-free-busy-set"), request)
             d.addCallback(getFreeBusy)
             return d
@@ -695,10 +704,10 @@
         """
         if self.hasDeadProperty((caldav_namespace, "schedule-outbox-URL")):
             outbox = self.readDeadProperty((caldav_namespace, "schedule-outbox-URL"))
-            return str(outbox.children[0])        
+            return str(outbox.children[0])
         else:
             return None
-        
+
     def dropboxURL(self):
         """
         @return: the drop box home collection URL for this principal.
@@ -708,7 +717,7 @@
             return str(inbox.children[0])
         else:
             return None
-        
+
 ##
 # Utilities
 ##

Modified: CalendarServer/trunk/twistedcaldav/root.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/root.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/root.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -25,6 +25,7 @@
 
 from twistedcaldav.extensions import DAVFile
 from twistedcaldav.config import config
+from twistedcaldav.cache import ResponseCache, _CachedResponseResource
 from twistedcaldav.log import Logger
 
 log = Logger()
@@ -51,6 +52,8 @@
 
         self.contentFilters = []
 
+        self.responseCache = ResponseCache(self.fp, config.ResponseCacheSize)
+
         if config.ResponseCompression:
             from twisted.web2.filter import gzip
             self.contentFilters.append((gzip.gzipfilter, True))
@@ -88,19 +91,19 @@
             # Figure out the "username" from the davxml.Principal object
             request.checkingSACL = True
             d = request.locateResource(authzUser.children[0].children[0].data)
-            
+
             def _checkedSACLCb(principal):
                 delattr(request, "checkingSACL")
                 username = principal.record.shortName
-                
+
                 if RootResource.CheckSACL(username, self.saclService) != 0:
                     log.msg("User '%s' is not enabled with the '%s' SACL" % (username, self.saclService,))
                     return Failure(HTTPError(403))
-    
+
                 # Mark SACL's as having been checked so we can avoid doing it multiple times
                 request.checkedSACL = True
                 return True
-            
+
             d.addCallback(_checkedSACLCb)
             return d
 
@@ -110,6 +113,18 @@
         return d
 
     def locateChild(self, request, segments):
+        def _authCb((authnUser, authzUser)):
+            request.authnUser = authnUser
+            request.authzUser = authzUser
+
+        def _authEb(failure):
+            # Make sure we propogate UnauthorizedLogin errors.
+            failure.trap(UnauthorizedLogin, LoginFailed)
+
+            return Failure(HTTPError(UnauthorizedResponse(
+                        request.credentialFactories,
+                        request.remoteAddr)))
+
         for filter in self.contentFilters:
             request.addResponseFilter(filter[0], atEnd=filter[1])
 
@@ -120,6 +135,29 @@
 
             return d
 
+        def _getCachedResource(_ign, request):
+            d = self.responseCache.getResponseForRequest(request)
+            d.addCallback(_serveResponse)
+            return d
+
+        def _serveResponse(response):
+            if response is None:
+                request.notInCache = True
+                raise KeyError("Not found in cache.")
+
+            return _CachedResponseResource(response), []
+
+        def _resourceNotInCacheEb(failure):
+            return super(RootResource, self).locateChild(request,segments)
+
+        if request.method == 'PROPFIND' and not getattr(
+            request, 'notInCache', False):
+            d = defer.maybeDeferred(self.authenticate, request)
+            d.addCallbacks(_authCb, _authEb)
+            d.addCallback(_getCachedResource, request)
+            d.addErrback(_resourceNotInCacheEb)
+            return d
+
         return super(RootResource, self).locateChild(request, segments)
 
     def http_COPY       (self, request): return responsecode.FORBIDDEN

Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/static.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -46,7 +46,7 @@
 from twisted.web2.dav.idav import IDAVResource
 from twisted.web2.dav.resource import AccessDeniedError
 from twisted.web2.dav.resource import davPrivilegeSet
-from twisted.web2.dav.util import parentForURL, bindMethods
+from twisted.web2.dav.util import parentForURL, bindMethods, allDataFromStream
 
 from twistedcaldav import caldavxml
 from twistedcaldav import customxml
@@ -67,6 +67,8 @@
 from twistedcaldav.directory.resource import AutoProvisioningResourceMixIn
 from twistedcaldav.log import Logger
 
+from twistedcaldav.cache import CacheChangeNotifier, PropfindCacheMixin
+
 log = Logger()
 
 class CalDAVFile (CalDAVResource, DAVFile):
@@ -115,9 +117,9 @@
                     responsecode.FORBIDDEN,
                     (caldavxml.caldav_namespace, "calendar-collection-location-ok")
                 ))
-    
+
             return self.createCalendarCollection()
-            
+
         parent = self._checkParents(request, isPseudoCalendarCollectionResource)
         parent.addCallback(_defer)
         return parent
@@ -129,13 +131,13 @@
         def onCalendarCollection(status):
             if status != responsecode.CREATED:
                 raise HTTPError(status)
-    
+
             # Initialize CTag on the calendar collection
             self.updateCTag()
-            
+
             # Create the index so its ready when the first PUTs come in
             self.index().create()
-            
+
             return status
 
         d = self.createSpecialCollection(davxml.ResourceType.calendar)
@@ -149,10 +151,10 @@
         def onCollection(status):
             if status != responsecode.CREATED:
                 raise HTTPError(status)
-    
+
             self.writeDeadProperty(resourceType)
             return status
-        
+
         def onError(f):
             try:
                 rmdir(self.fp)
@@ -165,7 +167,7 @@
             d.addCallback(onCollection)
         d.addErrback(onError)
         return d
- 
+
     def iCalendarRolledup(self, request):
         if self.isPseudoCalendarCollection():
             # Generate a monolithic calendar
@@ -187,7 +189,7 @@
                     child = IDAVResource(child)
                 except TypeError:
                     child = None
-    
+
                 if child is not None:
                     # Check privileges of child - skip if access denied
                     try:
@@ -201,7 +203,7 @@
 
                     for component in subcalendar.subcomponents():
                         calendar.addComponent(component)
-                        
+
             yield calendar
             return
 
@@ -254,7 +256,7 @@
             d = self.locateParent(request, request.urlForResource(self))
             d.addCallback(gotParent)
             return d
-        
+
         return super(CalDAVFile, self).supportedPrivileges(request)
 
     ##
@@ -301,17 +303,17 @@
                 """
                 Recursively descend the directory tree rooted at top,
                 calling the callback function for each regular file
-                
+
                 @param top: L{FilePath} for the directory to walk.
                 """
-            
+
                 total = 0
                 for f in top.listdir():
-    
+
                     # Ignore the database
                     if f.startswith("."):
                         continue
-    
+
                     child = top.child(f)
                     if child.isdir():
                         # It's a directory, recurse into it
@@ -324,11 +326,11 @@
                     else:
                         # Unknown file type, print a message
                         pass
-            
+
                 yield total
-            
+
             walktree = deferredGenerator(walktree)
-    
+
             return walktree(self.fp)
         else:
             return succeed(self.fp.getsize())
@@ -352,20 +354,20 @@
         #
         # Parse the URI
         #
-    
+
         (scheme, host, path, query, fragment) = urlsplit(uri) #@UnusedVariable
-    
+
         # Request hostname and child uri hostname have to be the same.
         if host and host != request.headers.getHeader("host"):
             return False
-        
+
         # Child URI must start with request uri text.
         parent = request.uri
         if not parent.endswith("/"):
             parent += "/"
-            
+
         return path.startswith(parent) and (len(path) > len(parent)) and (not immediateChild or (path.find("/", len(parent)) == -1))
-    
+
     def _checkParents(self, request, test):
         """
         @param request: the request being processed.
@@ -390,7 +392,7 @@
                 return
 
         yield None
-    
+
     _checkParents = deferredGenerator(_checkParents)
 
 class AutoProvisioningFileMixIn (AutoProvisioningResourceMixIn):
@@ -411,7 +413,7 @@
             parent = self.parent
             if not parent.exists() and isinstance(parent, AutoProvisioningFileMixIn):
                 parent.provision()
-                
+
             assert parent.exists(), "Parent %s of %s does not exist" % (parent, self)
             assert parent.isCollection(), "Parent %s of %s is not a collection" % (parent, self)
 
@@ -432,7 +434,7 @@
 
 class CalendarHomeProvisioningFile (AutoProvisioningFileMixIn, DirectoryCalendarHomeProvisioningResource, DAVFile):
     """
-    Resource which provisions calendar home collections as needed.    
+    Resource which provisions calendar home collections as needed.
     """
     def __init__(self, path, directory, url):
         """
@@ -518,7 +520,7 @@
     def createSimilarFile(self, path):
         raise HTTPError(responsecode.NOT_FOUND)
 
-class CalendarHomeFile (AutoProvisioningFileMixIn, DirectoryCalendarHomeResource, CalDAVFile):
+class CalendarHomeFile (PropfindCacheMixin, AutoProvisioningFileMixIn, DirectoryCalendarHomeResource, CalDAVFile):
     """
     Calendar home collection resource.
     """
@@ -528,6 +530,7 @@
         """
         CalDAVFile.__init__(self, path)
         DirectoryCalendarHomeResource.__init__(self, parent, record)
+        self.cacheNotifier = CacheChangeNotifier(self.deadProperties())
 
     def provisionChild(self, name):
         if config.EnableDropBox:
@@ -550,7 +553,9 @@
         if path == self.fp.path:
             return self
         else:
-            return CalDAVFile(path, principalCollections=self.principalCollections())
+            similar = CalDAVFile(path, principalCollections=self.principalCollections())
+            similar.cacheNotifier = self.cacheNotifier
+            return similar
 
     def getChild(self, name):
         # This avoids finding case variants of put children on case-insensitive filesystems.
@@ -559,6 +564,7 @@
 
         return super(CalendarHomeFile, self).getChild(name)
 
+
 class ScheduleFile (AutoProvisioningFileMixIn, CalDAVFile):
     def __init__(self, path, parent):
         super(ScheduleFile, self).__init__(path, principalCollections=parent.principalCollections())
@@ -592,6 +598,7 @@
             (caldav_namespace, "calendar-collection-location-ok")
         )
 
+
     ##
     # ACL
     ##

Copied: CalendarServer/trunk/twistedcaldav/test/test_cache.py (from rev 2414, CalendarServer/branches/propfind-cache-3/twistedcaldav/test/test_cache.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_cache.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_cache.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -0,0 +1,254 @@
+##
+# Copyright (c) 2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from new import instancemethod
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python.filepath import FilePath
+
+from twisted.web2.dav import davxml
+from twisted.web2.dav.util import allDataFromStream
+from twisted.web2.stream import MemoryStream
+from twisted.web2.http_headers import Headers
+
+from twistedcaldav.cache import CacheChangeNotifier
+from twistedcaldav.cache import CacheTokensProperty
+from twistedcaldav.cache import ResponseCache
+
+from twistedcaldav.test.util import InMemoryPropertyStore
+
+
+def _newCacheToken(self):
+    called = getattr(self, '_called', 0)
+
+    token = 'token%d' % (called,)
+    setattr(self, '_called', called + 1)
+    return token
+
+
+
+class StubRequest(object):
+    def __init__(self, method, uri, authnUser, depth=1, body=None):
+        self.method = method
+        self.uri = uri
+        self.authnUser = davxml.Principal(davxml.HRef.fromString(authnUser))
+        self.headers = Headers({'depth': depth})
+
+        if body is None:
+            body = "foobar"
+
+        self.body = body
+        self.stream = MemoryStream(body)
+
+
+class StubResponse(object):
+    def __init__(self, code, headers, body):
+        self.code = code
+        self.headers = Headers(headers)
+        self.body = body
+        self.stream = MemoryStream(body)
+
+
+
+class CacheChangeNotifierTests(TestCase):
+    def setUp(self):
+        self.props = InMemoryPropertyStore()
+        self.ccn = CacheChangeNotifier(self.props)
+        self.ccn._newCacheToken = instancemethod(_newCacheToken,
+                                                 self.ccn,
+                                                 CacheChangeNotifier)
+
+
+    def test_cacheTokenPropertyIsProvisioned(self):
+        self.ccn.changed()
+        token = self.props._properties[CacheTokensProperty.qname()
+                                        ].children[0].data
+        self.assertEquals(token, 'token0')
+
+
+    def test_changedChangesToken(self):
+        self.ccn.changed()
+        self.ccn.changed()
+        token = self.props._properties[CacheTokensProperty.qname()
+                                        ].children[0].data
+        self.assertEquals(token, 'token1')
+
+
+
+class ResponseCacheTests(TestCase):
+    def setUp(self):
+        self.tokens = {
+                '/calendars/users/cdaboo/': 'uriToken0',
+                '/principals/users/cdaboo/': 'principalToken0',
+                '/principals/users/dreid/': 'principalTokenX'}
+
+        self.rc = ResponseCache(None)
+        self.rc._tokenForURI = self.tokens.get
+
+        self.rc._time = (lambda:0)
+
+        self.expected_response = (200, Headers({}), "Foo")
+
+        self.rc._responses[(
+                'PROPFIND',
+                '/calendars/users/cdaboo/',
+                '/principals/users/cdaboo/',
+                1,
+                hash('foobar'),
+                )] = (
+            'principalToken0', 'uriToken0', 0, self.expected_response)
+
+
+    def assertResponse(self, response, expected):
+        self.assertEquals(response.code, expected[0])
+        self.assertEquals(response.headers, expected[1])
+
+        d = allDataFromStream(response.stream)
+        d.addCallback(self.assertEquals, expected[2])
+        return d
+
+
+    def test_getResponseForRequestNotInCache(self):
+        d = self.rc.getResponseForRequest(StubRequest(
+                'PROPFIND',
+                '/calendars/users/dreid/',
+                '/principals/users/dreid/'))
+
+        d.addCallback(self.assertEquals, None)
+        return d
+
+    def test_getResponseForRequestInCache(self):
+        d = self.rc.getResponseForRequest(StubRequest(
+                'PROPFIND',
+                '/calendars/users/cdaboo/',
+                '/principals/users/cdaboo/'))
+
+        d.addCallback(self.assertResponse, self.expected_response)
+        return d
+
+
+    def test_getResponseForRequestPrincipalTokenChanged(self):
+        self.tokens['/principals/users/cdaboo/'] = 'principalToken1'
+
+        d = self.rc.getResponseForRequest(StubRequest(
+                'PROPFIND',
+                '/calendars/users/cdaboo/',
+                '/principals/users/cdaboo/'))
+
+        d.addCallback(self.assertEquals, None)
+        return d
+
+
+    def test_getResponseForRequestUriTokenChanged(self):
+        self.tokens['/calendars/users/cdaboo/'] = 'uriToken1'
+
+        d = self.rc.getResponseForRequest(StubRequest(
+                'PROPFIND',
+                '/calendars/users/cdaboo/',
+                '/principals/users/cdaboo/'))
+
+        d.addCallback(self.assertEquals, None)
+        return d
+
+
+    def test_getResponseForDepthZero(self):
+        d = self.rc.getResponseForRequest(StubRequest(
+                'PROPFIND',
+                '/calendars/users/cdaboo/',
+                '/principals/users/cdaboo/',
+                depth=0))
+
+        d.addCallback(self.assertEquals, None)
+        return d
+
+
+    def test_getResponseForBody(self):
+        d = self.rc.getResponseForRequest(StubRequest(
+                'PROPFIND',
+                '/calendars/users/cdaboo/',
+                '/principals/users/cdaboo',
+                body='bazbax'))
+
+        d.addCallback(self.assertEquals, None)
+        return d
+
+
+    def test_cacheResponseForRequest(self):
+        expected_response = StubResponse(200, {}, "Foobar")
+
+        def _assertResponse(ign):
+            d1 = self.rc.getResponseForRequest(StubRequest(
+                    'PROPFIND',
+                    '/principals/users/dreid/',
+                    '/principals/users/dreid/'))
+
+
+            d1.addCallback(self.assertResponse,
+                           (expected_response.code,
+                            expected_response.headers,
+                            expected_response.body))
+            return d1
+
+        d = self.rc.cacheResponseForRequest(
+            StubRequest('PROPFIND',
+                        '/principals/users/dreid/',
+                        '/principals/users/dreid/'),
+            expected_response)
+
+        d.addCallback(_assertResponse)
+        return d
+
+
+    def test__tokenForURI(self):
+        docroot = FilePath(self.mktemp())
+        principal = docroot.child('principals').child('users').child('wsanchez')
+
+        expected_token = "wsanchezToken0"
+
+        props = InMemoryPropertyStore()
+        props._properties[CacheTokensProperty.qname()
+                          ] = CacheTokensProperty.fromString(expected_token)
+
+        stores = {principal.path: props}
+
+        rc = ResponseCache(docroot)
+
+        rc.propertyStoreFactory = (lambda rsrc: stores[rsrc.fp.path])
+
+        token = rc._tokenForURI('/principals/users/wsanchez')
+        self.assertEquals(token, expected_token)
+
+
+    def test_cacheSizeExceeded(self):
+        self.rc.CACHE_SIZE = 1
+        def _assertResponse(ign):
+            d1 = self.rc.getResponseForRequest(StubRequest(
+                    'PROPFIND',
+                    '/calendars/users/cdaboo/',
+                    '/principals/users/cdaboo/'))
+
+            d1.addCallback(self.assertEquals, None)
+            return d1
+
+        d = self.rc.cacheResponseForRequest(
+            StubRequest('PROPFIND',
+                        '/principals/users/dreid/',
+                        '/principals/users/dreid/'),
+            StubResponse(200, {}, "Foobar"))
+
+        d.addCallback(_assertResponse)
+        return d

Copied: CalendarServer/trunk/twistedcaldav/test/test_resource.py (from rev 2414, CalendarServer/branches/propfind-cache-3/twistedcaldav/test/test_resource.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_resource.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_resource.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -0,0 +1,58 @@
+##
+# Copyright (c) 2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.trial.unittest import TestCase
+
+from twistedcaldav.resource import CalDAVResource
+
+from twistedcaldav.test.util import StubCacheChangeNotifier
+from twistedcaldav.test.util import InMemoryPropertyStore
+
+
+class StubProperty(object):
+    def qname(self):
+        return "StubQname"
+
+
+class CalDAVResourceTests(TestCase):
+    def setUp(self):
+        self.resource = CalDAVResource()
+        self.resource._dead_properties = InMemoryPropertyStore()
+        self.resource.cacheNotifier = StubCacheChangeNotifier()
+
+
+    def test_writeDeadPropertyNotifiesCache(self):
+        self.resource.writeDeadProperty(StubProperty())
+        self.assertEquals(self.resource.cacheNotifier.changedCount, 1)
+
+
+    def test_writeDeadPropertyWritesProperty(self):
+        prop = StubProperty()
+        self.resource.writeDeadProperty(prop)
+        self.assertEquals(self.resource._dead_properties.get("StubQname"),
+                          prop)
+
+
+    def test_writeDeadPropertyFailureDoesntNotifyCache(self):
+        self.resource._dead_properties = None
+        self.assertRaises(Exception,
+                          self.resource.writeDeadProperty, StubProperty())
+        self.assertEquals(self.resource.cacheNotifier.changedCount, 0)
+
+
+    def test_writeDeadPropertyDoesntFailWithoutACacheNotifier(self):
+        del self.resource.cacheNotifier
+        self.resource.writeDeadProperty(StubProperty())

Modified: CalendarServer/trunk/twistedcaldav/test/test_root.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_root.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/test/test_root.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -64,11 +64,11 @@
             '/principals/',
             directory)
 
-        # Otherwise the tests that never touch the root resource will 
+        # Otherwise the tests that never touch the root resource will
         # fail on teardown.
         principals.provision()
 
-        root = RootResource(self.docroot, 
+        root = RootResource(self.docroot,
                             principalCollections=[principals])
 
         root.putChild('principals',
@@ -78,8 +78,8 @@
         portal.registerChecker(directory)
 
         self.root = auth.AuthenticationWrapper(
-            root, 
-            portal, 
+            root,
+            portal,
             credentialFactories=(basic.BasicCredentialFactory("Test realm"),),
             loginInterfaces=(auth.IPrincipal,))
 
@@ -88,7 +88,7 @@
     def test_noSacls(self):
         """
         Test the behaviour of locateChild when SACLs are not enabled.
-        
+
         should return a valid resource
         """
         self.root.resource.useSacls = False
@@ -111,7 +111,7 @@
 
     def test_inSacls(self):
         """
-        Test the behavior of locateChild when SACLs are enabled and the 
+        Test the behavior of locateChild when SACLs are enabled and the
         user is in the SACL group
 
         should return a valid resource
@@ -125,7 +125,7 @@
             headers=http_headers.Headers({
                     'Authorization': ['basic', '%s' % (
                             'dreid:dierd'.encode('base64'),)]}))
-        
+
         resrc, segments = self.root.locateChild(request,
                                          ['principals'])
 
@@ -137,10 +137,10 @@
 
             self.assertEquals(segments, [])
 
-            self.assertEquals(request.authzUser, 
+            self.assertEquals(request.authzUser,
                               davxml.Principal(
                     davxml.HRef('/principals/__uids__/5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1/')))
-            
+
         d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
         d.addCallback(_Cb)
 
@@ -150,7 +150,7 @@
         """
         Test the behavior of locateChild when SACLs are enabled and the
         user is not in the SACL group
-        
+
         should return a 403 forbidden response
         """
         self.root.resource.useSacls = True
@@ -162,14 +162,14 @@
             headers=http_headers.Headers({
                     'Authorization': ['basic', '%s' % (
                             'wsanchez:zehcnasw'.encode('base64'),)]}))
-        
+
         resrc, segments = self.root.locateChild(request,
                                          ['principals'])
 
         def _Eb(failure):
             failure.trap(HTTPError)
             self.assertEquals(failure.value.response.code, 403)
-            
+
         d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
         d.addErrback(_Eb)
 
@@ -179,7 +179,7 @@
         """
         Test the behavior of locateChild when SACLs are enabled and the request
         is unauthenticated
-        
+
         should return a 401 UnauthorizedResponse
         """
 
@@ -208,7 +208,7 @@
 
     def test_badCredentials(self):
         """
-        Test the behavior of locateChild when SACLS are enabled, and 
+        Test the behavior of locateChild when SACLS are enabled, and
         incorrect credentials are given.
 
         should return a 401 UnauthorizedResponse
@@ -222,14 +222,14 @@
             headers=http_headers.Headers({
                     'Authorization': ['basic', '%s' % (
                             'dreid:dreid'.encode('base64'),)]}))
-        
+
         resrc, segments = self.root.locateChild(request,
                                          ['principals'])
 
         def _Eb(failure):
             failure.trap(HTTPError)
             self.assertEquals(failure.value.response.code, 401)
-            
+
         d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
         d.addErrback(_Eb)
 
@@ -241,7 +241,7 @@
 
             if response.code != responsecode.FORBIDDEN:
                 self.fail("Incorrect response for DELETE /: %s" % (response.code,))
-            
+
         request = SimpleRequest(self.site, "DELETE", "/")
         return self.send(request, do_test)
 
@@ -251,7 +251,7 @@
 
             if response.code != responsecode.FORBIDDEN:
                 self.fail("Incorrect response for COPY /: %s" % (response.code,))
-            
+
         request = SimpleRequest(
             self.site,
             "COPY",
@@ -266,7 +266,7 @@
 
             if response.code != responsecode.FORBIDDEN:
                 self.fail("Incorrect response for MOVE /: %s" % (response.code,))
-            
+
         request = SimpleRequest(
             self.site,
             "MOVE",
@@ -274,4 +274,3 @@
             headers=http_headers.Headers({"Destination":"/copy/"})
         )
         return self.send(request, do_test)
-        

Copied: CalendarServer/trunk/twistedcaldav/test/test_static.py (from rev 2414, CalendarServer/branches/propfind-cache-3/twistedcaldav/test/test_static.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_static.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_static.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -0,0 +1,61 @@
+##
+# Copyright (c) 2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.trial.unittest import TestCase
+
+from twistedcaldav.static import CalendarHomeFile, CalDAVFile
+from twistedcaldav.cache import CacheChangeNotifier
+from twistedcaldav.test.util import StubCacheChangeNotifier
+
+class StubParentResource(object):
+    def principalCollections(self):
+        return set([])
+
+
+class CalendarHomeFileTests(TestCase):
+    def setUp(self):
+        self.calendarHome = CalendarHomeFile(self.mktemp(),
+                                             StubParentResource(),
+                                             object())
+
+
+    def test_hasCacheNotifier(self):
+        self.failUnless(isinstance(self.calendarHome.cacheNotifier,
+                                   CacheChangeNotifier))
+
+
+    def test_childrenHaveCacheNotifier(self):
+        child = self.calendarHome.createSimilarFile('/fake/path')
+        self.assertEquals(child.cacheNotifier, self.calendarHome.cacheNotifier)
+
+
+class CalDAVFileTests(TestCase):
+    def setUp(self):
+        self.caldavFile = CalDAVFile(self.mktemp())
+        self.caldavFile.fp.createDirectory()
+        self.caldavFile.cacheNotifier = StubCacheChangeNotifier()
+        self.assertEquals(self.caldavFile.cacheNotifier.changedCount, 0)
+        self.caldavFile.isCollection = (lambda: True)
+
+
+    def test_updateCTagNotifiesCache(self):
+        self.caldavFile.updateCTag()
+        self.assertEquals(self.caldavFile.cacheNotifier.changedCount, 1)
+
+
+    def test_updateCTagDoesntFailWithoutACacheNotifier(self):
+        del self.caldavFile.cacheNotifier
+        self.caldavFile.updateCTag()

Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py	2008-05-13 21:26:23 UTC (rev 2414)
+++ CalendarServer/trunk/twistedcaldav/test/util.py	2008-05-14 00:13:33 UTC (rev 2415)
@@ -15,8 +15,31 @@
 ##
 
 import twisted.web2.dav.test.util
+from twisted.web2.http import HTTPError, StatusResponse
 
 from twistedcaldav.static import CalDAVFile
 
+
 class TestCase(twisted.web2.dav.test.util.TestCase):
     resource_class = CalDAVFile
+
+
+class InMemoryPropertyStore(object):
+    def __init__(self):
+        self._properties = {}
+
+    def get(self, qname):
+        data = self._properties.get(qname)
+        if data is None:
+            raise HTTPError(StatusResponse(404, "No such property"))
+        return data
+
+    def set(self, property):
+        self._properties[property.qname()] = property
+
+
+class StubCacheChangeNotifier(object):
+    changedCount = 0
+
+    def changed(self):
+        self.changedCount += 1

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080513/d6a830e5/attachment-0001.html


More information about the calendarserver-changes mailing list