[CalendarServer-changes] [9773] CalendarServer/branches/users/gaya/directorybacker

source_changes at macosforge.org source_changes at macosforge.org
Tue Sep 4 17:16:12 PDT 2012


Revision: 9773
          http://trac.macosforge.org/projects/calendarserver/changeset/9773
Author:   gaya at apple.com
Date:     2012-09-04 17:16:10 -0700 (Tue, 04 Sep 2012)
Log Message:
-----------
update to trunk

Modified Paths:
--------------
    CalendarServer/branches/users/gaya/directorybacker/calendarserver/tap/caldav.py
    CalendarServer/branches/users/gaya/directorybacker/calendarserver/tap/test/test_caldav.py
    CalendarServer/branches/users/gaya/directorybacker/calendarserver/tools/calverify.py
    CalendarServer/branches/users/gaya/directorybacker/contrib/tools/readStats.py
    CalendarServer/branches/users/gaya/directorybacker/support/build.sh
    CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/ical.py
    CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/instance.py
    CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/scheduling/caldav.py
    CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/scheduling/processing.py
    CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/test/test_icalendar.py

Property Changed:
----------------
    CalendarServer/branches/users/gaya/directorybacker/


Property changes on: CalendarServer/branches/users/gaya/directorybacker
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:9759-9772

Modified: CalendarServer/branches/users/gaya/directorybacker/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/calendarserver/tap/caldav.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/calendarserver/tap/caldav.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -1252,12 +1252,20 @@
 
         # Start listening on the stats socket, for administrators to inspect
         # the current stats on the server.
-        stats = CalDAVStatisticsServer(logger)
-        statsService = GroupOwnedUNIXServer(
-            gid, config.GlobalStatsSocket, stats, mode=0660
-        )
-        statsService.setName("stats")
-        statsService.setServiceParent(s)
+        if config.Stats.EnableUnixStatsSocket:
+            stats = CalDAVStatisticsServer(logger)
+            statsService = GroupOwnedUNIXServer(
+                gid, config.Stats.UnixStatsSocket, stats, mode=0660
+            )
+            statsService.setName("unix-stats")
+            statsService.setServiceParent(s)
+        if config.Stats.EnableTCPStatsSocket:
+            stats = CalDAVStatisticsServer(logger)
+            statsService = TCPServer(
+                config.Stats.TCPStatsPort, stats, interface=""
+            )
+            statsService.setName("tcp-stats")
+            statsService.setServiceParent(s)
 
         # Optionally enable Manhole access
         if config.Manhole.Enabled:
@@ -1320,7 +1328,7 @@
     def deleteStaleSocketFiles(self):
 
         # Check all socket files we use.
-        for checkSocket in [config.ControlSocket, config.GlobalStatsSocket] :
+        for checkSocket in [config.ControlSocket, config.Stats.UnixStatsSocket] :
 
             # See if the file exists.
             if (os.path.exists(checkSocket)):

Modified: CalendarServer/branches/users/gaya/directorybacker/calendarserver/tap/test/test_caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/calendarserver/tap/test/test_caldav.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/calendarserver/tap/test/test_caldav.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2007-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2007-2012 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.
@@ -459,6 +459,7 @@
         self.config.GroupName = grp.getgrgid(alternateGroup).gr_name
 
         self.config["ProcessType"] = "Combined"
+        self.config.Stats.EnableUnixStatsSocket = True
         self.writeConfig()
         svc = self.makeService()
         for serviceName in [_CONTROL_SERVICE_NAME]:
@@ -470,7 +471,7 @@
                 "Wrong mode on %s: %s" % (serviceName, oct(m))
             )
             self.assertEquals(socketService.gid, alternateGroup)
-        for serviceName in ["stats"]:
+        for serviceName in ["unix-stats"]:
             socketService = svc.getServiceNamed(serviceName)
             self.assertIsInstance(socketService, GroupOwnedUNIXServer)
             m = socketService.kwargs.get("mode", 0666)

Modified: CalendarServer/branches/users/gaya/directorybacker/calendarserver/tools/calverify.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/calendarserver/tools/calverify.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/calendarserver/tools/calverify.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -164,9 +164,34 @@
 
     return fixed, unfixed
 
+def new_hasDuplicateAlarms(self, doFix=False):
+    """
+    test and optionally remove alarms that have the same ACTION and TRIGGER values in the same component.
+    """
+    changed = False
+    if self.name() in ("VCALENDAR", "X-CALENDARSERVER-PERUSER",):
+        for component in self.subcomponents():
+            if component.name() in ("VTIMEZONE",):
+                continue
+            changed = component.hasDuplicateAlarms(doFix) or changed
+    else:
+        action_trigger = set()
+        for component in tuple(self.subcomponents()):
+            if component.name() == "VALARM":
+                item = (component.propertyValue("ACTION"), component.propertyValue("TRIGGER"),)
+                if item in action_trigger:
+                    if doFix:
+                        self.removeComponent(component)
+                    changed = True
+                else:
+                    action_trigger.add(item)
+    return changed
+
 Component.validRecurrenceIDs = new_validRecurrenceIDs
+if not hasattr(Component, "maxAlarmCounts"):
+    Component.hasDuplicateAlarms = new_hasDuplicateAlarms
 
-VERSION = "6"
+VERSION = "7"
 
 def printusage(e=None):
     if e:
@@ -934,6 +959,8 @@
                 component.validCalendarData(doFix=False, validateRecurrences=True)
                 component.validCalendarForCalDAV(methodAllowed=isinbox)
                 component.validOrganizerForScheduling(doFix=False)
+                if component.hasDuplicateAlarms(doFix=False):
+                    raise InvalidICalendarDataError("Duplicate VALARMS")
             self.noPrincipalPathCUAddresses(component, doFix=False)
         except ValueError, e:
             result = False
@@ -1058,6 +1085,7 @@
                 component.validCalendarData(doFix=True, validateRecurrences=True)
                 component.validCalendarForCalDAV(methodAllowed=isinbox)
                 component.validOrganizerForScheduling(doFix=True)
+                component.hasDuplicateAlarms(doFix=True)
             self.noPrincipalPathCUAddresses(component, doFix=True)
         except ValueError:
             result = False

Modified: CalendarServer/branches/users/gaya/directorybacker/contrib/tools/readStats.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/contrib/tools/readStats.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/contrib/tools/readStats.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -30,23 +30,42 @@
 def safeDivision(value, total, factor=1):
     return value * factor / total if total else 0
 
-def readSock(sockname):
-    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-    s.connect(sockname)
-    data = ""
-    while True:
-        d = s.recv(1024)
-        if d:
-            data += d
-        else:
-            break
-    s.close()
+def readSock(sockname, useTCP):
+    try:
+        s = socket.socket(socket.AF_INET if useTCP else socket.AF_UNIX, socket.SOCK_STREAM)
+        s.connect(sockname)
+        data = ""
+        while True:
+            d = s.recv(1024)
+            if d:
+                data += d
+            else:
+                break
+        s.close()
+        data = json.loads(data)
+    except socket.error:
+        data = {"Failed": "Unable to read statistics from server: %s" % (sockname,)}
+    data["Server"] = sockname
     return data
 
-def printStats(data):
+def printStats(stats):
+    if len(stats) == 1 and False:
+        if "Failed" in stats[0]:
+            printFailedStats(stats[0]["Failed"]) 
+        else:
+            try:
+                printStat(stats[0])
+            except KeyError, e:
+                printFailedStats("Unable to find key '%s' in statistics from server socket" % (e,))
+                sys.exit(1)
+            
+    else:
+        printMultipleStats(stats)
+        
+def printStat(stats):
     
-    stats = json.loads(data)
     print "- " * 40
+    print "Server: %s" % (stats["Server"],)
     print datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
     print "Service Uptime: %s" % (datetime.timedelta(seconds=(int(time.time() - stats["System"]["start time"]))),)
     if stats["System"]["cpu count"] > 0:
@@ -66,6 +85,42 @@
     printRequestSummary(stats)
     printHistogramSummary(stats["5 Minutes"])
 
+def printMultipleStats(stats):
+
+    labels = serverLabels(stats)
+ 
+    print "- " * 40
+    print "Servers: %s" % (", ".join(labels),)
+
+    print datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
+
+    times = []
+    for stat in stats:
+        try:
+            t = str(datetime.timedelta(seconds=int(time.time() - stat["System"]["start time"])))
+        except KeyError:
+            t = "-"
+        times.append(t)
+    print "Service Uptime: %s" % (", ".join(times),)
+
+    cpus = []
+    memories = []
+    for stat in stats:
+        if stat["System"]["cpu count"] > 0:
+            cpus.append("%.1f%%" % (stat["System"]["cpu use"],))
+            memories.append("%.1f%%" % (stat["System"]["memory percent"],))
+        else:
+            cpus.append("-")
+            memories("-")
+    print "Current CPU: %s" % (", ".join(cpus),)
+    print "Current Memory Used: %s" % (", ".join(memories),)
+    print
+    printMultiRequestSummary(stats, labels, ("5 Minutes", 5*60,))
+    printMultiHistogramSummary(stats, "5 Minutes")
+
+def serverLabels(stats):
+    return [str(stat["Server"]) for stat in stats]
+
 def printFailedStats(message):
     
     print "- " * 40
@@ -114,6 +169,57 @@
     table.printTable(os=os)
     print os.getvalue()
 
+def printMultiRequestSummary(stats, labels, index):
+    table = tables.Table()
+    table.addHeader(
+        ("Server", "Requests", "Av. Requests", "Av. Response", "Av. Response", "Max. Response",    "Slot",     "CPU", "500's"),
+    )
+    table.addHeader(
+        (      "",         "",   "per second",         "(ms)", "no write(ms)",          "(ms)", "Average", "Average",      ""),
+    )
+    table.setDefaultColumnFormats(
+       (
+            tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY), 
+            tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.2f", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+        )
+    )
+    
+    key, seconds = index
+    totals = ["Overall:", 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0]
+    for ctr, stat in enumerate(stats):
+
+        stat = stat[key]
+        
+        col = []
+        col.append(labels[ctr])
+        col.append(stat["requests"])
+        col.append(safeDivision(float(stat["requests"]), seconds))
+        col.append(safeDivision(stat["t"], stat["requests"]))
+        col.append(safeDivision(stat["t"] - stat["t-resp-wr"], stat["requests"]))
+        col.append(stat["T-MAX"])
+        col.append(safeDivision(float(stat["slots"]), stat["requests"]))
+        col.append(safeDivision(stat["cpu"], stat["requests"]))
+        col.append(stat["500"])
+        table.addRow(col)
+        for item in xrange(1, len(col)):
+            totals[item] += col[item]
+    
+    for item in (2, 3, 4, 6, 7):
+        totals[item] /= len(stats)
+    
+    table.addFooter(totals)
+
+    os = StringIO()
+    table.printTable(os=os)
+    print os.getvalue()
+
 def printHistogramSummary(stat):
     
     print "5 minute average response histogram"
@@ -152,7 +258,58 @@
     table.printTable(os=os)
     print os.getvalue()
     
+def printMultiHistogramSummary(stats, index):
     
+    # Totals first
+    keys = ("requests", "<10ms", "10ms<->100ms", "100ms<->1s", "1s<->10s", "10s<->30s", "30s<->60s", ">60s", "Over 1s", "Over 10s",)
+    totals = {
+        "T"        : dict([(k, 0) for k in keys]),
+        "T-RESP-WR": dict([(k, 0) for k in keys]),
+    }
+    
+    for stat in stats:
+        for i in ("T", "T-RESP-WR",):
+            totals[i][keys[0]] += stat[index][keys[0]]
+            for k in keys[1:]:
+                totals[i][k] += stat[index][i][k]
+
+    print "5 minute average response histogram"
+    table = tables.Table()
+    table.addHeader(
+        ("", "<10ms", "10ms<->100ms", "100ms<->1s", "1s<->10s", "10s<->30s", "30s<->60s", ">60s",  "Over 1s", "Over 10s"),
+    )
+    table.setDefaultColumnFormats(
+       (
+            tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.CENTER_JUSTIFY), 
+            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+        )
+    )
+    for i in ("T", "T-RESP-WR",):
+        table.addRow((
+            "Overall Response" if i == "T" else "Response without Write",
+            (totals[i]["<10ms"], safeDivision(totals[i]["<10ms"], totals[i]["requests"], 100.0)),
+            (totals[i]["10ms<->100ms"], safeDivision(totals[i]["10ms<->100ms"], totals[i]["requests"], 100.0)),
+            (totals[i]["100ms<->1s"], safeDivision(totals[i]["100ms<->1s"], totals[i]["requests"], 100.0)),
+            (totals[i]["1s<->10s"], safeDivision(totals[i]["1s<->10s"], totals[i]["requests"], 100.0)),
+            (totals[i]["10s<->30s"], safeDivision(totals[i]["10s<->30s"], totals[i]["requests"], 100.0)),
+            (totals[i]["30s<->60s"], safeDivision(totals[i]["30s<->60s"], totals[i]["requests"], 100.0)),
+            (totals[i][">60s"], safeDivision(totals[i][">60s"], totals[i]["requests"], 100.0)),
+            safeDivision(totals[i]["Over 1s"], totals[i]["requests"], 100.0),
+            safeDivision(totals[i]["Over 10s"], totals[i]["requests"], 100.0),
+        ))
+    os = StringIO()
+    table.printTable(os=os)
+    print os.getvalue()
+    
+    
 def usage(error_msg=None):
     if error_msg:
         print error_msg
@@ -162,6 +319,7 @@
     -h            Print this help and exit
     -s            Name of local socket to read from
     -t            Delay in seconds between each sample [10 seconds]
+    --tcp host:port Use TCP connection with host:port
 
 Description:
     This utility will print a summary of statistics read from a
@@ -177,25 +335,22 @@
 if __name__ == '__main__':
     
     delay = 10
-    sockname = "data/Logs/state/caldavd-stats.sock"
+    servers = ("data/Logs/state/caldavd-stats.sock",)
+    useTCP = False
 
-    options, args = getopt.getopt(sys.argv[1:], "hs:t:", [])
+    options, args = getopt.getopt(sys.argv[1:], "hs:t:", ["tcp=",])
 
     for option, value in options:
         if option == "-h":
             usage()
         elif option == "-s":
-            sockname = value
+            servers = value.split(",")
         elif option == "-t":
             delay = int(value)
+        elif option == "--tcp":
+            servers = [(host, int(port),) for host, port in [server.split(":") for server in value.split(",")]]
+            useTCP = True
 
     while True:
-        try:
-            printStats(readSock(sockname))
-        except socket.error:
-            printFailedStats("Unable to read statistics from server socket: %s" % (sockname,))
-        except KeyError, e:
-            printFailedStats("Unable to find key '%s' in statistics from server socket" % (e,))
-            sys.exit(1)
-
+        printStats([readSock(server, useTCP) for server in servers])
         time.sleep(delay)

Modified: CalendarServer/branches/users/gaya/directorybacker/support/build.sh
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/support/build.sh	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/support/build.sh	2012-09-05 00:16:10 UTC (rev 9773)
@@ -1,7 +1,7 @@
 # -*- sh-basic-offset: 2 -*-
 
 ##
-# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2012 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.
@@ -743,7 +743,7 @@
     "${pypi}/p/python-ldap/${ld}.tar.gz";
 
   # XXX actually PyCalendar should be imported in-place.
-  py_dependency -fe -i "src" -r 211 \
+  py_dependency -fe -i "src" -r 212 \
     "pycalendar" "pycalendar" "pycalendar" \
     "http://svn.mulberrymail.com/repos/PyCalendar/branches/server";
 

Modified: CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/ical.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/ical.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -2301,7 +2301,30 @@
             for component in tuple(self.subcomponents()):
                 if component.name() == "VALARM":
                     self.removeComponent(component)
-                
+
+    def hasDuplicateAlarms(self, doFix=False):
+        """
+        Test and optionally remove alarms that have the same ACTION and TRIGGER values in the same component.
+        """
+        changed = False
+        if self.name() in ("VCALENDAR", "X-CALENDARSERVER-PERUSER",):
+            for component in self.subcomponents():
+                if component.name() in ("VTIMEZONE",):
+                    continue
+                changed = component.hasDuplicateAlarms(doFix) or changed
+        else:
+            action_trigger = set()
+            for component in tuple(self.subcomponents()):
+                if component.name() == "VALARM":
+                    item = (component.propertyValue("ACTION"), component.propertyValue("TRIGGER"),)
+                    if item in action_trigger:
+                        if doFix:
+                            self.removeComponent(component)
+                        changed = True
+                    else:
+                        action_trigger.add(item)
+        return changed
+
     def filterProperties(self, remove=None, keep=None, do_subcomponents=True):
         """
         Remove all properties that do not match the provided set.

Modified: CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/instance.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/instance.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/instance.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -351,7 +351,7 @@
 
         # Make sure override RECURRENCE-ID is a valid instance of the master
         if got_master:
-            if str(rid) not in self.instances and rid < upperlimit:
+            if str(rid) not in self.instances and rid < upperlimit and (lowerLimit is None or rid >= lowerLimit):
                 if self.ignoreInvalidInstances:
                     return
                 else:

Modified: CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/method/put_common.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/method/put_common.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -805,41 +805,45 @@
         
         returnValue(changed)
 
-    def addDefaultAlarm(self):
+    def processAlarms(self):
         """
-        Add a default alarm if required.
+        Remove duplicate alarms. Add a default alarm if required.
         
         @return: indicate whether a change was made
         @rtype: C{bool}
         """
 
+        # Remove duplicate alarms
+        changed = False
+        if config.RemoveDuplicateAlarms:
+            changed = self.calendar.hasDuplicateAlarms(doFix=True)
+
         # Only if feature enabled
         if not config.EnableDefaultAlarms:
-            return False
+            return changed
 
         # Check that we are creating and this is not the inbox
         if not self.destinationcal or self.destination.exists() or self.isiTIP:
-            return False
+            return changed
         
         # Never add default alarms to calendar data in shared calendars
         if self.destinationparent.isVirtualShare():
-            return False
+            return changed
 
         # Add default alarm for VEVENT and VTODO only
         mtype = self.calendar.mainType().upper()
         if self.calendar.mainType().upper() not in ("VEVENT", "VTODO"):
-            return False
+            return changed
         vevent = mtype == "VEVENT"
         
         # Check timed or all-day
         start, _ignore_end = self.calendar.mainComponent(allow_multiple=True).getEffectiveStartEnd()
         if start is None:
             # Yes VTODOs might have no DTSTART or DUE - in this case we do not add a default
-            return False
+            return changed
         timed = not start.isDateOnly()
         
         # See if default exists and add using appropriate logic
-        changed = False
         alarm = self.destinationparent.getDefaultAlarm(vevent, timed)
         if alarm:
             changed = self.calendar.addAlarms(alarm)
@@ -1167,8 +1171,8 @@
             # Handle sharing dropbox normalization
             dropboxChanged = (yield self.dropboxPathNormalization())
 
-            # Default alarms
-            alarmChanged = self.addDefaultAlarm()
+            # Default/duplicate alarms
+            alarmChanged = self.processAlarms()
 
             # Do scheduling
             implicit_result = (yield self.doImplicitScheduling())

Modified: CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/scheduling/caldav.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/scheduling/caldav.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -14,19 +14,16 @@
 # limitations under the License.
 ##
 
-import uuid
-
 from twext.python.log import Logger
+from twext.web2 import responsecode
 from twext.web2.dav.http import ErrorResponse
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.python.failure import Failure
-from twext.web2 import responsecode
-from txdav.xml import element as davxml
 from twext.web2.dav.resource import AccessDeniedError
 from twext.web2.dav.util import joinURL
 from twext.web2.http import HTTPError
 
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.failure import Failure
+
 from twistedcaldav import caldavxml
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
@@ -39,6 +36,11 @@
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
 from twistedcaldav.scheduling.processing import ImplicitProcessor, ImplicitProcessorException
 
+from txdav.xml import element as davxml
+
+import hashlib
+import uuid
+
 """
 Handles the sending of scheduling messages to the server itself. This will cause
 actual processing of the delivery of the message to the recipient's inbox, via the
@@ -141,7 +143,7 @@
     @inlineCallbacks
     def generateResponse(self, recipient, responses):
         # Hash the iCalendar data for use as the last path element of the URI path
-        name = str(uuid.uuid4()) + ".ics"
+        name =  "%s-%s.ics" % (hashlib.md5(self.scheduler.calendar.resourceUID()).hexdigest(), str(uuid.uuid4())[:8],)
     
         # Get a resource for the new item
         childURL = joinURL(recipient.inboxURL, name)

Modified: CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/scheduling/processing.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/scheduling/processing.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -37,6 +37,7 @@
 from pycalendar.datetime import PyCalendarDateTime
 from pycalendar.timezone import PyCalendarTimezone
 import uuid
+import hashlib
 
 """
 CalDAV implicit processing.
@@ -471,7 +472,6 @@
 
             log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - new processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
             new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr)
-            name =  str(uuid.uuid4()) + ".ics"
             
             # Handle auto-reply behavior
             if self.recipient.principal.canAutoSchedule():
@@ -483,7 +483,7 @@
                 send_reply = False
                 store_inbox = True
 
-            new_resource = (yield self.writeCalendarResource(default.url(), default, name, new_calendar))
+            new_resource = (yield self.writeCalendarResource(default.url(), default, None, new_calendar))
             
             if send_reply:
                 # Track outstanding auto-reply processing
@@ -900,7 +900,7 @@
         
         # Create a new name if one was not provided
         if name is None:
-            name =  str(uuid.uuid4()) + ".ics"
+            name =  "%s-%s.ics" % (hashlib.md5(calendar.resourceUID()).hexdigest(), str(uuid.uuid4())[:8],)
     
         # Get a resource for the new item
         newchildURL = joinURL(collURL, name)

Modified: CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/stdconfig.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/stdconfig.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -482,7 +482,12 @@
     "AccountingPrincipals": [],
     "AccountingLogRoot"   : "accounting",
 
-    "GlobalStatsSocket"           : "caldavd-stats.sock", 
+    "Stats" : {
+        "EnableUnixStatsSocket"  : False,
+        "UnixStatsSocket"        : "caldavd-stats.sock",
+        "EnableTCPStatsSocket"   : False,
+        "TCPStatsPort"           : 8100,
+    },
     
     "LogDatabase" : {
         "LabelsInSQL"            : False,
@@ -597,7 +602,8 @@
                             # the database, merge the data from the filesystem
                             # into the database homes.
 
-    "EnableDefaultAlarms" : True, # Support for default alarms generated by the server
+    "EnableDefaultAlarms" :  True, # Support for default alarms generated by the server
+    "RemoveDuplicateAlarms": True,  # Remove duplicate alarms on PUT
 
     # CardDAV Features
     "DirectoryAddressBook": {
@@ -1062,7 +1068,7 @@
     ("LogRoot", ("LogDatabase", "StatisticsLogFile",)),
     ("LogRoot", "AccountingLogRoot"),
     ("RunRoot", "PIDFile"),
-    ("RunRoot", "GlobalStatsSocket"),
+    ("RunRoot", ("Stats", "UnixStatsSocket",)),
     ("RunRoot", "ControlSocket"),
 ]
 

Modified: CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/test/test_icalendar.py	2012-09-04 23:11:57 UTC (rev 9772)
+++ CalendarServer/branches/users/gaya/directorybacker/twistedcaldav/test/test_icalendar.py	2012-09-05 00:16:10 UTC (rev 9773)
@@ -7441,3 +7441,613 @@
         component.removeComponent(subComponent)
         self.assertEquals(subComponent._parent, None)
         self.assertEquals(component._cachedCopy, None) # cache is invalidated
+
+    def test_hasDuplicateAlarms(self):
+        """
+        Test that L{Component.hasDuplicateAlarms} correctly detects, but does not fix, duplicate alarms.
+        """
+
+        data = (
+            # No Alarms
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            
+            # One alarm
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            
+            # Two different alarms
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            
+            # Two duplicates, one different
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            
+            # Two duplicates in one component, three different in another
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:PT0S
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            
+            # Four duplicates in X-CALENDARSERVER-PERINSTANCE
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+                True,
+            ),
+        )
+
+        for txt, result in data: 
+            component = Component.fromString(txt)
+            self.assertEqual(component.hasDuplicateAlarms(doFix=False), result)
+            self.assertEqual(str(component), txt.replace("\n", "\r\n"))
+
+    def test_hasDuplicateAlarms_withFix(self):
+        """
+        Test that L{Component.hasDuplicateAlarms} correctly removes duplicate alarms.
+        """
+
+        data = (
+            # No Alarms
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+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
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            
+            # One alarm
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            
+            # Two different alarms
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            
+            # Two duplicates, one different
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            
+            # Two duplicates in one component, three different in another
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT0M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:PT0S
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            
+            # Four duplicates in X-CALENDARSERVER-PERINSTANCE
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+                True,
+            ),
+        )
+
+        for txt, result, result_changed in data: 
+            component = Component.fromString(txt)
+            changed = component.hasDuplicateAlarms(doFix=True)
+            self.assertEqual(str(component), result.replace("\n", "\r\n"))
+            self.assertEqual(changed, result_changed)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120904/b2b52197/attachment-0001.html>


More information about the calendarserver-changes mailing list