[CalendarServer-changes] [3872] CalendarServer/branches/exarkun/update-twisted-3816/lib-patches/ Twisted

source_changes at macosforge.org source_changes at macosforge.org
Mon Mar 16 13:13:22 PDT 2009


Revision: 3872
          http://trac.macosforge.org/projects/calendarserver/changeset/3872
Author:   exarkun at twistedmatrix.com
Date:     2009-03-16 13:13:22 -0700 (Mon, 16 Mar 2009)
Log Message:
-----------
Update xattrprops to apply to Twisted; add some unit tests

Modified Paths:
--------------
    CalendarServer/branches/exarkun/update-twisted-3816/lib-patches/Twisted/twisted.web2.dav.xattrprops.patch

Added Paths:
-----------
    CalendarServer/branches/exarkun/update-twisted-3816/lib-patches/Twisted/twisted.web2.dav.test.test_xattrprops.patch

Added: CalendarServer/branches/exarkun/update-twisted-3816/lib-patches/Twisted/twisted.web2.dav.test.test_xattrprops.patch
===================================================================
--- CalendarServer/branches/exarkun/update-twisted-3816/lib-patches/Twisted/twisted.web2.dav.test.test_xattrprops.patch	                        (rev 0)
+++ CalendarServer/branches/exarkun/update-twisted-3816/lib-patches/Twisted/twisted.web2.dav.test.test_xattrprops.patch	2009-03-16 20:13:22 UTC (rev 3872)
@@ -0,0 +1,238 @@
+Index: twisted/web2/dav/test/test_xattrprops.py
+===================================================================
+--- twisted/web2/dav/test/test_xattrprops.py	(revision 0)
++++ twisted/web2/dav/test/test_xattrprops.py	(revision 0)
+@@ -0,0 +1,233 @@
++# Copyright (c) 2009 Twisted Matrix Laboratories.
++# See LICENSE for details.
++
++"""
++Tests for L{twisted.web2.dav.xattrprops}.
++"""
++
++from zlib import compress, decompress
++from pickle import dumps
++from cPickle import UnpicklingError
++
++from twisted.python.filepath import FilePath
++from twisted.trial.unittest import TestCase
++from twisted.web2.responsecode import NOT_FOUND, INTERNAL_SERVER_ERROR
++from twisted.web2.responsecode import FORBIDDEN
++from twisted.web2.http import HTTPError
++from twisted.web2.dav.static import DAVFile
++from twisted.web2.dav.davxml import Depth, WebDAVDocument
++
++try:
++    from twisted.web2.dav.xattrprops import xattrPropertyStore
++except ImportError:
++    xattrPropertyStore = None
++else:
++    from xattr import xattr
++
++class ExtendedAttributesPropertyStoreTests(TestCase):
++    """
++    Tests for L{xattrPropertyStore}.
++    """
++    if xattrPropertyStore is None:
++        skip = "xattr package missing, cannot test xattr property store"
++
++    def setUp(self):
++        """
++        Create a resource and a xattr property store for it.
++        """
++        self.resourcePath = FilePath(self.mktemp())
++        self.resourcePath.setContent("")
++        self.attrs = xattr(self.resourcePath.path)
++        self.resource = DAVFile(self.resourcePath.path)
++        self.propertyStore = xattrPropertyStore(self.resource)
++
++
++    def test_getAbsent(self):
++        """
++        L{xattrPropertyStore.get} raises L{HTTPError} with a I{NOT FOUND}
++        response code if passed the name of an attribute for which there is no
++        corresponding value.
++        """
++        error = self.assertRaises(HTTPError, self.propertyStore.get, ("foo", "bar"))
++        self.assertEquals(error.response.code, NOT_FOUND)
++
++
++    def _makeValue(self):
++        """
++        Create and return any old WebDAVDocument for use by the get tests.
++        """
++        element = Depth("0")
++        document = WebDAVDocument(element)
++        return document
++
++
++    def _setValue(self, originalDocument, value):
++        element = originalDocument.root_element
++        attribute = (
++            self.propertyStore.deadPropertyXattrPrefix +
++            "{%s}%s" % element.qname())
++        self.attrs[attribute] = value
++
++
++    def _getValue(self, originalDocument):
++        element = originalDocument.root_element
++        attribute = (
++            self.propertyStore.deadPropertyXattrPrefix +
++            "{%s}%s" % element.qname())
++        return self.attrs[attribute]
++
++
++    def _checkValue(self, originalDocument):
++        property = originalDocument.root_element.qname()
++
++        # Try to load it via xattrPropertyStore.get
++        loadedDocument = self.propertyStore.get(property)
++
++        # XXX Why isn't this a WebDAVDocument?
++        self.assertIsInstance(loadedDocument, Depth)
++        self.assertEquals(str(loadedDocument), "0")
++
++
++    def test_getXML(self):
++        """
++        If there is an XML document associated with the property name passed to
++        L{xattrPropertyStore.get}, that value is parsed into a
++        L{WebDAVDocument}, the root element of which C{get} then returns.
++        """
++        document = self._makeValue()
++        self._setValue(document, document.toxml())
++        self._checkValue(document)
++
++
++    def test_getCompressed(self):
++        """
++        If there is a compressed value associated with the property name passed
++        to L{xattrPropertyStore.get}, that value is decompressed and parsed
++        into a L{WebDAVDocument}, the root element of which C{get} then
++        returns.
++        """
++        document = self._makeValue()
++        self._setValue(document, compress(document.toxml()))
++        self._checkValue(document)
++
++
++    def test_getPickled(self):
++        """
++        If there is a pickled document associated with the property name passed
++        to L{xattrPropertyStore.get}, that value is unpickled into a
++        L{WebDAVDocument}, the root element of which is returned.
++        """
++        document = self._makeValue()
++        self._setValue(document, dumps(document))
++        self._checkValue(document)
++
++
++    def test_getUpgradeXML(self):
++        """
++        If the value associated with the property name passed to
++        L{xattrPropertyStore.get} is an uncompressed XML document, it is
++        upgraded on access by compressing it.
++        """
++        document = self._makeValue()
++        originalValue = document.toxml()
++        self._setValue(document, originalValue)
++        self._checkValue(document)
++        self.assertEquals(
++            decompress(self._getValue(document)), originalValue)
++
++
++    def test_getUpgradeCompressedPickle(self):
++        """
++        If the value associated with the property name passed to
++        L{xattrPropertyStore.get} is a compressed pickled document, it is
++        upgraded on access to the compressed XML format.
++        """
++        document = self._makeValue()
++        self._setValue(document, compress(dumps(document)))
++        self._checkValue(document)
++        self.assertEquals(
++            decompress(self._getValue(document)), document.toxml())
++
++
++    def test_getInvalid(self):
++        """
++        If the value associated with the property name passed to
++        L{xattrPropertyStore.get} cannot be interpreted, an error is logged and
++        L{HTTPError} is raised with the I{INTERNAL SERVER ERROR} response code.
++        """
++        document = self._makeValue()
++        self._setValue(
++            document,
++            "random garbage goes here! \0 that nul is definitely garbage")
++
++        property = document.root_element.qname()
++        error = self.assertRaises(HTTPError, self.propertyStore.get, property)
++        self.assertEquals(error.response.code, INTERNAL_SERVER_ERROR)
++        self.assertEquals(
++            len(self.flushLoggedErrors(UnpicklingError)), 1)
++
++
++    def test_set(self):
++        """
++        L{xattrPropertyStore.set} accepts a L{WebDAVElement} and stores a
++        compressed XML document representing it in an extended attribute.
++        """
++        document = self._makeValue()
++        self.propertyStore.set(document.root_element)
++        self.assertEquals(
++            decompress(self._getValue(document)), document.toxml())
++
++
++    def test_delete(self):
++        """
++        L{xattrPropertyStore.delete} deletes the named property.
++        """
++        document = self._makeValue()
++        self.propertyStore.set(document.root_element)
++        self.propertyStore.delete(document.root_element.qname())
++        self.assertRaises(KeyError, self._getValue, document)
++
++
++    def test_deleteNonExistent(self):
++        """
++        L{xattrPropertyStore.delete} does nothing if passed a property which
++        has no value.
++        """
++        document = self._makeValue()
++        self.propertyStore.delete(document.root_element.qname())
++        self.assertRaises(KeyError, self._getValue, document)
++
++
++    def test_deleteErrors(self):
++        """
++        If there is a problem deleting the specified property (aside from the
++        property not existing), L{xattrPropertyStore.delete} raises
++        L{HTTPError} with a status code which is determined by the nature of
++        the problem.
++        """
++        # Remove access from the directory containing the file so that deleting
++        # extended attributes of it fails with EPERM.
++        self.resourcePath.remove()
++
++        # Try to delete a property from it - and fail.
++        document = self._makeValue()
++        error = self.assertRaises(
++            HTTPError,
++            self.propertyStore.delete, document.root_element.qname())
++
++        # Make sure that the status is FORBIDDEN, a roughly reasonable mapping
++        # of the EPERM failure.
++        self.assertEquals(error.response.code, FORBIDDEN)
++
++
++    def test_contains(self):
++        """
++        L{xattrPropertyStore.contains} returns C{True} if the given property
++        has a value, C{False} otherwise.
++        """
++        document = self._makeValue()
++        self.assertFalse(
++            self.propertyStore.contains(document.root_element.qname()))
++        self._setValue(document, document.toxml())
++        self.assertTrue(
++            self.propertyStore.contains(document.root_element.qname()))

Modified: CalendarServer/branches/exarkun/update-twisted-3816/lib-patches/Twisted/twisted.web2.dav.xattrprops.patch
===================================================================
--- CalendarServer/branches/exarkun/update-twisted-3816/lib-patches/Twisted/twisted.web2.dav.xattrprops.patch	2009-03-16 19:45:17 UTC (rev 3871)
+++ CalendarServer/branches/exarkun/update-twisted-3816/lib-patches/Twisted/twisted.web2.dav.xattrprops.patch	2009-03-16 20:13:22 UTC (rev 3872)
@@ -1,72 +1,78 @@
 Index: twisted/web2/dav/xattrprops.py
 ===================================================================
---- twisted/web2/dav/xattrprops.py	(revision 19773)
+--- twisted/web2/dav/xattrprops.py	(revision 26389)
 +++ twisted/web2/dav/xattrprops.py	(working copy)
-@@ -33,15 +33,24 @@
- 
+@@ -34,21 +34,27 @@
  import urllib
  import sys
-+import zlib
-+from time import sleep
-+from random import random
-+from errno import EAGAIN
-+from zlib import compress, decompress
-+from cPickle import loads as unpickle, PicklingError, UnpicklingError
+ import zlib
++
++from operator import setitem
+ from time import sleep
+ from random import random
+ from errno import EAGAIN
+ from zlib import compress, decompress
++from cPickle import UnpicklingError, loads as unpickle
  
  import xattr
  
  if getattr(xattr, 'xattr', None) is None:
      raise ImportError("wrong xattr package imported")
  
-+from twisted.python import log
-+from twisted.python.failure import Failure
++from twisted.python.util import untilConcludes
+ from twisted.python import log
  from twisted.web2 import responsecode
  from twisted.web2.http import HTTPError, StatusResponse
  from twisted.web2.dav import davxml
 +from twisted.web2.dav.http import statusForFailure
  
++
  class xattrPropertyStore (object):
      """
-@@ -66,16 +75,8 @@
-         deadPropertyXattrPrefix = "user."
  
-     def _encode(clazz, name):
--        #
--        # FIXME: The xattr API in Mac OS 10.4.2 breaks if you have "/" in an
--        # attribute name (radar://4202440). We'll quote the strings to get rid
--        # of "/" characters for now.
--        #
--        result = list("{%s}%s" % name)
--        for i in range(len(result)):
--            c = result[i]
--            if c in "%/": result[i] = "%%%02X" % (ord(c),)
--        r = clazz.deadPropertyXattrPrefix + ''.join(result)
-+        result = urllib.quote("{%s}%s" % name, safe='{}:')
-+        r = clazz.deadPropertyXattrPrefix + result
-         return r
+@@ -94,59 +100,109 @@
+         self.attrs = xattr.xattr(self.resource.fp.path)
  
-     def _decode(clazz, name):
-@@ -97,19 +98,83 @@
- 
      def get(self, qname):
++        """
++        Retrieve the value of a property stored as an extended attribute on the
++        wrapped path.
++
++        @param qname: The property to retrieve as a two-tuple of namespace URI
++            and local name.
++
++        @raise HTTPError: If there is no value associated with the given
++            property.
++
++        @return: A L{WebDAVDocument} representing the value associated with the
++            given property.
++        """
          try:
--            value = self.attrs[self._encode(qname)]
-+            data = self.attrs[self._encode(qname)]
+             data = self.attrs[self._encode(qname)]
+-            try:
+-                value = decompress(data)
+-            except zlib.error:
+-                # Value is not compressed; data was stored by old
+-                # code.  This is easy to handle, so let's keep
+-                # compatibility here.
+-                value = data
+-            del data
          except KeyError:
              raise HTTPError(StatusResponse(
                  responsecode.NOT_FOUND,
                  "No such property: {%s}%s" % qname
              ))
-+        except Exception, e:
-+            raise HTTPError(StatusResponse(
-+                statusForFailure(Failure()),
-+                "Unable to read property: {%s}%s" % qname
-+            ))
++        except:
++            # XXX Untested, I can't get the getitem to actually fail in a test
++            # environment. -exarkun
++            raise HTTPError(
++                StatusResponse(
++                    statusForFailure(Failure()),
++                    "Unable to read property: " + key))
  
--        doc = davxml.WebDAVDocument.fromString(value)
 +        #
-+        # Serialize XML data to an xattr.  The storage format has
-+        # changed over time:
++        # Unserialize XML data from an xattr.  The storage format has changed
++        # over time:
 +        #
 +        #  1- Started with XML
 +        #  2- Started compressing the XML due to limits on xattr size
@@ -77,93 +83,85 @@
 +        # ones for compatibility.
 +        #
 +        legacy = False
- 
--        return doc.root_element
-+        try:
++
+         try:
+-            doc = davxml.WebDAVDocument.fromString(value)
 +            data = decompress(data)
 +        except zlib.error:
 +            legacy = True
  
+-            return doc.root_element
 +        try:
 +            doc = davxml.WebDAVDocument.fromString(data)
-+            result = doc.root_element
-+        except ValueError:
-+            legacy = True
-+
-+            try:
-+                result = unpickle(data)
+         except ValueError:
+-            msg = "Invalid property value stored on server: {%s}%s %s" % (qname[0], qname[1], value)
+-            log.err(msg)
+-            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, msg))
+-
+-    def set(self, property):
+-        for n in range(20):
+             try:
+-                self.attrs[self._encode(property.qname())] = compress(property.toxml())
+-            except IOError, error:
+-                if error.errno != EAGAIN:
+-                    raise
+-                sleep(random() / 10) # OMG Brutal Hax
++                doc = unpickle(data)
 +            except UnpicklingError:
-+                msg = ("Invalid property value stored on server: {%s}%s %s"
-+                       % (qname[0], qname[1], data))
-+                log.err(msg)
-+                raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, msg))
-+
++                format = "Invalid property value stored on server: {%s}%s %s"
++                msg = format % (qname[0], qname[1], data)
++                log.err(None, msg)
++                raise HTTPError(
++                    StatusResponse(responsecode.INTERNAL_SERVER_ERROR, msg))
+             else:
+-                break
+-    
++                legacy = True
+ 
 +        if legacy:
-+            # Try to upgrade the property to the current format
-+            try:
-+                log.msg("Upgrading property %r on resource %r"
-+                        % (qname, self.resource.fp.path))
-+                self.set(result)
-+            except Exception, e:
-+                #
-+                # Hrm, that failed.  No need to re-raise here, since
-+                # we can do what we were asked to do, but let's
-+                # complain about it.
-+                #
-+                log.err("Error while upgrading property %r on resource %r: %s"
-+                        % (qname, self.resource.fp.path, e))
++            self.set(doc.root_element)
 +
-+        return result
++        return doc.root_element
 +
-     def set(self, property):
--        self.attrs[self._encode(property.qname())] = property.toxml()
-+        for n in range(20):
-+            data = compress(property.toxml())
-+            try:
-+                self.attrs[self._encode(property.qname())] = data
-+            except Exception, e:
-+                if e.errno == EAGAIN and n < 19:
-+                    sleep(random() / 10) # OMG Brutal Hax
-+                else:
-+                    raise HTTPError(StatusResponse(
-+                        statusForFailure(Failure()),
-+                        "Unable to write property: %s" % (property.sname(),)
-+                    ))
-+            else:
-+                break
- 
++
++    def set(self, property):
++        key = self._encode(property.qname())
++        value = compress(property.toxml())
++        untilConcludes(setitem, self.attrs, key, value)
++
          # Update the resource because we've modified it
          self.resource.fp.restat()
-@@ -121,15 +186,31 @@
+ 
+     def delete(self, qname):
++        key = self._encode(qname)
+         try:
+-            del(self.attrs[self._encode(qname)])
+-        except KeyError:
++            del self.attrs[key]
++        except KeyError, e:
              # RFC 2518 Section 12.13.1 says that removal of
              # non-existing property is not an error.
              pass
-+        except Exception, e:
-+            raise HTTPError(StatusResponse(
-+                statusForFailure(Failure()),
-+                "Unable to delete property: {%s}%s" % qname
-+            ))
++        except:
++            # XXX Untested, I can't get the delitem to actually fail in a test
++            # environment. -exarkun
++            raise HTTPError(
++                StatusResponse(
++                    statusForFailure(Failure()),
++                    "Unable to delete property: " + key))
  
      def contains(self, qname):
          try:
              return self._encode(qname) in self.attrs
          except TypeError:
              return False
-+        except Exception, e:
-+            raise HTTPError(StatusResponse(
-+                statusForFailure(Failure()),
-+                "Unable to read properties"
-+            ))
++        except:
++            # XXX Untested, I can't get the __contains__ to actually fail in a
++            # test environment. -exarkun
++            raise HTTPError(
++                StatusResponse(
++                    statusForFailure(Failure()),
++                    "Unable to read property: " + key))
  
      def list(self):
          prefix     = self.deadPropertyXattrPrefix
-         prefix_len = len(prefix)
- 
--        return [ self._decode(name) for name in self.attrs if name.startswith(prefix) ]
-+        try:
-+            return [ self._decode(name) for name in self.attrs if name.startswith(prefix) ]
-+        except Exception, e:
-+            raise HTTPError(StatusResponse(
-+                statusForFailure(Failure()),
-+                "Unable to list properties"
-+            ))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090316/90d54e55/attachment-0001.html>


More information about the calendarserver-changes mailing list