[CalendarServer-changes] [8127] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Wed Sep 28 07:50:46 PDT 2011


Revision: 8127
          http://trac.macosforge.org/projects/calendarserver/changeset/8127
Author:   cdaboo at apple.com
Date:     2011-09-28 07:50:44 -0700 (Wed, 28 Sep 2011)
Log Message:
-----------
Do proper SEQUENCE processing to cope without of order iTIP we might get from external servers.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
    CalendarServer/trunk/twistedcaldav/test/test_icalendar.py

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2011-09-27 23:52:37 UTC (rev 8126)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2011-09-28 14:50:44 UTC (rev 8127)
@@ -526,6 +526,7 @@
     def overriddenComponent(self, recurrence_id):
         """
         Return the overridden iCal component in this calendar matching the supplied RECURRENCE-ID property.
+        This also returns the matching master component if recurrence_id is C{None}.
 
         @param recurrence_id: The RECURRENCE-ID property value to match.
         @type recurrence_id: L{PyCalendarDateTime}
@@ -729,7 +730,7 @@
         """
         Return the trigger information for the specified alarm component.
         @param component: the Component whose start should be returned.
-        @return: ta tuple consisting of:
+        @return: a tuple consisting of:
             trigger : the 'native' trigger value
             related : either True (for START) or False (for END)
             repeat : an integer for the REPEAT count
@@ -1890,7 +1891,7 @@
         @param from_calendar: the old calendar to copy from
         @type from_calendar: L{Component}
         @param properties: the property names to copy over
-        @type properties: C{typle} or C{list}
+        @type properties: C{tuple} or C{list}
         """
 
         assert from_calendar.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
@@ -1947,7 +1948,7 @@
                 if not found_all_attendees:
                     removed_master = True
                 
-        # Now remove the unwanted components - but we may need to exdate the master
+        # Now remove the unwanted components - but we may need to EXDATE the master
         exdates = []
         for component in remove_components:
             rid = component.getRecurrenceIDUTC()
@@ -2085,6 +2086,129 @@
                 for param, value in paramvalues:
                     prop.removeParameterValue(param, value)
 
+    def getITIPInfo(self):
+        """
+        Get property value details needed to synchronize iTIP components.
+        
+        @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+        """
+        try:
+            # Extract items from component
+            uid = self.propertyValue("UID")
+            seq = self.propertyValue("SEQUENCE")
+            if seq:
+                seq = int(seq)
+            dtstamp = self.propertyValue("DTSTAMP")
+            rid = self.propertyValue("RECURRENCE-ID")
+            
+        except ValueError:
+            return (None, None, None, None)
+        
+        return (uid, seq, dtstamp, rid)
+
+    @staticmethod
+    def compareComponentsForITIP(component1, component2, use_dtstamp=True):
+        """
+        Compare synchronization information for two components to see if they match according to iTIP.
+    
+        @param component1: first component to check.
+        @type component1: L{Component}
+        @param component2: second component to check.
+        @type component2: L{Component}
+        @param use_dtstamp: whether DTSTAMP is used in addition to SEQUENCE.
+        @type component2: C{bool}
+        
+        @return: 0, 1, -1 as per compareSyncInfo.
+        """
+        info1 = (None,) + Component.getITIPInfo(component1)
+        info2 = (None,) + Component.getITIPInfo(component2)
+        return Component.compareITIPInfo(info1, info2, use_dtstamp)
+    
+    @staticmethod
+    def compareITIPInfo(info1, info2, use_dtstamp=True):
+        """
+        Compare two synchronization information records.
+        
+        @param info1: a C{tuple} as returned by L{getSyncInfo}.
+        @param info2: a C{tuple} as returned by L{getSyncInfo}.
+        @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
+        """
+        
+        _ignore_name1, uid1, seq1, dtstamp1, _ignore_rid1 = info1
+        _ignore_name2, uid2, seq2, dtstamp2, _ignore_rid2 = info2
+        
+        # UIDs MUST match
+        assert uid1 == uid2
+        
+        # Look for sequence
+        if (seq1 is not None) and (seq2 is not None):
+            if seq1 > seq2:
+                return 1
+            if seq1 < seq2:
+                return -1
+        elif (seq1 is not None) and (seq2 is None):
+            return 1
+        elif (seq1 is None) and (seq2 is not None):
+            return -1
+    
+        # Look for DTSTAMP
+        if use_dtstamp:
+            if (dtstamp1 is not None) and (dtstamp2 is not None):
+                if dtstamp1 > dtstamp2:
+                    return 1
+                if dtstamp1 < dtstamp2:
+                    return -1
+            elif (dtstamp1 is not None) and (dtstamp2 is None):
+                return 1
+            elif (dtstamp1 is None) and (dtstamp2 is not None):
+                return -1
+    
+        return 0
+
+    def needsiTIPSequenceChange(self, oldcalendar):
+        """
+        Compare this calendar with the old one and indicate whether the current one has SEQUENCE
+        that is always greater than the old.
+        """
+        
+        for component in self.subcomponents():
+            if component.name() in ignoredComponents:
+                continue
+            oldcomponent = oldcalendar.overriddenComponent(component.getRecurrenceIDUTC())
+            if oldcomponent is None:
+                oldcomponent = oldcalendar.masterComponent()
+                if oldcomponent is None:
+                    continue
+            newseq = component.propertyValue("SEQUENCE")
+            if newseq is None:
+                newseq = 0
+            oldseq = oldcomponent.propertyValue("SEQUENCE")
+            if oldseq is None:
+                oldseq = 0
+            if newseq <= oldseq:
+                return True
+
+        return False
+
+    def bumpiTIPInfo(self, oldcalendar=None, doSequence=False):
+        """
+        Change DTSTAMP and optionally SEQUENCE on all components.
+        """
+        
+        if doSequence:
+            
+            def maxSequence(calendar):
+                seqs = calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1)
+                return max(seqs, key=lambda x:x.value()).value() if seqs else 0
+
+            # Determine value to bump to from old calendar (if exists) or self
+            newseq = maxSequence(oldcalendar if oldcalendar is not None else self) + 1                
+                
+            # Bump all components
+            self.replacePropertyInAllComponents(Property("SEQUENCE", newseq))
+        
+        self.replacePropertyInAllComponents(Property("DTSTAMP", PyCalendarDateTime.getNowUTC()))
+            
     def normalizeAll(self):
         
         # Normalize all properties

Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2011-09-27 23:52:37 UTC (rev 8126)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2011-09-28 14:50:44 UTC (rev 8127)
@@ -722,7 +722,6 @@
                 "DTSTAMP",
                 "CREATED",
                 "LAST-MODIFIED",
-                "SEQUENCE",
                 "X-CALENDARSERVER-PRIVATE-COMMENT",
             ):
                 continue

Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2011-09-27 23:52:37 UTC (rev 8126)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2011-09-28 14:50:44 UTC (rev 8127)
@@ -475,6 +475,8 @@
         self.cancelledAttendees = ()
         self.reinvites = None
         self.needs_action_rids = None
+        
+        self.needs_sequence_change = False
 
         # Check for a delete
         if self.action == "remove":
@@ -484,6 +486,9 @@
 
             # Cancel all attendees
             self.cancelledAttendees = [(attendee, None) for attendee in self.attendees]
+            
+            # CANCEL always bumps sequence
+            self.needs_sequence_change = True
 
         # Check for a new resource or an update
         elif self.action == "modify":
@@ -524,6 +529,10 @@
                 # Check for removed attendees
                 if not recurrence_reschedule:
                     self.findRemovedAttendees()
+                    
+                # For now we always bump the sequence number on modifications because we cannot track DTSTAMP on
+                # the Attendee side. But we check the old and the new and only bump if the client did not already do it.
+                self.needs_sequence_change = self.calendar.needsiTIPSequenceChange(self.oldcalendar)
 
         elif self.action == "create":
             log.debug("Implicit - organizer '%s' is creating UID: '%s'" % (self.organizer, self.uid))
@@ -534,6 +543,9 @@
             if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION").upper() == "NEEDS-ACTION":
                 attendee.setParameter("RSVP", "TRUE")
 
+        if self.needs_sequence_change:
+            self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
+
         yield self.scheduleWithAttendees()
         
         # Always clear SCHEDULE-FORCE-SEND from all attendees after scheduling

Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2011-09-27 23:52:37 UTC (rev 8126)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2011-09-28 14:50:44 UTC (rev 8127)
@@ -15,7 +15,7 @@
 ##
 
 """
-iTIP (RFC2446) processing.
+iTIP (RFC5546) processing.
 """
 
 #
@@ -33,7 +33,8 @@
 from twext.python.log import Logger
 
 from twistedcaldav.config import config
-from twistedcaldav.ical import Property, iCalendarProductID, Component
+from twistedcaldav.ical import Property, iCalendarProductID, Component,\
+    ignoredComponents
 
 from pycalendar.datetime import PyCalendarDateTime
 
@@ -83,10 +84,14 @@
         
         @return: a C{tuple} of:
             calendar object ready to save, or C{None} (request should be ignored)
-            a C{set} of iCalendar properties that changed, or C{None},
             a C{set} of recurrences that changed, or C{None}
         """
         
+        # Check sequencing
+        if not iTipProcessing.sequenceComparison(itip_message, calendar):
+            # Ignore out of sequence message
+            return None, None
+
         # Merge Organizer data with Attendee's own changes (VALARMs, Comment only for now).
         from twistedcaldav.scheduling.icaldiff import iCalDiff
         rids = iCalDiff(calendar, itip_message, False).whatIsDifferent()
@@ -189,6 +194,11 @@
         assert itip_message.propertyValue("METHOD") == "CANCEL", "iTIP message must have METHOD:CANCEL"
         assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
 
+        # Check sequencing
+        if not iTipProcessing.sequenceComparison(itip_message, calendar):
+            # Ignore out of sequence message
+            return False, False, None
+
         # Check to see if this is a cancel of the entire event
         if itip_message.masterComponent() is not None:
             if autoprocessing:
@@ -263,6 +273,8 @@
         Process a METHOD=REPLY.
         
         TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
+        TODO: We have no way to track SEQUENCE/DTSTAMP on a per-attendee basis to correctly serialize out-of-order
+              replies.
         
         @param itip_message: the iTIP message calendar object to process.
         @type itip_message:
@@ -511,10 +523,93 @@
                     if component.name() == "VEVENT":
                         component.replaceProperty(Property("TRANSP", "TRANSPARENT"))
 
+    @staticmethod
+    def sequenceComparison(itip, calendar):
+        """
+        Do appropriate itip message sequencing based by comparison with existing calendar data.
+        
+        @return: C{True} if the itip message is new and should be processed, C{False}
+            if no processing is needed
+        @rtype: C{bool}
+        """
+        
+        # Master component comparison trumps all else
+        itip_master = itip.masterComponent()
+        cal_master = calendar.masterComponent()
+        
+        # If master component exists, compare all in iTIP and update if any are new
+        if cal_master:
+            for itip_component in itip.subcomponents():
+                if itip_component.name() in ignoredComponents:
+                    continue
+                cal_component = calendar.overriddenComponent(itip_component.getRecurrenceIDUTC())
+                if cal_component is None:
+                    cal_component = cal_master
+                    
+                # TODO: No DTSTAMP comparison because we do not track DTSTAMPs
+                # Treat components the same as meaning so an update - in theory no harm in doing that
+                if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0:
+                    return True
+
+            return False
+
+        elif itip_master:
+            
+            # Do comparison of each appropriate component if any one is new, process the itip
+            for cal_component in calendar.subcomponents():
+                if cal_component.name() in ignoredComponents:
+                    continue
+                itip_component = itip.overriddenComponent(cal_component.getRecurrenceIDUTC())
+                if itip_component is None:
+                    itip_component = itip_master
+
+                # TODO: No DTSTAMP comparison because we do not track DTSTAMPs
+                # Treat components the same as meaning so an update - in theory no harm in doing that
+                if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0:
+                    return True
+            
+            return False
+    
+        else:
+            # Do comparison of each matching component if any one is new, process the entire itip.
+            # There is a race condition here, similar to REPLY, where we could reinstate an instance
+            # that has been removed. Not much we can do about it without additional tracking.
+            
+            cal_rids = set()
+            for cal_component in calendar.subcomponents():
+                if cal_component.name() in ignoredComponents:
+                    continue
+                cal_rids.add(cal_component.getRecurrenceIDUTC())
+            itip_rids = set()
+            for itip_component in itip.subcomponents():
+                if itip_component.name() in ignoredComponents:
+                    continue
+                itip_rids.add(itip_component.getRecurrenceIDUTC())
+            
+            # Compare ones that match
+            for rid in cal_rids & itip_rids:
+                cal_component = calendar.overriddenComponent(rid)
+                itip_component = itip.overriddenComponent(rid)
+
+                # TODO: No DTSTAMP comparison because we do not track DTSTAMPs
+                # Treat components the same as meaning so an update - in theory no harm in doing that
+                if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0:
+                    return True
+            
+            # If there are others in one set and not the other - always process, else no process
+            return len(cal_rids ^ itip_rids) > 0
+            
 class iTipGenerator(object):
+    """
+    This assumes that DTSTAMP and SEQUENCE are already at their new values in the original calendar
+    data passed in to each generateXXX() call.
+    """
     
     @staticmethod
     def generateCancel(original, attendees, instances=None, full_cancel=False):
+        """
+        This assumes that SEQUENCE is already at its new value in the original calendar data.
+        """
         
         itip = Component("VCALENDAR")
         itip.addProperty(Property("VERSION", "2.0"))
@@ -540,18 +635,16 @@
             assert instance is not None, "Need a master component"
 
             # Add some required properties extracted from the original
-            comp.addProperty(Property("DTSTAMP", PyCalendarDateTime.getNowUTC()))
+            comp.addProperty(Property("DTSTAMP", instance.propertyValue("DTSTAMP")))
             comp.addProperty(Property("UID", instance.propertyValue("UID")))
-            seq = instance.propertyValue("SEQUENCE")
-            seq = int(seq) + 1 if seq else 1
-            comp.addProperty(Property("SEQUENCE", seq))
+            comp.addProperty(Property("SEQUENCE", instance.propertyValue("SEQUENCE") if instance.hasProperty("SEQUENCE") else 0))
             comp.addProperty(instance.getOrganizerProperty())
             if instance_rid:
                 comp.addProperty(Property("RECURRENCE-ID", instance_rid.duplicate().adjustToUTC()))
             
             def addProperties(propname):
-                for property in instance.properties(propname):
-                    comp.addProperty(property)
+                for icalproperty in instance.properties(propname):
+                    comp.addProperty(icalproperty)
                     
             addProperties("SUMMARY")
             addProperties("DTSTART")
@@ -589,24 +682,22 @@
 
     @staticmethod
     def generateAttendeeRequest(original, attendees, filter_rids):
-
+        """
+        This assumes that SEQUENCE is already at its new value in the original calendar data.
+        """
+        
         # Start with a copy of the original as we may have to modify bits of it
         itip = original.duplicate()
         itip.replaceProperty(Property("PRODID", iCalendarProductID))
         itip.addProperty(Property("METHOD", "REQUEST"))
         
-        # Force update to DTSTAMP everywhere
-        itip.replacePropertyInAllComponents(Property("DTSTAMP", PyCalendarDateTime.getNowUTC()))
-
         # Now filter out components that do not contain every attendee
         itip.attendeesView(attendees, onlyScheduleAgentServer=True)
         
         # Now filter out components except the ones specified
         if itip.filterComponents(filter_rids):
-
             # Strip out unwanted bits
             iTipGenerator.prepareSchedulingMessage(itip)
-    
             return itip
         
         else:
@@ -623,7 +714,7 @@
         # Now filter out components except the ones specified
         itip.filterComponents(changedRids)
 
-        # Force update to DTSTAMP everywhere
+        # Force update to DTSTAMP everywhere so reply sequencing will work
         itip.replacePropertyInAllComponents(Property("DTSTAMP", PyCalendarDateTime.getNowUTC()))
 
         # Remove all attendees except the one we want

Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py	2011-09-27 23:52:37 UTC (rev 8126)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py	2011-09-28 14:50:44 UTC (rev 8127)
@@ -18,8 +18,8 @@
 from pycalendar.timezone import PyCalendarTimezone
 from twistedcaldav.ical import Component
 from twistedcaldav.scheduling.itip import iTipProcessing, iTipGenerator
+import twistedcaldav.test.util
 import os
-import twistedcaldav.test.util
 
 class iTIPProcessing (twistedcaldav.test.util.TestCase):
     """
@@ -899,6 +899,501 @@
                     msg=description
                 )
 
+    def test_sequenceComparison(self):
+        """
+        Test iTIPProcessing.sequenceComparison
+        """
+        
+        data = (
+            (
+                "1.1 Simple Update - SEQUENCE change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "1.2 Simple Update - DTSTAMP change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T010000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "1.3 Simple Update - no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "2.1 Recurrence add changed SEQUENCE instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T010000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "2.2 Recurrence add changed DTSTAMP instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "2.3 Recurrence add unchanged instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "3.1 Recurrence master/no-master changed SEQUENCE instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:CANCEL
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "3.2 Recurrence master/no-master old SEQUENCE instance no prior instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:CANCEL
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "3.3 Recurrence master/no-master old SEQUENCE instance with prior instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+SEQUENCE:2
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:CANCEL
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "4.1 Recurrence no-master/master changed SEQUENCE master",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHID:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "4.2 Recurrence no-master/master changed DTSTAMP master",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHID:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T010000Z
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "4.3 Recurrence no-master/master old SEQUENCE instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHID:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "4.4 Recurrence no-master/master changed SEQUENCE instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHID:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "5.1 Recurrence no-masters changed SEQUENCE same instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHID:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "5.2 Recurrence no-masters changed DTSTAMP same instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHID:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T020000Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "5.3 Recurrence no-masters changed SEQUENCE different instances",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHID:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071117T000000Z
+DTSTART:20071117T000000Z
+DTSTAMP:20071114T010000Z
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+        )
+        
+        for title, calendar_txt, itip_txt, expected in data:
+            calendar = Component.fromString(calendar_txt)
+            itip = Component.fromString(itip_txt)
+
+            result = iTipProcessing.sequenceComparison(itip, calendar)
+            self.assertEqual(result, expected, msg="Result mismatch: %s" % (title,))
+            
+
 class iTIPGenerator (twistedcaldav.test.util.TestCase):
     """
     iCalendar support tests
@@ -962,6 +1457,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """,
@@ -974,6 +1470,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1229,6 +1726,7 @@
 ATTENDEE:mailto:user2 at example.com
 ATTENDEE:mailto:user3 at example.com
 ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1260,7 +1758,7 @@
 ATTENDEE:mailto:user2 at example.com
 ATTENDEE:mailto:user3 at example.com
 ORGANIZER:mailto:user1 at example.com
-SEQUENCE:1
+SEQUENCE:2
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1293,6 +1791,7 @@
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1325,6 +1824,7 @@
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
+SEQUENCE:1
 END:VEVENT
 BEGIN:VEVENT
 UID:12345-67890-4
@@ -1332,6 +1832,7 @@
 DTSTART:20081114T010000Z
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1364,6 +1865,7 @@
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
+SEQUENCE:1
 END:VEVENT
 BEGIN:VEVENT
 UID:12345-67890-5
@@ -1371,6 +1873,7 @@
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """,

Modified: CalendarServer/trunk/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_icalendar.py	2011-09-27 23:52:37 UTC (rev 8126)
+++ CalendarServer/trunk/twistedcaldav/test/test_icalendar.py	2011-09-28 14:50:44 UTC (rev 8127)
@@ -4564,3 +4564,743 @@
             calendar = Component.fromString(text)
             for rid, result in results:
                 self.assertEqual(calendar.perUserTransparency(rid), result, "Failed comparison: %s %s" % (title, rid,))
+
+    def test_needsiTIPSequenceChange(self):
+
+        data = (
+            (
+                "Simple old < new",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Simple old == new",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Simple old > new",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:2
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Recurring same instances all old < new",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:2
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Recurring same instances some old == new",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:2
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:2
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Recurring derived instance all old < new",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:2
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Recurring derived instance some old == new",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SEQUENCE:2
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SEQUENCE:2
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SEQUENCE:2
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+        )
+
+        for title, old_txt, new_txt, result in data:
+            ical_old = Component.fromString(old_txt)
+            ical_new = Component.fromString(new_txt)
+            self.assertEqual(ical_new.needsiTIPSequenceChange(ical_old), result, "Failed: %s" % (title,))
+
+    def test_bumpiTIPInfo(self):
+
+        data = (
+            (
+                "Simple no sequence, no sequence change",
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Simple sequence, no sequence change",
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Simple no sequence, sequence change",
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Simple sequence, sequence change",
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Simple sequence, sequence change, old calendar",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:3
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Recurring override no sequence, no sequence change",
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Recurring override vary sequence, no sequence change",
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Recurring override no sequence, sequence change",
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Recurring override vary sequence, sequence change",
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+SEQUENCE:3
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:3
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Recurring override vary sequence, sequence change, old calendar",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:3
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+SEQUENCE:4
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+SEQUENCE:4
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+        )
+        
+        for title, old_txt, ical_txt, result_txt, doSequence in data:
+            old = Component.fromString(old_txt) if old_txt else None
+            ical = Component.fromString(ical_txt)
+            result = Component.fromString(result_txt)
+            ical.bumpiTIPInfo(oldcalendar=old, doSequence=doSequence)
+            
+            ical1 = str(ical).split("\n")
+            ical2 = str(result).split("\n")
+            
+            # Check without DTSTAMPs which we expect to be different
+            ical1_withoutDTSTAMP = [item for item in ical1 if not item.startswith("DTSTAMP:")]
+            ical2_withoutDTSTAMP = [item for item in ical2 if not item.startswith("DTSTAMP:")]
+
+            diff = "\n".join(unified_diff(ical1_withoutDTSTAMP, ical2_withoutDTSTAMP))
+            self.assertEqual("\n".join(ical1_withoutDTSTAMP), "\n".join(ical2_withoutDTSTAMP), "Failed comparison: %s\n%s" % (title, diff,))
+
+            # Check that all DTSTAMPs changed    
+            dtstamps1 = set([item for item in ical1 if item.startswith("DTSTAMP:")])
+            dtstamps2 = set([item for item in ical2 if item.startswith("DTSTAMP:")])
+            
+            diff = "\n".join(unified_diff(ical1, ical2))
+            self.assertEqual(len(dtstamps1 & dtstamps2), 0, "Failed comparison: %s\n%s" % (title, diff,))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110928/6658175c/attachment-0001.html>


More information about the calendarserver-changes mailing list