Revision: 9342 http://trac.macosforge.org/projects/calendarserver/changeset/9342 Author: cdaboo@apple.com Date: 2012-06-07 18:11:47 -0700 (Thu, 07 Jun 2012) Log Message: ----------- Fixes to improve performance by doing less work during per user data filtering. Modified Paths: -------------- CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py CalendarServer/trunk/twistedcaldav/ical.py CalendarServer/trunk/twistedcaldav/test/test_icalendar.py Modified: CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py =================================================================== --- CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py 2012-06-08 01:10:12 UTC (rev 9341) +++ CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py 2012-06-08 01:11:47 UTC (rev 9342) @@ -210,6 +210,14 @@ ical.addProperty(property) def _splitPerUserData(self, ical): + """ + Split the per-user data out of the "normal" iCalendar components into separate per-user + components. Along the way keep the iCalendar representation in a "minimal" state by eliminating + any components that are the same as the master derived component. + + @param ical: calendar data to process + @type ical: L{Component} + """ def init_peruser_component(): peruser = Component(PerUserDataFilter.PERUSER_COMPONENT) @@ -229,8 +237,6 @@ def init_perinstance_component(): peruser = Component(PerUserDataFilter.PERINSTANCE_COMPONENT) rid = component.getRecurrenceIDUTC() - if rid: - peruser.addProperty(Property("RECURRENCE-ID", rid)) perinstance_components[rid] = peruser return peruser @@ -253,15 +259,13 @@ if self.uid: # Add unique per-instance components into the per-user component master_perinstance = perinstance_components.get(None) - master_perinstance_txt = str(master_perinstance) if master_perinstance: peruser_component.addComponent(master_perinstance) for rid, perinstance in perinstance_components.iteritems(): if rid is None: continue - perinstance_txt = str(perinstance) - perinstance_txt = "".join([line for line in perinstance_txt.splitlines(True) if not line.startswith("RECURRENCE-ID:")]) - if master_perinstance is None or perinstance_txt != master_perinstance_txt: + if master_perinstance is None or perinstance != master_perinstance: + perinstance.addProperty(Property("RECURRENCE-ID", rid)) peruser_component.addComponent(perinstance) self._compactInstances(ical) @@ -280,16 +284,17 @@ if master is None: return + masterDerived = ical.masterDerived() + for subcomponent in tuple(ical.subcomponents()): if subcomponent.name() == "VTIMEZONE" or subcomponent.name().startswith("X-"): continue rid = subcomponent.getRecurrenceIDUTC() if rid is None: continue - derived = ical.deriveInstance(rid) - if derived: - if str(derived) == str(subcomponent): - ical.removeComponent(subcomponent) + derived = ical.deriveInstance(rid, newcomp=masterDerived) + if derived and derived == subcomponent: + ical.removeComponent(subcomponent) def _mergeRepresentations(self, icalnew, icalold): Modified: CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py =================================================================== --- CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py 2012-06-08 01:10:12 UTC (rev 9341) +++ CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py 2012-06-08 01:11:47 UTC (rev 9342) @@ -5974,3 +5974,431 @@ for newitem in (newdata, Component.fromString(newdata),): self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01) + +class PerUserDataMergeTestCompact (twistedcaldav.test.util.TestCase): + + def test_merge_vevent_compact(self): + + newdata = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART:20080601T110000Z +DTEND:20080601T120000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VEVENT +BEGIN:VEVENT +UID:12345-67890 +RECURRENCE-ID:20080602T110000Z +DTSTART:20080602T110000Z +DTEND:20080602T120000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT5M +END:VALARM +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") + result = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART:20080601T110000Z +DTEND:20080601T120000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +END:VEVENT +BEGIN:X-CALENDARSERVER-PERUSER +UID:12345-67890 +X-CALENDARSERVER-PERUSER-UID:user01 +BEGIN:X-CALENDARSERVER-PERINSTANCE +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:X-CALENDARSERVER-PERINSTANCE +BEGIN:X-CALENDARSERVER-PERINSTANCE +RECURRENCE-ID:20080602T110000Z +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT5M +END:VALARM +END:X-CALENDARSERVER-PERINSTANCE +END:X-CALENDARSERVER-PERUSER +END:VCALENDAR +""".replace("\n", "\r\n") + + filtered = PerUserDataFilter("user01").merge(newdata, None) + self.assertEqual(str(filtered), result) + unfiltered = PerUserDataFilter("user01").filter(filtered) + self.assertEqual(str(unfiltered), newdata) + + + def test_merge_vevent_all_day_compact(self): + + newdata = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART;VALUE=DATE:20080601 +DTEND;VALUE=DATE:20080602 +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VEVENT +BEGIN:VEVENT +UID:12345-67890 +RECURRENCE-ID;VALUE=DATE:20080602 +DTSTART;VALUE=DATE:20080602 +DTEND;VALUE=DATE:20080603 +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT5M +END:VALARM +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") + result = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART;VALUE=DATE:20080601 +DTEND;VALUE=DATE:20080602 +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +END:VEVENT +BEGIN:X-CALENDARSERVER-PERUSER +UID:12345-67890 +X-CALENDARSERVER-PERUSER-UID:user01 +BEGIN:X-CALENDARSERVER-PERINSTANCE +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:X-CALENDARSERVER-PERINSTANCE +BEGIN:X-CALENDARSERVER-PERINSTANCE +RECURRENCE-ID;VALUE=DATE:20080602 +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT5M +END:VALARM +END:X-CALENDARSERVER-PERINSTANCE +END:X-CALENDARSERVER-PERUSER +END:VCALENDAR +""".replace("\n", "\r\n") + + filtered = PerUserDataFilter("user01").merge(newdata, None) + self.assertEqual(str(filtered), result) + unfiltered = PerUserDataFilter("user01").filter(filtered) + self.assertEqual(str(unfiltered), newdata) + + + def test_merge_peruser_compact(self): + + newdata = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART:20080601T110000Z +DTEND:20080601T120000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VEVENT +BEGIN:VEVENT +UID:12345-67890 +RECURRENCE-ID:20080602T110000Z +DTSTART:20080602T120000Z +DTEND:20080602T130000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") + result = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART:20080601T110000Z +DTEND:20080601T120000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +END:VEVENT +BEGIN:VEVENT +UID:12345-67890 +RECURRENCE-ID:20080602T110000Z +DTSTART:20080602T120000Z +DTEND:20080602T130000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +END:VEVENT +BEGIN:X-CALENDARSERVER-PERUSER +UID:12345-67890 +X-CALENDARSERVER-PERUSER-UID:user01 +BEGIN:X-CALENDARSERVER-PERINSTANCE +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:X-CALENDARSERVER-PERINSTANCE +END:X-CALENDARSERVER-PERUSER +END:VCALENDAR +""".replace("\n", "\r\n") + + filtered = PerUserDataFilter("user01").merge(newdata, None) + self.assertEqual(str(filtered), result) + unfiltered = PerUserDataFilter("user01").filter(filtered) + self.assertEqual(str(unfiltered), newdata) + + + def test_merge_peruser_all_day_compact(self): + + newdata = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART;VALUE=DATE:20080601 +DTEND;VALUE=DATE:20080602 +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VEVENT +BEGIN:VEVENT +UID:12345-67890 +RECURRENCE-ID;VALUE=DATE:20080602 +DTSTART;VALUE=DATE:20080602 +DTEND;VALUE=DATE:20080603 +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +SUMMARY:Test +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") + result = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART;VALUE=DATE:20080601 +DTEND;VALUE=DATE:20080602 +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +END:VEVENT +BEGIN:VEVENT +UID:12345-67890 +RECURRENCE-ID;VALUE=DATE:20080602 +DTSTART;VALUE=DATE:20080602 +DTEND;VALUE=DATE:20080603 +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +SUMMARY:Test +END:VEVENT +BEGIN:X-CALENDARSERVER-PERUSER +UID:12345-67890 +X-CALENDARSERVER-PERUSER-UID:user01 +BEGIN:X-CALENDARSERVER-PERINSTANCE +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:X-CALENDARSERVER-PERINSTANCE +END:X-CALENDARSERVER-PERUSER +END:VCALENDAR +""".replace("\n", "\r\n") + + filtered = PerUserDataFilter("user01").merge(newdata, None) + self.assertEqual(str(filtered), result) + unfiltered = PerUserDataFilter("user01").filter(filtered) + self.assertEqual(str(unfiltered), newdata) + + + def test_merge_both_compact(self): + + newdata = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART:20080601T110000Z +DTEND:20080601T120000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VEVENT +BEGIN:VEVENT +UID:12345-67890 +RECURRENCE-ID:20080602T110000Z +DTSTART:20080602T110000Z +DTEND:20080602T120000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") + result = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART:20080601T110000Z +DTEND:20080601T120000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +END:VEVENT +BEGIN:X-CALENDARSERVER-PERUSER +UID:12345-67890 +X-CALENDARSERVER-PERUSER-UID:user01 +BEGIN:X-CALENDARSERVER-PERINSTANCE +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:X-CALENDARSERVER-PERINSTANCE +END:X-CALENDARSERVER-PERUSER +END:VCALENDAR +""".replace("\n", "\r\n") + + unfiltered_result = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890 +DTSTART:20080601T110000Z +DTEND:20080601T120000Z +ATTENDEE:mailto:user1@example.com +ATTENDEE:mailto:user2@example.com +DTSTAMP:20080601T120000Z +ORGANIZER;CN=User 01:mailto:user1@example.com +RRULE:FREQ=DAILY;COUNT=5 +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Test-1mod +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") + + filtered = PerUserDataFilter("user01").merge(newdata, None) + self.assertEqual(str(filtered), result) + unfiltered = PerUserDataFilter("user01").filter(filtered) + self.assertEqual(str(unfiltered), unfiltered_result) Modified: CalendarServer/trunk/twistedcaldav/ical.py =================================================================== --- CalendarServer/trunk/twistedcaldav/ical.py 2012-06-08 01:10:12 UTC (rev 9341) +++ CalendarServer/trunk/twistedcaldav/ical.py 2012-06-08 01:11:47 UTC (rev 9342) @@ -1139,7 +1139,7 @@ return True return False - def deriveInstance(self, rid, allowCancelled=False): + def deriveInstance(self, rid, allowCancelled=False, newcomp=None): """ Derive an instance from the master component that has the provided RECURRENCE-ID, but with all other properties, components etc from the master. If the requested override is @@ -1154,6 +1154,9 @@ @return: L{Component} for newly derived instance, or None if not valid override """ + if allowCancelled and newcomp is not None: + raise ValueError("Cannot re-use master component with allowCancelled") + # Must have a master component master = self.masterComponent() if master is None: @@ -1188,22 +1191,17 @@ rrules = master.properties("RRULE") if len(tuple(rrules)): instances = self.cacheExpandedTimeRanges(rid) - rids = set([instances[key].rid for key in instances]) instance_rid = normalizeForIndex(rid) - if instance_rid not in rids: + if str(instance_rid) not in instances.instances: # No match to a valid RRULE instance return None else: # No RRULE and no match to an RDATE => error return None - # Create the derived instance - newcomp = master.duplicate() - - # Strip out unwanted recurrence properties - for property in tuple(newcomp.properties()): - if property.name() in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",): - newcomp.removeProperty(property) + # If we were fed an already derived component, use that, otherwise make a new one + if newcomp is None: + newcomp = self.masterDerived() # New DTSTART is the RECURRENCE-ID we are deriving but adjusted to the # original DTSTART's localtime @@ -1223,7 +1221,7 @@ if newcomp.hasProperty("DTEND"): dtend.setValue(newdtstartValue + oldduration) - newcomp.addProperty(Property("RECURRENCE-ID", dtstart.value(), params={})) + newcomp.replaceProperty(Property("RECURRENCE-ID", dtstart.value(), params={})) if didCancel: newcomp.replaceProperty(Property("STATUS", "CANCELLED")) @@ -1232,7 +1230,30 @@ newcomp._pycalendar.finalise() return newcomp + + def masterDerived(self): + """ + Generate a component from the master instance that can be fed repeatedly to + deriveInstance in the case where the result of deriveInstance is not going + to be inserted into the component. This provides an optimization for avoiding + unnecessary .duplicate() calls on the master for each deriveInstance. + """ + + # Must have a master component + master = self.masterComponent() + if master is None: + return None + + # Create the derived instance + newcomp = master.duplicate() + + # Strip out unwanted recurrence properties + for property in tuple(newcomp.properties()): + if property.name() in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",): + newcomp.removeProperty(property) + return newcomp + def validInstances(self, rids, ignoreInvalidInstances=False): """ Test whether the specified recurrence-ids are valid instances in this event. Modified: CalendarServer/trunk/twistedcaldav/test/test_icalendar.py =================================================================== --- CalendarServer/trunk/twistedcaldav/test/test_icalendar.py 2012-06-08 01:10:12 UTC (rev 9341) +++ CalendarServer/trunk/twistedcaldav/test/test_icalendar.py 2012-06-08 01:11:47 UTC (rev 9342) @@ -4200,6 +4200,59 @@ self.assertEqual(ical.cachedInstances.limit, oldLimit) + def test_derive_instance_with_master_passed_in(self): + """ + Test that derivation of instances only triggers an instance cache re-expansion when it + goes past the end of the last cache. + """ + + event = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN +BEGIN:VEVENT +UID:12345-67890-1 +DTSTART:20090101T080000Z +DTEND:20090101T090000Z +DTSTAMP:20080601T120000Z +RRULE:FREQ=DAILY +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") + + derived1 = """BEGIN:VEVENT +UID:12345-67890-1 +RECURRENCE-ID:20090102T080000Z +DTSTART:20090102T080000Z +DTEND:20090102T090000Z +DTSTAMP:20080601T120000Z +END:VEVENT +""".replace("\n", "\r\n") + + derived2 = """BEGIN:VEVENT +UID:12345-67890-1 +RECURRENCE-ID:20090203T080000Z +DTSTART:20090203T080000Z +DTEND:20090203T090000Z +DTSTAMP:20080601T120000Z +END:VEVENT +""".replace("\n", "\r\n") + + ical = Component.fromString(event) + masterDerived = ical.masterDerived() + + # Derive one day apart - no re-cache + result = ical.deriveInstance(PyCalendarDateTime(2009, 1, 2, 8, 0, 0, tzid=PyCalendarTimezone(utc=True)), newcomp=masterDerived) + self.assertEqual(str(result), derived1) + + result = ical.deriveInstance(PyCalendarDateTime(2009, 2, 3, 8, 0, 0, tzid=PyCalendarTimezone(utc=True)), newcomp=masterDerived) + self.assertEqual(str(result), derived2) + + result = ical.deriveInstance(PyCalendarDateTime(2009, 3, 3, 9, 0, 0, tzid=PyCalendarTimezone(utc=True)), newcomp=masterDerived) + self.assertEqual(result, None) + + self.assertEqual(str(ical), event) + + def test_truncate_recurrence(self): data = (
participants (1)
-
source_changes@macosforge.org